tempo_cli/ui/
dashboard.rs

1use anyhow::Result;
2use chrono::{Local, Timelike};
3use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
4use log::debug;
5use ratatui::{
6    backend::Backend,
7    buffer::Buffer,
8    layout::{Alignment, Constraint, Direction, Layout, Rect},
9    style::{Color, Modifier, Style},
10    text::{Line, Span},
11    widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table, Widget},
12    Frame, Terminal,
13};
14use std::time::{Duration, Instant};
15
16use crate::{
17    models::{Project, Session},
18    ui::formatter::Formatter,
19    ui::widgets::{ColorScheme, SessionStatsWidget, Spinner},
20    utils::ipc::{
21        get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse, ProjectWithStats,
22    },
23};
24
25#[derive(Clone, PartialEq)]
26pub enum DashboardView {
27    FocusedSession,
28    Overview,
29    History,
30    Projects,
31}
32
33#[derive(Clone)]
34pub struct SessionFilter {
35    pub start_date: Option<chrono::NaiveDate>,
36    pub end_date: Option<chrono::NaiveDate>,
37    pub project_filter: Option<String>,
38    pub duration_filter: Option<(i64, i64)>, // min, max seconds
39    pub search_text: String,
40}
41
42impl Default for SessionFilter {
43    fn default() -> Self {
44        Self {
45            start_date: None,
46            end_date: None,
47            project_filter: None,
48            duration_filter: None,
49            search_text: String::new(),
50        }
51    }
52}
53
54pub struct Dashboard {
55    client: IpcClient,
56    current_session: Option<Session>,
57    current_project: Option<Project>,
58    daily_stats: (i64, i64, i64),
59    weekly_stats: i64,
60    today_sessions: Vec<Session>,
61    recent_projects: Vec<ProjectWithStats>,
62    available_projects: Vec<Project>,
63    selected_project_index: usize,
64    show_project_switcher: bool,
65    current_view: DashboardView,
66    
67    // History browser state
68    history_sessions: Vec<Session>,
69    selected_session_index: usize,
70    session_filter: SessionFilter,
71    filter_input_mode: bool,
72    
73    // Project grid state
74    selected_project_row: usize,
75    selected_project_col: usize,
76    projects_per_row: usize,
77    
78    spinner: Spinner,
79    last_update: Instant,
80}
81
82impl Dashboard {
83    pub async fn new() -> Result<Self> {
84        let socket_path = get_socket_path()?;
85        let client = if socket_path.exists() && is_daemon_running() {
86            IpcClient::connect(&socket_path)
87                .await
88                .unwrap_or_else(|_| IpcClient::new().unwrap())
89        } else {
90            IpcClient::new()?
91        };
92        Ok(Self {
93            client,
94            current_session: None,
95            current_project: None,
96            daily_stats: (0, 0, 0),
97            weekly_stats: 0,
98            today_sessions: Vec::new(),
99            recent_projects: Vec::new(),
100            available_projects: Vec::new(),
101            selected_project_index: 0,
102            show_project_switcher: false,
103            current_view: DashboardView::FocusedSession,
104            
105            // Initialize history browser state
106            history_sessions: Vec::new(),
107            selected_session_index: 0,
108            session_filter: SessionFilter::default(),
109            filter_input_mode: false,
110            
111            // Initialize project grid state
112            selected_project_row: 0,
113            selected_project_col: 0,
114            projects_per_row: 3,
115            
116            spinner: Spinner::new(),
117            last_update: Instant::now(),
118        })
119    }
120
121    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
122        loop {
123            // Update state
124            self.update_state().await?;
125
126            terminal.draw(|f| self.render_dashboard_sync(f))?;
127
128            if event::poll(Duration::from_millis(100))? {
129                match event::read()? {
130                    Event::Key(key) if key.kind == KeyEventKind::Press => {
131                        if self.show_project_switcher {
132                            self.handle_project_switcher_input(key).await?;
133                        } else {
134                            // Handle global exit here
135                            match key.code {
136                                KeyCode::Char('q') => break,
137                                KeyCode::Esc => {
138                                    if self.current_view == DashboardView::FocusedSession {
139                                        self.current_view = DashboardView::Overview;
140                                    } else {
141                                        break;
142                                    }
143                                }
144                                _ => self.handle_dashboard_input(key).await?,
145                            }
146                        }
147                    }
148                    _ => {}
149                }
150            }
151        }
152        Ok(())
153    }
154    async fn update_state(&mut self) -> Result<()> {
155        // Send activity heartbeat (throttled)
156        if self.last_update.elapsed() >= Duration::from_secs(3) {
157            if let Err(e) = self.send_activity_heartbeat().await {
158                debug!("Heartbeat error: {}", e);
159            }
160            self.last_update = Instant::now();
161        }
162
163        // Tick animations
164        self.spinner.next();
165
166        // Get current status
167        self.current_session = self.get_current_session().await?;
168
169        // Clone session to avoid borrow conflict
170        let session_clone = self.current_session.clone();
171        if let Some(session) = session_clone {
172            self.current_project = self.get_project_by_session(&session).await?;
173        } else {
174            self.current_project = None;
175        }
176
177        self.daily_stats = self.get_today_stats().await.unwrap_or((0, 0, 0));
178        self.weekly_stats = self.get_weekly_stats().await.unwrap_or(0);
179        self.today_sessions = self.get_today_sessions().await.unwrap_or_default();
180        self.recent_projects = self.get_recent_projects().await.unwrap_or_default();
181        
182        // Update history sessions if in history view
183        if self.current_view == DashboardView::History {
184            self.history_sessions = self.get_history_sessions().await.unwrap_or_default();
185        }
186        
187        // Update project list if in project view
188        if self.current_view == DashboardView::Projects && self.available_projects.is_empty() {
189            if let Err(_) = self.refresh_projects().await {
190                // Ignore errors and use empty list
191            }
192        }
193
194        Ok(())
195    }
196
197    async fn get_weekly_stats(&mut self) -> Result<i64> {
198        match self.client.send_message(&IpcMessage::GetWeeklyStats).await {
199            Ok(IpcResponse::WeeklyStats { total_seconds }) => Ok(total_seconds),
200            Ok(response) => {
201                debug!("Unexpected response for GetWeeklyStats: {:?}", response);
202                Err(anyhow::anyhow!("Unexpected response"))
203            }
204            Err(e) => {
205                debug!("Failed to receive GetWeeklyStats response: {}", e);
206                Err(anyhow::anyhow!("Failed to receive response"))
207            }
208        }
209    }
210
211    async fn get_recent_projects(&mut self) -> Result<Vec<ProjectWithStats>> {
212        match self
213            .client
214            .send_message(&IpcMessage::GetRecentProjects)
215            .await
216        {
217            Ok(IpcResponse::RecentProjects(projects)) => Ok(projects),
218            Ok(response) => {
219                debug!("Unexpected response for GetRecentProjects: {:?}", response);
220                Err(anyhow::anyhow!("Unexpected response"))
221            }
222            Err(e) => {
223                debug!("Failed to receive GetRecentProjects response: {}", e);
224                Err(anyhow::anyhow!("Failed to receive response"))
225            }
226        }
227    }
228
229    async fn handle_dashboard_input(&mut self, key: KeyEvent) -> Result<()> {
230        // Handle view-specific inputs first
231        match self.current_view {
232            DashboardView::History => {
233                return self.handle_history_input(key).await;
234            }
235            DashboardView::Projects => {
236                return self.handle_project_grid_input(key).await;
237            }
238            _ => {}
239        }
240
241        // Handle global navigation
242        match key.code {
243            // View navigation
244            KeyCode::Char('1') => self.current_view = DashboardView::FocusedSession,
245            KeyCode::Char('2') => self.current_view = DashboardView::Overview,
246            KeyCode::Char('3') => self.current_view = DashboardView::History,
247            KeyCode::Char('4') => self.current_view = DashboardView::Projects,
248            KeyCode::Char('f') => self.current_view = DashboardView::FocusedSession,
249            KeyCode::Tab => {
250                self.current_view = match self.current_view {
251                    DashboardView::FocusedSession => DashboardView::Overview,
252                    DashboardView::Overview => DashboardView::History,
253                    DashboardView::History => DashboardView::Projects,
254                    DashboardView::Projects => DashboardView::FocusedSession,
255                };
256            }
257            // Project switcher (only in certain views)
258            KeyCode::Char('p') if self.current_view != DashboardView::Projects => {
259                self.refresh_projects().await?;
260                self.show_project_switcher = true;
261            }
262            _ => {}
263        }
264        Ok(())
265    }
266
267    async fn handle_history_input(&mut self, key: KeyEvent) -> Result<()> {
268        match key.code {
269            // Navigation in session list
270            KeyCode::Up | KeyCode::Char('k') => {
271                if !self.history_sessions.is_empty() && self.selected_session_index > 0 {
272                    self.selected_session_index -= 1;
273                }
274            }
275            KeyCode::Down | KeyCode::Char('j') => {
276                if self.selected_session_index < self.history_sessions.len().saturating_sub(1) {
277                    self.selected_session_index += 1;
278                }
279            }
280            // Search mode
281            KeyCode::Char('/') => {
282                self.filter_input_mode = true;
283            }
284            KeyCode::Enter if self.filter_input_mode => {
285                self.filter_input_mode = false;
286                self.history_sessions = self.get_history_sessions().await.unwrap_or_default();
287            }
288            // Character input in search mode
289            KeyCode::Char(c) if self.filter_input_mode => {
290                self.session_filter.search_text.push(c);
291            }
292            KeyCode::Backspace if self.filter_input_mode => {
293                self.session_filter.search_text.pop();
294            }
295            KeyCode::Esc if self.filter_input_mode => {
296                self.filter_input_mode = false;
297                self.session_filter.search_text.clear();
298            }
299            _ => {}
300        }
301        Ok(())
302    }
303
304    async fn handle_project_grid_input(&mut self, key: KeyEvent) -> Result<()> {
305        match key.code {
306            // Grid navigation
307            KeyCode::Up | KeyCode::Char('k') => {
308                if self.selected_project_row > 0 {
309                    self.selected_project_row -= 1;
310                }
311            }
312            KeyCode::Down | KeyCode::Char('j') => {
313                let total_projects = self.available_projects.len();
314                let total_rows = (total_projects + self.projects_per_row - 1) / self.projects_per_row;
315                if self.selected_project_row < total_rows.saturating_sub(1) {
316                    // Only move down if there's a project on the next row
317                    let next_row_first_index = (self.selected_project_row + 1) * self.projects_per_row;
318                    if next_row_first_index < total_projects {
319                        self.selected_project_row += 1;
320                    }
321                }
322            }
323            KeyCode::Left | KeyCode::Char('h') => {
324                if self.selected_project_col > 0 {
325                    self.selected_project_col -= 1;
326                }
327            }
328            KeyCode::Right | KeyCode::Char('l') => {
329                let row_start = self.selected_project_row * self.projects_per_row;
330                let row_end = (row_start + self.projects_per_row).min(self.available_projects.len());
331                let max_col = (row_end - row_start).saturating_sub(1);
332                if self.selected_project_col < max_col {
333                    self.selected_project_col += 1;
334                }
335            }
336            // Project selection
337            KeyCode::Enter => {
338                self.switch_to_grid_selected_project().await?;
339            }
340            _ => {}
341        }
342        Ok(())
343    }
344
345    async fn handle_project_switcher_input(&mut self, key: KeyEvent) -> Result<()> {
346        match key.code {
347            KeyCode::Esc => {
348                self.show_project_switcher = false;
349            }
350            KeyCode::Up | KeyCode::Char('k') => {
351                self.navigate_projects(-1);
352            }
353            KeyCode::Down | KeyCode::Char('j') => {
354                self.navigate_projects(1);
355            }
356            KeyCode::Enter => {
357                self.switch_to_selected_project().await?;
358            }
359            _ => {}
360        }
361        Ok(())
362    }
363
364    async fn ensure_connected(&mut self) -> Result<()> {
365        if !is_daemon_running() {
366            return Err(anyhow::anyhow!("Daemon is not running"));
367        }
368
369        // Test if we have a working connection
370        if self.client.stream.is_some() {
371            return Ok(());
372        }
373
374        // Reconnect if needed
375        let socket_path = get_socket_path()?;
376        if socket_path.exists() {
377            self.client = IpcClient::connect(&socket_path).await?;
378        }
379        Ok(())
380    }
381
382    async fn switch_to_grid_selected_project(&mut self) -> Result<()> {
383        let selected_index = self.selected_project_row * self.projects_per_row + self.selected_project_col;
384        if let Some(selected_project) = self.available_projects.get(selected_index) {
385            let project_id = selected_project.id.unwrap_or(0);
386
387            self.ensure_connected().await?;
388
389            // Switch to the selected project
390            let response = self
391                .client
392                .send_message(&IpcMessage::SwitchProject(project_id))
393                .await?;
394            match response {
395                IpcResponse::Success => {
396                    // Switch to focused view after selection
397                    self.current_view = DashboardView::FocusedSession;
398                }
399                IpcResponse::Error(e) => {
400                    return Err(anyhow::anyhow!("Failed to switch project: {}", e))
401                }
402                _ => return Err(anyhow::anyhow!("Unexpected response")),
403            }
404        }
405        Ok(())
406    }
407
408    fn render_keyboard_hints(&self, area: Rect, buf: &mut Buffer) {
409        let hints = match self.current_view {
410            DashboardView::FocusedSession => vec![
411                ("Esc", "Exit Focus"),
412                ("Tab", "Next View"),
413                ("p", "Projects"),
414            ],
415            DashboardView::History => vec![
416                ("↑/↓", "Navigate"),
417                ("/", "Search"),
418                ("Tab", "Next View"),
419                ("q", "Quit"),
420            ],
421            DashboardView::Projects => vec![
422                ("↑/↓/←/→", "Navigate"),
423                ("Enter", "Select"),
424                ("Tab", "Next View"),
425                ("q", "Quit"),
426            ],
427            _ => vec![
428                ("q", "Quit"),
429                ("f", "Focus"),
430                ("Tab", "Next View"), 
431                ("1-4", "View"),
432                ("p", "Projects"),
433            ],
434        };
435
436        let spans: Vec<Span> = hints
437            .iter()
438            .flat_map(|(key, desc)| {
439                vec![
440                    Span::styled(
441                        format!(" {} ", key),
442                        Style::default()
443                            .fg(Color::Yellow)
444                            .add_modifier(Modifier::BOLD),
445                    ),
446                    Span::styled(format!("{} ", desc), Style::default().fg(Color::DarkGray)),
447                ]
448            })
449            .collect();
450
451        let line = Line::from(spans);
452        let block = Block::default()
453            .borders(Borders::TOP)
454            .border_style(Style::default().fg(Color::DarkGray));
455        Paragraph::new(line).block(block).render(area, buf);
456    }
457
458    fn render_dashboard_sync(&mut self, f: &mut Frame) {
459        match self.current_view {
460            DashboardView::FocusedSession => self.render_focused_session_view(f),
461            DashboardView::Overview => self.render_overview_dashboard(f),
462            DashboardView::History => self.render_history_browser(f),
463            DashboardView::Projects => self.render_project_grid(f),
464        }
465
466        // Project switcher overlay (available on most views)
467        if self.show_project_switcher {
468            self.render_project_switcher(f, f.size());
469        }
470    }
471
472    fn render_focused_session_view(&mut self, f: &mut Frame) {
473        let chunks = Layout::default()
474            .direction(Direction::Vertical)
475            .constraints([
476                Constraint::Length(3),  // Header with ESC hint
477                Constraint::Length(2),  // Spacer
478                Constraint::Length(6),  // Project info box
479                Constraint::Length(2),  // Spacer
480                Constraint::Length(8),  // Large timer box
481                Constraint::Length(2),  // Spacer
482                Constraint::Length(8),  // Session details
483                Constraint::Min(0),     // Bottom spacer
484                Constraint::Length(1),  // Footer
485            ])
486            .split(f.size());
487
488        // Top header with ESC hint
489        let header_layout = Layout::default()
490            .direction(Direction::Horizontal)
491            .constraints([Constraint::Percentage(100)])
492            .split(chunks[0]);
493
494        f.render_widget(
495            Paragraph::new("Press ESC to exit focused mode.")
496                .alignment(Alignment::Center)
497                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
498            header_layout[0],
499        );
500
501        if let (Some(session), Some(project)) = (&self.current_session, &self.current_project) {
502            // Project info box
503            let project_area = self.centered_rect(60, 20, chunks[2]);
504            let project_block = Block::default()
505                .borders(Borders::ALL)
506                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
507                .style(Style::default().bg(ColorScheme::CLEAN_BG));
508
509            let project_layout = Layout::default()
510                .direction(Direction::Vertical)
511                .constraints([
512                    Constraint::Length(1),
513                    Constraint::Length(1),
514                    Constraint::Length(1),
515                    Constraint::Length(1),
516                ])
517                .margin(1)
518                .split(project_area);
519
520            f.render_widget(project_block, project_area);
521
522            // Project name
523            f.render_widget(
524                Paragraph::new(project.name.clone())
525                    .alignment(Alignment::Center)
526                    .style(
527                        Style::default()
528                            .fg(ColorScheme::WHITE_TEXT)
529                            .add_modifier(Modifier::BOLD),
530                    ),
531                project_layout[0],
532            );
533
534            // Project description or refactor info
535            let default_description = "Refactor authentication module".to_string();
536            let description = project
537                .description
538                .as_ref()
539                .unwrap_or(&default_description);
540            f.render_widget(
541                Paragraph::new(description.clone())
542                    .alignment(Alignment::Center)
543                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
544                project_layout[1],
545            );
546
547            // Large timer box
548            let timer_area = self.centered_rect(40, 20, chunks[4]);
549            let timer_block = Block::default()
550                .borders(Borders::ALL)
551                .border_style(Style::default().fg(ColorScheme::CLEAN_GREEN))
552                .style(Style::default().bg(Color::Black));
553
554            let timer_inner = timer_block.inner(timer_area);
555            f.render_widget(timer_block, timer_area);
556
557            // Calculate and display large timer
558            let now = Local::now();
559            let elapsed_seconds = (now.timestamp() - session.start_time.timestamp())
560                - session.paused_duration.num_seconds();
561            let duration_str = Formatter::format_duration_clock(elapsed_seconds);
562
563            f.render_widget(
564                Paragraph::new(duration_str)
565                    .alignment(Alignment::Center)
566                    .style(
567                        Style::default()
568                            .fg(ColorScheme::CLEAN_GREEN)
569                            .add_modifier(Modifier::BOLD),
570                    ),
571                timer_inner,
572            );
573
574            // Session details box
575            let details_area = self.centered_rect(60, 25, chunks[6]);
576            let details_block = Block::default()
577                .borders(Borders::ALL)
578                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
579                .style(Style::default().bg(ColorScheme::CLEAN_BG));
580
581            let details_layout = Layout::default()
582                .direction(Direction::Vertical)
583                .constraints([
584                    Constraint::Length(2), // Start time
585                    Constraint::Length(2), // Session type
586                    Constraint::Length(2), // Tags
587                ])
588                .margin(1)
589                .split(details_area);
590
591            f.render_widget(details_block, details_area);
592
593            // Start time
594            let start_time_layout = Layout::default()
595                .direction(Direction::Horizontal)
596                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
597                .split(details_layout[0]);
598
599            f.render_widget(
600                Paragraph::new("Start Time")
601                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
602                start_time_layout[0],
603            );
604            f.render_widget(
605                Paragraph::new(session.start_time.with_timezone(&Local).format("%H:%M").to_string())
606                    .alignment(Alignment::Right)
607                    .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
608                start_time_layout[1],
609            );
610
611            // Session type
612            let session_type_layout = Layout::default()
613                .direction(Direction::Horizontal)
614                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
615                .split(details_layout[1]);
616
617            f.render_widget(
618                Paragraph::new("Session Type")
619                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
620                session_type_layout[0],
621            );
622            f.render_widget(
623                Paragraph::new("Deep Work")
624                    .alignment(Alignment::Right)
625                    .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
626                session_type_layout[1],
627            );
628
629            // Tags
630            let tags_layout = Layout::default()
631                .direction(Direction::Horizontal)
632                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
633                .split(details_layout[2]);
634
635            f.render_widget(
636                Paragraph::new("Tags")
637                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
638                tags_layout[0],
639            );
640
641            // Create tag spans
642            let tag_spans = vec![
643                Span::styled(
644                    " Backend ",
645                    Style::default()
646                        .fg(ColorScheme::CLEAN_BG)
647                        .bg(ColorScheme::GRAY_TEXT),
648                ),
649                Span::raw(" "),
650                Span::styled(
651                    " Refactor ",
652                    Style::default()
653                        .fg(ColorScheme::CLEAN_BG)
654                        .bg(ColorScheme::GRAY_TEXT),
655                ),
656                Span::raw(" "),
657                Span::styled(
658                    " Security ",
659                    Style::default()
660                        .fg(ColorScheme::CLEAN_BG)
661                        .bg(ColorScheme::GRAY_TEXT),
662                ),
663            ];
664
665            f.render_widget(
666                Paragraph::new(Line::from(tag_spans))
667                    .alignment(Alignment::Right),
668                tags_layout[1],
669            );
670
671        } else {
672            // No active session - show idle state
673            let idle_area = self.centered_rect(50, 20, chunks[4]);
674            let idle_block = Block::default()
675                .borders(Borders::ALL)
676                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
677                .style(Style::default().bg(ColorScheme::CLEAN_BG));
678
679            f.render_widget(idle_block.clone(), idle_area);
680
681            let idle_inner = idle_block.inner(idle_area);
682            f.render_widget(
683                Paragraph::new("No Active Session\n\nPress 's' to start tracking")
684                    .alignment(Alignment::Center)
685                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
686                idle_inner,
687            );
688        }
689    }
690
691    fn render_overview_dashboard(&mut self, f: &mut Frame) {
692        let chunks = Layout::default()
693            .direction(Direction::Vertical)
694            .constraints([
695                Constraint::Length(3),  // Header
696                Constraint::Length(1),  // Spacer
697                Constraint::Length(10), // Active Session Panel
698                Constraint::Length(1),  // Spacer
699                Constraint::Length(3),  // Quick Stats Header
700                Constraint::Length(5),  // Quick Stats Grid
701                Constraint::Length(1),  // Spacer
702                Constraint::Min(10),    // Recent Projects & Timeline
703                Constraint::Length(1),  // Bottom bar
704            ])
705            .split(f.size());
706
707        // Header
708        self.render_header(f, chunks[0]);
709
710        let daily_stats = self.get_daily_stats();
711        let current_session = &self.current_session;
712        let current_project = &self.current_project;
713
714        // 1. Active Session Panel
715        self.render_active_session_panel(f, chunks[2], current_session, current_project);
716
717        // 2. Quick Stats
718        SessionStatsWidget::render(daily_stats, self.weekly_stats, chunks[5], f.buffer_mut());
719        self.render_quick_stats(f, chunks[4], chunks[5], daily_stats);
720
721        // 3. Recent Projects & Timeline
722        self.render_projects_and_timeline(f, chunks[5]);
723
724        // 4. Bottom Bar
725        self.render_keyboard_hints(chunks[8], f.buffer_mut());
726
727        self.render_bottom_bar(f, chunks[6]);
728    }
729
730    fn render_history_browser(&mut self, f: &mut Frame) {
731        let chunks = Layout::default()
732            .direction(Direction::Horizontal)
733            .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
734            .split(f.size());
735
736        let left_chunks = Layout::default()
737            .direction(Direction::Vertical)
738            .constraints([
739                Constraint::Length(3),  // Header
740                Constraint::Length(8),  // Filters
741                Constraint::Min(10),    // Session list
742            ])
743            .split(chunks[0]);
744
745        let right_chunks = Layout::default()
746            .direction(Direction::Vertical)
747            .constraints([
748                Constraint::Percentage(60), // Session details
749                Constraint::Length(4),      // Action buttons
750                Constraint::Min(0),         // Summary
751            ])
752            .split(chunks[1]);
753
754        // Header
755        f.render_widget(
756            Paragraph::new("Tempo TUI :: History Browser")
757                .style(Style::default().fg(ColorScheme::CLEAN_BLUE).add_modifier(Modifier::BOLD))
758                .block(
759                    Block::default()
760                        .borders(Borders::BOTTOM)
761                        .border_style(Style::default().fg(ColorScheme::GRAY_TEXT)),
762                ),
763            left_chunks[0],
764        );
765
766        // Filters panel
767        self.render_history_filters(f, left_chunks[1]);
768
769        // Session list
770        self.render_session_list(f, left_chunks[2]);
771
772        // Session details
773        self.render_session_details(f, right_chunks[0]);
774
775        // Action buttons
776        self.render_session_actions(f, right_chunks[1]);
777
778        // Summary
779        self.render_history_summary(f, right_chunks[2]);
780    }
781
782    fn render_history_filters(&self, f: &mut Frame, area: Rect) {
783        let block = Block::default()
784            .borders(Borders::ALL)
785            .title(" Filters ")
786            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
787
788        let inner_area = block.inner(area);
789        f.render_widget(block, area);
790
791        let filter_chunks = Layout::default()
792            .direction(Direction::Vertical)
793            .constraints([
794                Constraint::Length(2), // Date filters
795                Constraint::Length(1), // Project filter
796                Constraint::Length(1), // Duration filter
797                Constraint::Length(1), // Search
798            ])
799            .split(inner_area);
800
801        // Date range
802        let date_layout = Layout::default()
803            .direction(Direction::Horizontal)
804            .constraints([Constraint::Percentage(30), Constraint::Percentage(35), Constraint::Percentage(35)])
805            .split(filter_chunks[0]);
806
807        f.render_widget(
808            Paragraph::new("Start Date\nEnd Date")
809                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
810            date_layout[0],
811        );
812        f.render_widget(
813            Paragraph::new("2023-10-01\n2023-10-31")
814                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
815            date_layout[1],
816        );
817        f.render_widget(
818            Paragraph::new("Project\nDuration Filter")
819                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
820            date_layout[2],
821        );
822
823        // Project filter
824        let project_layout = Layout::default()
825            .direction(Direction::Horizontal)
826            .constraints([Constraint::Length(15), Constraint::Min(0)])
827            .split(filter_chunks[1]);
828
829        f.render_widget(
830            Paragraph::new("Project")
831                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
832            project_layout[0],
833        );
834        f.render_widget(
835            Paragraph::new("Filter by project ▼")
836                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
837            project_layout[1],
838        );
839
840        // Duration filter
841        let duration_layout = Layout::default()
842            .direction(Direction::Horizontal)
843            .constraints([Constraint::Length(15), Constraint::Min(0)])
844            .split(filter_chunks[2]);
845
846        f.render_widget(
847            Paragraph::new("Duration Filter")
848                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
849            duration_layout[0],
850        );
851        f.render_widget(
852            Paragraph::new(">1h, <30m")
853                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
854            duration_layout[1],
855        );
856
857        // Free-text search
858        let search_layout = Layout::default()
859            .direction(Direction::Horizontal)
860            .constraints([Constraint::Length(15), Constraint::Min(0)])
861            .split(filter_chunks[3]);
862
863        f.render_widget(
864            Paragraph::new("Free-text Search")
865                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
866            search_layout[0],
867        );
868
869        let search_style = if self.filter_input_mode {
870            Style::default().fg(ColorScheme::CLEAN_BLUE)
871        } else {
872            Style::default().fg(ColorScheme::WHITE_TEXT)
873        };
874
875        let search_text = if self.session_filter.search_text.is_empty() {
876            "Search session notes and context..."
877        } else {
878            &self.session_filter.search_text
879        };
880
881        f.render_widget(
882            Paragraph::new(search_text)
883                .style(search_style),
884            search_layout[1],
885        );
886    }
887
888    fn render_session_list(&self, f: &mut Frame, area: Rect) {
889        let block = Block::default()
890            .borders(Borders::ALL)
891            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
892
893        let inner_area = block.inner(area);
894        f.render_widget(block, area);
895
896        // Header row
897        let header_row = Row::new(vec![
898            Cell::from("DATE").style(Style::default().add_modifier(Modifier::BOLD)),
899            Cell::from("PROJECT").style(Style::default().add_modifier(Modifier::BOLD)),
900            Cell::from("DURATION").style(Style::default().add_modifier(Modifier::BOLD)),
901            Cell::from("START").style(Style::default().add_modifier(Modifier::BOLD)),
902            Cell::from("END").style(Style::default().add_modifier(Modifier::BOLD)),
903            Cell::from("STATUS").style(Style::default().add_modifier(Modifier::BOLD)),
904        ])
905        .style(Style::default().fg(ColorScheme::GRAY_TEXT))
906        .bottom_margin(1);
907
908        // Create sample session rows
909        let rows: Vec<Row> = self.history_sessions
910            .iter()
911            .enumerate()
912            .map(|(i, session)| {
913                let is_selected = i == self.selected_session_index;
914                let style = if is_selected {
915                    Style::default().bg(ColorScheme::CLEAN_BLUE).fg(Color::Black)
916                } else {
917                    Style::default().fg(ColorScheme::WHITE_TEXT)
918                };
919
920                let status = if session.end_time.is_some() {
921                    "[✓] Completed"
922                } else {
923                    "[▶] Running"
924                };
925
926                let start_time = session.start_time.with_timezone(&Local).format("%H:%M").to_string();
927                let end_time = if let Some(end) = session.end_time {
928                    end.with_timezone(&Local).format("%H:%M").to_string()
929                } else {
930                    "--:--".to_string()
931                };
932
933                let duration = if let Some(_) = session.end_time {
934                    let duration_secs = (session.start_time.timestamp() - session.start_time.timestamp()).abs();
935                    Formatter::format_duration(duration_secs)
936                } else {
937                    "0h 0m".to_string()
938                };
939
940                Row::new(vec![
941                    Cell::from(session.start_time.with_timezone(&Local).format("%Y-%m-%d").to_string()),
942                    Cell::from("Project Phoenix"), // TODO: Get actual project name
943                    Cell::from(duration),
944                    Cell::from(start_time),
945                    Cell::from(end_time),
946                    Cell::from(status),
947                ])
948                .style(style)
949            })
950            .collect();
951
952        if rows.is_empty() {
953            // Show sample data
954            let sample_rows = vec![
955                Row::new(vec![
956                    Cell::from("2023-10-26"),
957                    Cell::from("Project Phoenix"),
958                    Cell::from("2h 15m"),
959                    Cell::from("09:03"),
960                    Cell::from("11:18"),
961                    Cell::from("[✓] Completed"),
962                ]).style(Style::default().bg(ColorScheme::CLEAN_BLUE).fg(Color::Black)),
963                Row::new(vec![
964                    Cell::from("2023-10-26"),
965                    Cell::from("Internal Tools"),
966                    Cell::from("0h 45m"),
967                    Cell::from("11:30"),
968                    Cell::from("12:15"),
969                    Cell::from("[✓] Completed"),
970                ]),
971                Row::new(vec![
972                    Cell::from("2023-10-25"),
973                    Cell::from("Project Phoenix"),
974                    Cell::from("4h 05m"),
975                    Cell::from("13:00"),
976                    Cell::from("17:05"),
977                    Cell::from("[✓] Completed"),
978                ]),
979                Row::new(vec![
980                    Cell::from("2023-10-25"),
981                    Cell::from("Client Support"),
982                    Cell::from("1h 00m"),
983                    Cell::from("10:00"),
984                    Cell::from("11:00"),
985                    Cell::from("[✓] Completed"),
986                ]),
987                Row::new(vec![
988                    Cell::from("2023-10-24"),
989                    Cell::from("Project Phoenix"),
990                    Cell::from("8h 00m"),
991                    Cell::from("09:00"),
992                    Cell::from("17:00"),
993                    Cell::from("[✓] Completed"),
994                ]),
995                Row::new(vec![
996                    Cell::from("2023-10-27"),
997                    Cell::from("Project Nova"),
998                    Cell::from("0h 22m"),
999                    Cell::from("14:00"),
1000                    Cell::from("--:--"),
1001                    Cell::from("[▶] Running"),
1002                ]),
1003            ];
1004
1005            let table = Table::new(sample_rows)
1006                .header(header_row)
1007                .widths(&[
1008                    Constraint::Length(12),
1009                    Constraint::Min(15),
1010                    Constraint::Length(10),
1011                    Constraint::Length(8),
1012                    Constraint::Length(8),
1013                    Constraint::Min(12),
1014                ]);
1015
1016            f.render_widget(table, inner_area);
1017        }
1018    }
1019
1020    fn render_session_details(&self, f: &mut Frame, area: Rect) {
1021        let block = Block::default()
1022            .borders(Borders::ALL)
1023            .title(" Session Details ")
1024            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1025
1026        let inner_area = block.inner(area);
1027        f.render_widget(block, area);
1028
1029        let details_chunks = Layout::default()
1030            .direction(Direction::Vertical)
1031            .constraints([
1032                Constraint::Length(3), // Session notes
1033                Constraint::Length(2), // Tags
1034                Constraint::Length(3), // Context
1035            ])
1036            .split(inner_area);
1037
1038        // Session notes
1039        f.render_widget(
1040            Paragraph::new("SESSION NOTES\n\nWorked on the new authentication flow.\nImplemented JWT token refresh logic and fixed\nthe caching issue on the user profile page.\nReady for QA review.")
1041                .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1042                .wrap(ratatui::widgets::Wrap { trim: true }),
1043            details_chunks[0],
1044        );
1045
1046        // Tags
1047        let tag_spans = vec![
1048            Span::styled(
1049                " #backend ",
1050                Style::default()
1051                    .fg(Color::Black)
1052                    .bg(ColorScheme::CLEAN_BLUE),
1053            ),
1054            Span::raw(" "),
1055            Span::styled(
1056                " #auth ",
1057                Style::default()
1058                    .fg(Color::Black)
1059                    .bg(ColorScheme::CLEAN_BLUE),
1060            ),
1061            Span::raw(" "),
1062            Span::styled(
1063                " #bugfix ",
1064                Style::default()
1065                    .fg(Color::Black)
1066                    .bg(ColorScheme::CLEAN_BLUE),
1067            ),
1068        ];
1069
1070        f.render_widget(
1071            Paragraph::new(vec![
1072                Line::from("TAGS"),
1073                Line::from(tag_spans),
1074            ])
1075            .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1076            details_chunks[1],
1077        );
1078
1079        // Context
1080        let context_chunks = Layout::default()
1081            .direction(Direction::Vertical)
1082            .constraints([Constraint::Length(1), Constraint::Min(0)])
1083            .split(details_chunks[2]);
1084
1085        f.render_widget(
1086            Paragraph::new("CONTEXT")
1087                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1088            context_chunks[0],
1089        );
1090
1091        let context_layout = Layout::default()
1092            .direction(Direction::Horizontal)
1093            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1094            .split(context_chunks[1]);
1095
1096        f.render_widget(
1097            Paragraph::new("Git\nBranch:\nIssue ID:\nCommit:")
1098                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1099            context_layout[0],
1100        );
1101        f.render_widget(
1102            Paragraph::new("feature/PHX-123-auth\nPHX-123\na1b2c3d")
1103                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1104            context_layout[1],
1105        );
1106    }
1107
1108    fn render_session_actions(&self, f: &mut Frame, area: Rect) {
1109        let button_layout = Layout::default()
1110            .direction(Direction::Horizontal)
1111            .constraints([
1112                Constraint::Percentage(25),
1113                Constraint::Percentage(25),
1114                Constraint::Percentage(25),
1115                Constraint::Percentage(25),
1116            ])
1117            .split(area);
1118
1119        let buttons = [
1120            ("[ Edit ]", ColorScheme::GRAY_TEXT),
1121            ("[ Duplicate ]", ColorScheme::GRAY_TEXT),
1122            ("[ Delete ]", Color::Red),
1123            ("", ColorScheme::GRAY_TEXT),
1124        ];
1125
1126        for (i, (text, color)) in buttons.iter().enumerate() {
1127            if !text.is_empty() {
1128                f.render_widget(
1129                    Paragraph::new(*text)
1130                        .alignment(Alignment::Center)
1131                        .style(Style::default().fg(*color)),
1132                    button_layout[i],
1133                );
1134            }
1135        }
1136    }
1137
1138    fn render_history_summary(&self, f: &mut Frame, area: Rect) {
1139        let block = Block::default()
1140            .borders(Borders::ALL)
1141            .title(" Summary ")
1142            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1143
1144        let inner_area = block.inner(area);
1145        f.render_widget(block, area);
1146
1147        f.render_widget(
1148            Paragraph::new("Showing 7 of 128 sessions. Total Duration: 17h 40m")
1149                .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1150                .alignment(Alignment::Center),
1151            inner_area,
1152        );
1153    }
1154
1155    fn render_project_grid(&mut self, f: &mut Frame) {
1156        let area = f.size();
1157        
1158        let main_layout = Layout::default()
1159            .direction(Direction::Vertical)
1160            .constraints([
1161                Constraint::Length(3),  // Header
1162                Constraint::Min(10),    // Project grid
1163                Constraint::Length(3),  // Stats summary
1164                Constraint::Length(1),  // Bottom hints
1165            ])
1166            .split(area);
1167
1168        // Header
1169        f.render_widget(
1170            Paragraph::new("Project Dashboard")
1171                .style(Style::default().fg(ColorScheme::CLEAN_BLUE).add_modifier(Modifier::BOLD))
1172                .block(
1173                    Block::default()
1174                        .borders(Borders::BOTTOM)
1175                        .border_style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1176                ),
1177            main_layout[0],
1178        );
1179
1180        // Project grid area
1181        self.render_project_cards(f, main_layout[1]);
1182
1183        // Stats summary
1184        self.render_project_stats_summary(f, main_layout[2]);
1185
1186        // Bottom hints
1187        let hints = vec![
1188            ("↑/↓/←/→", "Navigate"),
1189            ("Enter", "Select"),
1190            ("Tab", "Next View"),
1191            ("q", "Quit"),
1192        ];
1193
1194        let spans: Vec<Span> = hints
1195            .iter()
1196            .flat_map(|(key, desc)| {
1197                vec![
1198                    Span::styled(
1199                        format!(" {} ", key),
1200                        Style::default()
1201                            .fg(Color::Yellow)
1202                            .add_modifier(Modifier::BOLD),
1203                    ),
1204                    Span::styled(format!("{} ", desc), Style::default().fg(Color::DarkGray)),
1205                ]
1206            })
1207            .collect();
1208
1209        let line = Line::from(spans);
1210        let block = Block::default()
1211            .borders(Borders::TOP)
1212            .border_style(Style::default().fg(Color::DarkGray));
1213        Paragraph::new(line).block(block).render(main_layout[3], f.buffer_mut());
1214    }
1215
1216    fn render_project_cards(&mut self, f: &mut Frame, area: Rect) {
1217        if self.available_projects.is_empty() {
1218            // Show empty state
1219            let empty_block = Block::default()
1220                .borders(Borders::ALL)
1221                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
1222                .title(" No Projects Found ");
1223            
1224            let empty_area = self.centered_rect(50, 30, area);
1225            f.render_widget(empty_block.clone(), empty_area);
1226            
1227            let inner = empty_block.inner(empty_area);
1228            f.render_widget(
1229                Paragraph::new("No projects available.\n\nStart a session to create a project.")
1230                    .alignment(Alignment::Center)
1231                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1232                inner,
1233            );
1234            return;
1235        }
1236
1237        // Calculate grid layout
1238        let margin = 2;
1239        let card_height = 8;
1240        let card_spacing = 1;
1241        
1242        // Calculate how many rows we can fit
1243        let available_height = area.height.saturating_sub(margin * 2);
1244        let total_rows = (self.available_projects.len() + self.projects_per_row - 1) / self.projects_per_row;
1245        let visible_rows = (available_height / (card_height + card_spacing)).min(total_rows as u16) as usize;
1246        
1247        // Render visible rows
1248        for row in 0..visible_rows {
1249            let y_offset = margin + row as u16 * (card_height + card_spacing);
1250            
1251            // Create horizontal layout for this row
1252            let row_area = Rect::new(area.x, area.y + y_offset, area.width, card_height);
1253            let card_constraints = vec![Constraint::Percentage(100 / self.projects_per_row as u16); self.projects_per_row];
1254            let row_layout = Layout::default()
1255                .direction(Direction::Horizontal)
1256                .constraints(card_constraints)
1257                .margin(1)
1258                .split(row_area);
1259            
1260            // Render cards in this row
1261            for col in 0..self.projects_per_row {
1262                let project_index = row * self.projects_per_row + col;
1263                if project_index >= self.available_projects.len() {
1264                    break;
1265                }
1266                
1267                let is_selected = row == self.selected_project_row && col == self.selected_project_col;
1268                self.render_project_card(f, row_layout[col], project_index, is_selected);
1269            }
1270        }
1271    }
1272
1273    fn render_project_card(&self, f: &mut Frame, area: Rect, project_index: usize, is_selected: bool) {
1274        if let Some(project) = self.available_projects.get(project_index) {
1275            // Card styling based on selection
1276            let border_style = if is_selected {
1277                Style::default().fg(ColorScheme::CLEAN_BLUE)
1278            } else {
1279                Style::default().fg(ColorScheme::GRAY_TEXT)
1280            };
1281            
1282            let bg_color = if is_selected {
1283                ColorScheme::CLEAN_BG
1284            } else {
1285                Color::Black
1286            };
1287
1288            let card_block = Block::default()
1289                .borders(Borders::ALL)
1290                .border_style(border_style)
1291                .style(Style::default().bg(bg_color));
1292
1293            f.render_widget(card_block.clone(), area);
1294
1295            let inner_area = card_block.inner(area);
1296            let card_layout = Layout::default()
1297                .direction(Direction::Vertical)
1298                .constraints([
1299                    Constraint::Length(1), // Project name
1300                    Constraint::Length(1), // Path
1301                    Constraint::Length(1), // Spacer
1302                    Constraint::Length(2), // Stats
1303                    Constraint::Length(1), // Status
1304                ])
1305                .split(inner_area);
1306
1307            // Project name
1308            let name = if project.name.len() > 20 {
1309                format!("{}...", &project.name[..17])
1310            } else {
1311                project.name.clone()
1312            };
1313            
1314            f.render_widget(
1315                Paragraph::new(name)
1316                    .style(Style::default().fg(ColorScheme::WHITE_TEXT).add_modifier(Modifier::BOLD))
1317                    .alignment(Alignment::Center),
1318                card_layout[0],
1319            );
1320
1321            // Path (shortened)
1322            let path_str = project.path.to_string_lossy();
1323            let short_path = if path_str.len() > 25 {
1324                format!("...{}", &path_str[path_str.len()-22..])
1325            } else {
1326                path_str.to_string()
1327            };
1328            
1329            f.render_widget(
1330                Paragraph::new(short_path)
1331                    .style(Style::default().fg(ColorScheme::GRAY_TEXT))
1332                    .alignment(Alignment::Center),
1333                card_layout[1],
1334            );
1335
1336            // Stats placeholder - in real implementation, we'd fetch these
1337            let stats_layout = Layout::default()
1338                .direction(Direction::Horizontal)
1339                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1340                .split(card_layout[3]);
1341
1342            f.render_widget(
1343                Paragraph::new("Sessions\n42")
1344                    .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1345                    .alignment(Alignment::Center),
1346                stats_layout[0],
1347            );
1348
1349            f.render_widget(
1350                Paragraph::new("Time\n24h 15m")
1351                    .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1352                    .alignment(Alignment::Center),
1353                stats_layout[1],
1354            );
1355
1356            // Status
1357            let status = if project.is_archived {
1358                (" Archived ", Color::Red)
1359            } else {
1360                (" Active ", ColorScheme::CLEAN_GREEN)
1361            };
1362
1363            f.render_widget(
1364                Paragraph::new(status.0)
1365                    .style(Style::default().fg(status.1))
1366                    .alignment(Alignment::Center),
1367                card_layout[4],
1368            );
1369        }
1370    }
1371
1372    fn render_project_stats_summary(&self, f: &mut Frame, area: Rect) {
1373        let block = Block::default()
1374            .borders(Borders::ALL)
1375            .title(" Summary ")
1376            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1377
1378        f.render_widget(block.clone(), area);
1379
1380        let inner = block.inner(area);
1381        let stats_layout = Layout::default()
1382            .direction(Direction::Horizontal)
1383            .constraints([
1384                Constraint::Percentage(25),
1385                Constraint::Percentage(25),
1386                Constraint::Percentage(25),
1387                Constraint::Percentage(25),
1388            ])
1389            .split(inner);
1390
1391        let total_projects = self.available_projects.len();
1392        let active_projects = self.available_projects.iter().filter(|p| !p.is_archived).count();
1393        let archived_projects = total_projects - active_projects;
1394
1395        let stats = [
1396            ("Total Projects", total_projects.to_string()),
1397            ("Active", active_projects.to_string()),
1398            ("Archived", archived_projects.to_string()),
1399            ("Selected", format!("{}/{}", 
1400                self.selected_project_row * self.projects_per_row + self.selected_project_col + 1, 
1401                total_projects)),
1402        ];
1403
1404        for (i, (label, value)) in stats.iter().enumerate() {
1405            let content = Paragraph::new(vec![
1406                Line::from(Span::styled(*label, Style::default().fg(ColorScheme::GRAY_TEXT))),
1407                Line::from(Span::styled(
1408                    value.as_str(),
1409                    Style::default().fg(ColorScheme::WHITE_TEXT).add_modifier(Modifier::BOLD),
1410                )),
1411            ])
1412            .alignment(Alignment::Center);
1413
1414            f.render_widget(content, stats_layout[i]);
1415        }
1416    }
1417
1418    fn render_active_session_panel(
1419        &self,
1420        f: &mut Frame,
1421        area: Rect,
1422        session: &Option<Session>,
1423        project: &Option<Project>,
1424    ) {
1425        let block = Block::default().style(Style::default().bg(ColorScheme::CLEAN_BG));
1426
1427        f.render_widget(block, area);
1428
1429        let layout = Layout::default()
1430            .direction(Direction::Vertical)
1431            .constraints([
1432                Constraint::Length(1), // "Active Session" label
1433                Constraint::Length(2), // Project Name & State
1434                Constraint::Length(1), // Spacer
1435                Constraint::Length(3), // Large Timer
1436            ])
1437            .margin(1)
1438            .split(area);
1439
1440        // Label
1441        f.render_widget(
1442            Paragraph::new("Active Session").style(
1443                Style::default()
1444                    .fg(ColorScheme::GRAY_TEXT)
1445                    .add_modifier(Modifier::BOLD),
1446            ),
1447            layout[0],
1448        );
1449
1450        if let Some(session) = session {
1451            let project_name = project
1452                .as_ref()
1453                .map(|p| p.name.as_str())
1454                .unwrap_or("Unknown Project");
1455
1456            // Project Name & State
1457            let info_layout = Layout::default()
1458                .direction(Direction::Horizontal)
1459                .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
1460                .split(layout[1]);
1461
1462            f.render_widget(
1463                Paragraph::new(project_name).style(
1464                    Style::default()
1465                        .fg(ColorScheme::GRAY_TEXT)
1466                        .add_modifier(Modifier::BOLD),
1467                ),
1468                info_layout[0],
1469            );
1470
1471            f.render_widget(
1472                Paragraph::new("State: ACTIVE")
1473                    .alignment(Alignment::Right)
1474                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1475                info_layout[1],
1476            );
1477
1478            // Timer
1479            let now = Local::now();
1480            let elapsed_seconds = (now.timestamp() - session.start_time.timestamp())
1481                - session.paused_duration.num_seconds();
1482            let duration_str = Formatter::format_duration_clock(elapsed_seconds);
1483
1484            f.render_widget(
1485                Paragraph::new(duration_str)
1486                    .alignment(Alignment::Center)
1487                    .style(
1488                        Style::default()
1489                            .fg(ColorScheme::WHITE_TEXT)
1490                            .add_modifier(Modifier::BOLD),
1491                    ),
1492                layout[3],
1493            );
1494        } else {
1495            // Idle State
1496            f.render_widget(
1497                Paragraph::new("No Active Session")
1498                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1499                layout[1],
1500            );
1501            f.render_widget(
1502                Paragraph::new("--:--:--")
1503                    .alignment(Alignment::Center)
1504                    .style(
1505                        Style::default()
1506                            .fg(ColorScheme::GRAY_TEXT)
1507                            .add_modifier(Modifier::DIM),
1508                    ),
1509                layout[3],
1510            );
1511        }
1512    }
1513
1514    fn render_quick_stats(
1515        &self,
1516        f: &mut Frame,
1517        header_area: Rect,
1518        grid_area: Rect,
1519        daily_stats: &(i64, i64, i64),
1520    ) {
1521        let (sessions_count, total_seconds, _avg_seconds) = *daily_stats;
1522
1523        // Header
1524        f.render_widget(
1525            Paragraph::new("Quick Stats").style(
1526                Style::default()
1527                    .fg(ColorScheme::WHITE_TEXT)
1528                    .add_modifier(Modifier::BOLD),
1529            ),
1530            header_area,
1531        );
1532
1533        // Grid
1534        let cols = Layout::default()
1535            .direction(Direction::Horizontal)
1536            .constraints([
1537                Constraint::Percentage(25),
1538                Constraint::Percentage(25),
1539                Constraint::Percentage(25),
1540                Constraint::Percentage(25),
1541            ])
1542            .split(grid_area);
1543
1544        let stats = [
1545            ("Today", Formatter::format_duration(total_seconds)),
1546            ("This Week", Formatter::format_duration(self.weekly_stats)),
1547            ("Active", sessions_count.to_string()),
1548            ("Projects", self.available_projects.len().to_string()),
1549        ];
1550
1551        for (i, (label, value)) in stats.iter().enumerate() {
1552            let block = Block::default()
1553                .borders(Borders::ALL)
1554                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
1555                .style(Style::default().bg(ColorScheme::CLEAN_BG));
1556
1557            let content = Paragraph::new(vec![
1558                Line::from(Span::styled(
1559                    *label,
1560                    Style::default().fg(ColorScheme::GRAY_TEXT),
1561                )),
1562                Line::from(Span::styled(
1563                    value.as_str(),
1564                    Style::default()
1565                        .fg(ColorScheme::WHITE_TEXT)
1566                        .add_modifier(Modifier::BOLD),
1567                )),
1568            ])
1569            .block(block)
1570            .alignment(Alignment::Center);
1571
1572            f.render_widget(content, cols[i]);
1573        }
1574    }
1575
1576    fn render_bottom_bar(&self, f: &mut Frame, area: Rect) {
1577        let help_text = if self.show_project_switcher {
1578            vec![
1579                Span::styled(
1580                    "[Q]",
1581                    Style::default()
1582                        .fg(ColorScheme::CLEAN_ACCENT)
1583                        .add_modifier(Modifier::BOLD),
1584                ),
1585                Span::raw(" Close  "),
1586                Span::raw("[↑/↓] Navigate  "),
1587                Span::raw("[Enter] Select"),
1588            ]
1589        } else {
1590            vec![
1591                Span::styled(
1592                    "[Q]",
1593                    Style::default()
1594                        .fg(ColorScheme::CLEAN_ACCENT)
1595                        .add_modifier(Modifier::BOLD),
1596                ),
1597                Span::raw(" Quit  "),
1598                Span::raw("[P] Projects  "),
1599                Span::raw("[R] Refresh"),
1600            ]
1601        };
1602
1603        let help_paragraph = Paragraph::new(Line::from(help_text))
1604            .alignment(Alignment::Center)
1605            .style(Style::default().fg(ColorScheme::GRAY_TEXT));
1606
1607        f.render_widget(help_paragraph, area);
1608    }
1609
1610    fn render_projects_and_timeline(&self, f: &mut Frame, area: Rect) {
1611        let chunks = Layout::default()
1612            .direction(Direction::Vertical)
1613            .constraints([
1614                Constraint::Length(2), // Header
1615                Constraint::Min(5),    // Project List
1616                Constraint::Length(2), // Timeline Header
1617                Constraint::Length(3), // Timeline Bar
1618            ])
1619            .split(area);
1620
1621        // Projects Header
1622        f.render_widget(
1623            Paragraph::new("Recent Projects").style(
1624                Style::default()
1625                    .fg(ColorScheme::WHITE_TEXT)
1626                    .add_modifier(Modifier::BOLD),
1627            ),
1628            chunks[0],
1629        );
1630
1631        let list_area = chunks[1];
1632        let items_area = Rect::new(list_area.x, list_area.y, list_area.width, list_area.height);
1633
1634        // Recent Projects Table
1635        let header = Row::new(vec![
1636            Cell::from("Project").style(Style::default().add_modifier(Modifier::BOLD)),
1637            Cell::from("Today").style(Style::default().add_modifier(Modifier::BOLD)),
1638            Cell::from("Total").style(Style::default().add_modifier(Modifier::BOLD)),
1639            Cell::from("Last Active").style(Style::default().add_modifier(Modifier::BOLD)),
1640        ])
1641        .style(Style::default().fg(ColorScheme::CLEAN_BLUE))
1642        .bottom_margin(1);
1643
1644        let items: Vec<Row> = self
1645            .recent_projects
1646            .iter()
1647            .map(|p| {
1648                let last_active = if let Some(date) = p.last_active {
1649                    let now = chrono::Utc::now();
1650                    let diff = now - date;
1651                    if diff.num_days() > 0 {
1652                        format!("{}d ago", diff.num_days())
1653                    } else if diff.num_hours() > 0 {
1654                        format!("{}h ago", diff.num_hours())
1655                    } else {
1656                        format!("{}m ago", diff.num_minutes())
1657                    }
1658                } else {
1659                    "-".to_string()
1660                };
1661
1662                Row::new(vec![
1663                    Cell::from(p.project.name.clone()),
1664                    Cell::from(Formatter::format_duration(p.today_seconds)),
1665                    Cell::from(Formatter::format_duration(p.total_seconds)),
1666                    Cell::from(last_active),
1667                ])
1668            })
1669            .collect();
1670
1671        let table = Table::new(items)
1672            .header(header)
1673            .block(Block::default().borders(Borders::NONE))
1674            .widths(&[
1675                Constraint::Percentage(40),
1676                Constraint::Percentage(20),
1677                Constraint::Percentage(20),
1678                Constraint::Percentage(20),
1679            ]);
1680
1681        f.render_widget(table, items_area);
1682
1683        // Timeline Header
1684        f.render_widget(
1685            Paragraph::new("Activity Timeline").style(
1686                Style::default()
1687                    .fg(ColorScheme::WHITE_TEXT)
1688                    .add_modifier(Modifier::BOLD),
1689            ),
1690            chunks[2],
1691        );
1692
1693        // Timeline Bar
1694        let timeline_area = chunks[3];
1695        let bar_area = Rect::new(timeline_area.x, timeline_area.y, timeline_area.width, 1);
1696
1697        // Background
1698        f.render_widget(
1699            Block::default().style(Style::default().bg(ColorScheme::GRAY_TEXT)),
1700            bar_area,
1701        );
1702
1703        // Sessions
1704        let width = bar_area.width as f64;
1705        for session in &self.today_sessions {
1706            let start = session.start_time.with_timezone(&Local).time();
1707            let start_seconds = start.num_seconds_from_midnight() as f64;
1708
1709            let duration = if let Some(end) = session.end_time {
1710                (end - session.start_time).num_seconds() as f64
1711            } else {
1712                (Local::now().signed_duration_since(session.start_time.with_timezone(&Local)))
1713                    .num_seconds() as f64
1714            };
1715
1716            // Subtract paused duration
1717            let duration = duration - session.paused_duration.num_seconds() as f64;
1718
1719            let x_offset = (start_seconds / 86400.0) * width;
1720            let bar_width = (duration / 86400.0) * width;
1721
1722            if bar_width > 0.0 {
1723                let x_pos = bar_area.x + x_offset as u16;
1724                // Ensure we don't draw outside bounds or wrap
1725                if x_pos < bar_area.x + bar_area.width {
1726                    let w = (bar_width.max(1.0) as u16).min(bar_area.width - (x_pos - bar_area.x));
1727                    let session_rect = Rect::new(x_pos, bar_area.y, w, 1);
1728                    f.render_widget(
1729                        Block::default().style(Style::default().bg(ColorScheme::CLEAN_BLUE)),
1730                        session_rect,
1731                    );
1732                }
1733            }
1734        }
1735
1736        // Labels
1737        let label_y = timeline_area.y + 1;
1738        f.render_widget(
1739            Paragraph::new("00:00").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1740            Rect::new(timeline_area.x, label_y, 5, 1),
1741        );
1742        f.render_widget(
1743            Paragraph::new("12:00")
1744                .style(Style::default().fg(ColorScheme::GRAY_TEXT))
1745                .alignment(Alignment::Center),
1746            Rect::new(timeline_area.x, label_y, timeline_area.width, 1),
1747        );
1748        f.render_widget(
1749            Paragraph::new("24:00")
1750                .style(Style::default().fg(ColorScheme::GRAY_TEXT))
1751                .alignment(Alignment::Right),
1752            Rect::new(timeline_area.x, label_y, timeline_area.width, 1),
1753        );
1754    }
1755
1756    fn render_project_switcher(&self, f: &mut Frame, area: Rect) {
1757        let popup_area = self.centered_rect(60, 50, area);
1758
1759        let block = Block::default()
1760            .borders(Borders::ALL)
1761            .border_style(Style::default().fg(ColorScheme::CLEAN_BLUE))
1762            .title(" Select Project ")
1763            .title_alignment(Alignment::Center)
1764            .style(Style::default().bg(ColorScheme::CLEAN_BG));
1765
1766        f.render_widget(block.clone(), popup_area);
1767
1768        let list_area = block.inner(popup_area);
1769
1770        if self.available_projects.is_empty() {
1771            let no_projects = Paragraph::new("No projects found")
1772                .alignment(Alignment::Center)
1773                .style(Style::default().fg(ColorScheme::GRAY_TEXT));
1774            f.render_widget(no_projects, list_area);
1775        } else {
1776            let items: Vec<ListItem> = self
1777                .available_projects
1778                .iter()
1779                .enumerate()
1780                .map(|(i, p)| {
1781                    let style = if i == self.selected_project_index {
1782                        Style::default()
1783                            .fg(ColorScheme::CLEAN_BG)
1784                            .bg(ColorScheme::CLEAN_BLUE)
1785                    } else {
1786                        Style::default().fg(ColorScheme::WHITE_TEXT)
1787                    };
1788                    ListItem::new(format!(" {} ", p.name)).style(style)
1789                })
1790                .collect();
1791
1792            let list = List::new(items);
1793            f.render_widget(list, list_area);
1794        }
1795    }
1796
1797    fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
1798        let popup_layout = Layout::default()
1799            .direction(Direction::Vertical)
1800            .constraints([
1801                Constraint::Percentage((100 - percent_y) / 2),
1802                Constraint::Percentage(percent_y),
1803                Constraint::Percentage((100 - percent_y) / 2),
1804            ])
1805            .split(r);
1806
1807        Layout::default()
1808            .direction(Direction::Horizontal)
1809            .constraints([
1810                Constraint::Percentage((100 - percent_x) / 2),
1811                Constraint::Percentage(percent_x),
1812                Constraint::Percentage((100 - percent_x) / 2),
1813            ])
1814            .split(popup_layout[1])[1]
1815    }
1816
1817    async fn get_current_session(&mut self) -> Result<Option<Session>> {
1818        if !is_daemon_running() {
1819            return Ok(None);
1820        }
1821
1822        self.ensure_connected().await?;
1823
1824        let response = self
1825            .client
1826            .send_message(&IpcMessage::GetActiveSession)
1827            .await?;
1828        match response {
1829            IpcResponse::ActiveSession(session) => Ok(session),
1830            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get active session: {}", e)),
1831            _ => Ok(None),
1832        }
1833    }
1834
1835    async fn get_project_by_session(&mut self, session: &Session) -> Result<Option<Project>> {
1836        if !is_daemon_running() {
1837            return Ok(None);
1838        }
1839
1840        self.ensure_connected().await?;
1841
1842        let response = self
1843            .client
1844            .send_message(&IpcMessage::GetProject(session.project_id))
1845            .await?;
1846        match response {
1847            IpcResponse::Project(project) => Ok(project),
1848            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get project: {}", e)),
1849            _ => Ok(None),
1850        }
1851    }
1852
1853    async fn get_today_stats(&mut self) -> Result<(i64, i64, i64)> {
1854        // (sessions_count, total_seconds, avg_seconds)
1855        if !is_daemon_running() {
1856            return Ok((0, 0, 0));
1857        }
1858
1859        self.ensure_connected().await?;
1860
1861        let today = chrono::Local::now().date_naive();
1862        let response = self
1863            .client
1864            .send_message(&IpcMessage::GetDailyStats(today))
1865            .await?;
1866        match response {
1867            IpcResponse::DailyStats {
1868                sessions_count,
1869                total_seconds,
1870                avg_seconds,
1871            } => Ok((sessions_count, total_seconds, avg_seconds)),
1872            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get daily stats: {}", e)),
1873            _ => Ok((0, 0, 0)),
1874        }
1875    }
1876
1877    async fn get_today_sessions(&mut self) -> Result<Vec<Session>> {
1878        if !is_daemon_running() {
1879            return Ok(Vec::new());
1880        }
1881
1882        self.ensure_connected().await?;
1883
1884        let today = chrono::Local::now().date_naive();
1885        let response = self
1886            .client
1887            .send_message(&IpcMessage::GetSessionsForDate(today))
1888            .await?;
1889        match response {
1890            IpcResponse::SessionList(sessions) => Ok(sessions),
1891            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get sessions: {}", e)),
1892            _ => Ok(Vec::new()),
1893        }
1894    }
1895
1896    async fn get_history_sessions(&mut self) -> Result<Vec<Session>> {
1897        if !is_daemon_running() {
1898            return Ok(Vec::new());
1899        }
1900
1901        self.ensure_connected().await?;
1902
1903        // For now, get a date range of the last 30 days
1904        let end_date = chrono::Local::now().date_naive();
1905        let _start_date = end_date - chrono::Duration::days(30);
1906        
1907        // Get sessions for the date range (simplified - in a real implementation, 
1908        // this would use a new IPC message like GetSessionsInRange)
1909        let mut all_sessions = Vec::new();
1910        for days_ago in 0..30 {
1911            let date = end_date - chrono::Duration::days(days_ago);
1912            if let Ok(IpcResponse::SessionList(sessions)) = self
1913                .client
1914                .send_message(&IpcMessage::GetSessionsForDate(date))
1915                .await
1916            {
1917                all_sessions.extend(sessions);
1918            }
1919        }
1920
1921        // Apply filters
1922        let filtered_sessions: Vec<Session> = all_sessions
1923            .into_iter()
1924            .filter(|session| {
1925                // Apply search filter if set
1926                if !self.session_filter.search_text.is_empty() {
1927                    if let Some(notes) = &session.notes {
1928                        if !notes.to_lowercase().contains(&self.session_filter.search_text.to_lowercase()) {
1929                            return false;
1930                        }
1931                    } else {
1932                        return false;
1933                    }
1934                }
1935                true
1936            })
1937            .collect();
1938
1939        Ok(filtered_sessions)
1940    }
1941
1942    async fn send_activity_heartbeat(&mut self) -> Result<()> {
1943        if !is_daemon_running() {
1944            return Ok(());
1945        }
1946
1947        self.ensure_connected().await?;
1948
1949        let _response = self
1950            .client
1951            .send_message(&IpcMessage::ActivityHeartbeat)
1952            .await?;
1953        Ok(())
1954    }
1955
1956    // Helper methods for project switcher navigation
1957
1958    fn navigate_projects(&mut self, direction: i32) {
1959        if self.available_projects.is_empty() {
1960            return;
1961        }
1962
1963        let new_index = self.selected_project_index as i32 + direction;
1964        if new_index >= 0 && new_index < self.available_projects.len() as i32 {
1965            self.selected_project_index = new_index as usize;
1966        }
1967    }
1968
1969    async fn refresh_projects(&mut self) -> Result<()> {
1970        if !is_daemon_running() {
1971            return Ok(());
1972        }
1973
1974        self.ensure_connected().await?;
1975
1976        let response = self.client.send_message(&IpcMessage::ListProjects).await?;
1977        if let IpcResponse::ProjectList(projects) = response {
1978            self.available_projects = projects;
1979            self.selected_project_index = 0;
1980        }
1981        Ok(())
1982    }
1983
1984    async fn switch_to_selected_project(&mut self) -> Result<()> {
1985        if let Some(selected_project) = self.available_projects.get(self.selected_project_index) {
1986            let project_id = selected_project.id.unwrap_or(0);
1987
1988            self.ensure_connected().await?;
1989
1990            // Switch to the selected project
1991            let response = self
1992                .client
1993                .send_message(&IpcMessage::SwitchProject(project_id))
1994                .await?;
1995            match response {
1996                IpcResponse::Success => {
1997                    self.show_project_switcher = false;
1998                }
1999                IpcResponse::Error(e) => {
2000                    return Err(anyhow::anyhow!("Failed to switch project: {}", e))
2001                }
2002                _ => return Err(anyhow::anyhow!("Unexpected response")),
2003            }
2004        }
2005        Ok(())
2006    }
2007
2008    fn render_header(&self, f: &mut Frame, area: Rect) {
2009        let time_str = Local::now().format("%H:%M").to_string();
2010        let date_str = Local::now().format("%A, %B %d").to_string();
2011
2012        let header_layout = Layout::default()
2013            .direction(Direction::Horizontal)
2014            .constraints([
2015                Constraint::Percentage(50), // Title
2016                Constraint::Percentage(50), // Date/Time
2017            ])
2018            .split(area);
2019
2020        f.render_widget(
2021            Paragraph::new("TEMPO").style(
2022                Style::default()
2023                    .fg(ColorScheme::CLEAN_GOLD)
2024                    .add_modifier(Modifier::BOLD),
2025            ),
2026            header_layout[0],
2027        );
2028
2029        f.render_widget(
2030            Paragraph::new(format!("{}  {}", date_str, time_str))
2031                .alignment(Alignment::Right)
2032                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
2033            header_layout[1],
2034        );
2035    }
2036
2037    fn get_daily_stats(&self) -> &(i64, i64, i64) {
2038        &self.daily_stats
2039    }
2040}