1use super::{Cli, Commands, ProjectAction, SessionAction, TagAction, ConfigAction, GoalAction, EstimateAction, BranchAction, TemplateAction, WorkspaceAction, CalendarAction, IssueAction, ClientAction};
2use crate::utils::ipc::{IpcClient, IpcMessage, IpcResponse, get_socket_path, is_daemon_running};
3use crate::db::queries::{ProjectQueries, SessionQueries, TagQueries, SessionEditQueries};
4use crate::db::advanced_queries::{GoalQueries, GitBranchQueries, TimeEstimateQueries, InsightQueries, TemplateQueries, WorkspaceQueries};
5use crate::db::{Database, get_database_path};
6use crate::models::{Project, Tag, Goal, TimeEstimate, ProjectTemplate, Workspace};
7use crate::utils::paths::{canonicalize_path, detect_project_name, get_git_hash, is_git_repository};
8use crate::utils::config::{load_config, save_config};
9use crate::cli::reports::ReportGenerator;
10use anyhow::Result;
11use std::env;
12use std::path::PathBuf;
13use std::process::{Command, Stdio};
14use chrono::{Utc, TimeZone};
15
16use crate::ui::dashboard::Dashboard;
17use crate::ui::timer::InteractiveTimer;
18use crate::ui::history::SessionHistoryBrowser;
19use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
20use ratatui::{backend::CrosstermBackend, Terminal};
21use std::io;
22use tokio::runtime::Handle;
23
24pub async fn handle_command(cli: Cli) -> Result<()> {
25 match cli.command {
26 Commands::Start => {
27 start_daemon().await
28 }
29
30 Commands::Stop => {
31 stop_daemon().await
32 }
33
34 Commands::Restart => {
35 restart_daemon().await
36 }
37
38 Commands::Status => {
39 status_daemon().await
40 }
41
42 Commands::Init { name, path, description } => {
43 init_project(name, path, description).await
44 }
45
46 Commands::List { all, tag } => {
47 list_projects(all, tag).await
48 }
49
50 Commands::Report { project, from, to, format, group } => {
51 generate_report(project, from, to, format, group).await
52 }
53
54 Commands::Project { action } => {
55 handle_project_action(action).await
56 }
57
58 Commands::Session { action } => {
59 handle_session_action(action).await
60 }
61
62 Commands::Tag { action } => {
63 handle_tag_action(action).await
64 }
65
66 Commands::Config { action } => {
67 handle_config_action(action).await
68 }
69
70 Commands::Dashboard => {
71 launch_dashboard().await
72 }
73
74 Commands::Tui => {
75 launch_dashboard().await
76 }
77
78 Commands::Timer => {
79 launch_timer().await
80 }
81
82 Commands::History => {
83 launch_history().await
84 }
85
86 Commands::Goal { action } => {
87 handle_goal_action(action).await
88 }
89
90 Commands::Insights { period, project } => {
91 show_insights(period, project).await
92 }
93
94 Commands::Summary { period, from } => {
95 show_summary(period, from).await
96 }
97
98 Commands::Compare { projects, from, to } => {
99 compare_projects(projects, from, to).await
100 }
101
102 Commands::Estimate { action } => {
103 handle_estimate_action(action).await
104 }
105
106 Commands::Branch { action } => {
107 handle_branch_action(action).await
108 }
109
110 Commands::Template { action } => {
111 handle_template_action(action).await
112 }
113
114 Commands::Workspace { action } => {
115 handle_workspace_action(action).await
116 }
117
118 Commands::Calendar { action } => {
119 handle_calendar_action(action).await
120 }
121
122 Commands::Issue { action } => {
123 handle_issue_action(action).await
124 }
125
126 Commands::Client { action } => {
127 handle_client_action(action).await
128 }
129
130 Commands::Completions { shell } => {
131 Cli::generate_completions(shell);
132 Ok(())
133 }
134 }
135}
136
137async fn handle_project_action(action: ProjectAction) -> Result<()> {
138 match action {
139 ProjectAction::Archive { project } => {
140 archive_project(project).await
141 }
142
143 ProjectAction::Unarchive { project } => {
144 unarchive_project(project).await
145 }
146
147 ProjectAction::UpdatePath { project, path } => {
148 update_project_path(project, path).await
149 }
150
151 ProjectAction::AddTag { project, tag } => {
152 add_tag_to_project(project, tag).await
153 }
154
155 ProjectAction::RemoveTag { project, tag } => {
156 remove_tag_from_project(project, tag).await
157 }
158 }
159}
160
161async fn handle_session_action(action: SessionAction) -> Result<()> {
162 match action {
163 SessionAction::Start { project, context } => {
164 start_session(project, context).await
165 }
166
167 SessionAction::Stop => {
168 stop_session().await
169 }
170
171 SessionAction::Pause => {
172 pause_session().await
173 }
174
175 SessionAction::Resume => {
176 resume_session().await
177 }
178
179 SessionAction::Current => {
180 current_session().await
181 }
182
183 SessionAction::List { limit, project } => {
184 list_sessions(limit, project).await
185 }
186
187 SessionAction::Edit { id, start, end, project, reason } => {
188 edit_session(id, start, end, project, reason).await
189 }
190
191 SessionAction::Delete { id, force } => {
192 delete_session(id, force).await
193 }
194
195 SessionAction::Merge { session_ids, project, notes } => {
196 merge_sessions(session_ids, project, notes).await
197 }
198
199 SessionAction::Split { session_id, split_times, notes } => {
200 split_session(session_id, split_times, notes).await
201 }
202 }
203}
204
205async fn handle_tag_action(action: TagAction) -> Result<()> {
206 match action {
207 TagAction::Create { name, color, description } => {
208 create_tag(name, color, description).await
209 }
210
211 TagAction::List => {
212 list_tags().await
213 }
214
215 TagAction::Delete { name } => {
216 delete_tag(name).await
217 }
218 }
219}
220
221async fn handle_config_action(action: ConfigAction) -> Result<()> {
222 match action {
223 ConfigAction::Show => {
224 show_config().await
225 }
226
227 ConfigAction::Set { key, value } => {
228 set_config(key, value).await
229 }
230
231 ConfigAction::Get { key } => {
232 get_config(key).await
233 }
234
235 ConfigAction::Reset => {
236 reset_config().await
237 }
238 }
239}
240
241async fn start_daemon() -> Result<()> {
243 if is_daemon_running() {
244 println!("Daemon is already running");
245 return Ok(());
246 }
247
248 println!("Starting tempo daemon...");
249
250 let current_exe = std::env::current_exe()?;
251 let daemon_exe = current_exe.with_file_name("tempo-daemon");
252
253 if !daemon_exe.exists() {
254 return Err(anyhow::anyhow!("tempo-daemon executable not found at {:?}", daemon_exe));
255 }
256
257 let mut cmd = Command::new(&daemon_exe);
258 cmd.stdout(Stdio::null())
259 .stderr(Stdio::null())
260 .stdin(Stdio::null());
261
262 #[cfg(unix)]
263 {
264 use std::os::unix::process::CommandExt;
265 cmd.process_group(0);
266 }
267
268 let child = cmd.spawn()?;
269
270 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
272
273 if is_daemon_running() {
274 println!("Daemon started successfully (PID: {})", child.id());
275 Ok(())
276 } else {
277 Err(anyhow::anyhow!("Failed to start daemon"))
278 }
279}
280
281async fn stop_daemon() -> Result<()> {
282 if !is_daemon_running() {
283 println!("Daemon is not running");
284 return Ok(());
285 }
286
287 println!("Stopping tempo daemon...");
288
289 if let Ok(socket_path) = get_socket_path() {
291 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
292 match client.send_message(&IpcMessage::Shutdown).await {
293 Ok(_) => {
294 println!("Daemon stopped successfully");
295 return Ok(());
296 }
297 Err(e) => {
298 eprintln!("Failed to send shutdown message: {}", e);
299 }
300 }
301 }
302 }
303
304 if let Ok(Some(pid)) = crate::utils::ipc::read_pid_file() {
306 #[cfg(unix)]
307 {
308 let result = Command::new("kill")
309 .arg(pid.to_string())
310 .output();
311
312 match result {
313 Ok(_) => println!("Daemon stopped via kill signal"),
314 Err(e) => eprintln!("Failed to kill daemon: {}", e),
315 }
316 }
317
318 #[cfg(windows)]
319 {
320 let result = Command::new("taskkill")
321 .args(&["/PID", &pid.to_string(), "/F"])
322 .output();
323
324 match result {
325 Ok(_) => println!("Daemon stopped via taskkill"),
326 Err(e) => eprintln!("Failed to kill daemon: {}", e),
327 }
328 }
329 }
330
331 Ok(())
332}
333
334async fn restart_daemon() -> Result<()> {
335 println!("Restarting tempo daemon...");
336 stop_daemon().await?;
337 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
338 start_daemon().await
339}
340
341async fn status_daemon() -> Result<()> {
342 if !is_daemon_running() {
343 print_daemon_not_running();
344 return Ok(());
345 }
346
347 if let Ok(socket_path) = get_socket_path() {
348 match IpcClient::connect(&socket_path).await {
349 Ok(mut client) => {
350 match client.send_message(&IpcMessage::GetStatus).await {
351 Ok(IpcResponse::Status { daemon_running: _, active_session, uptime }) => {
352 print_daemon_status(uptime, active_session.as_ref());
353 }
354 Ok(IpcResponse::Pong) => {
355 print_daemon_status(0, None); }
357 Ok(other) => {
358 println!("Daemon is running (unexpected response: {:?})", other);
359 }
360 Err(e) => {
361 println!("Daemon is running but not responding: {}", e);
362 }
363 }
364 }
365 Err(e) => {
366 println!("Daemon appears to be running but cannot connect: {}", e);
367 }
368 }
369 } else {
370 println!("Cannot determine socket path");
371 }
372
373 Ok(())
374}
375
376async fn start_session(project: Option<String>, context: Option<String>) -> Result<()> {
378 if !is_daemon_running() {
379 println!("Daemon is not running. Start it with 'tempo start'");
380 return Ok(());
381 }
382
383 let project_path = if let Some(proj) = project {
384 PathBuf::from(proj)
385 } else {
386 env::current_dir()?
387 };
388
389 let context = context.unwrap_or_else(|| "manual".to_string());
390
391 let socket_path = get_socket_path()?;
392 let mut client = IpcClient::connect(&socket_path).await?;
393
394 let message = IpcMessage::StartSession {
395 project_path: Some(project_path.clone()),
396 context
397 };
398
399 match client.send_message(&message).await {
400 Ok(IpcResponse::Ok) => {
401 println!("Session started for project at {:?}", project_path);
402 }
403 Ok(IpcResponse::Error(message)) => {
404 println!("Failed to start session: {}", message);
405 }
406 Ok(other) => {
407 println!("Unexpected response: {:?}", other);
408 }
409 Err(e) => {
410 println!("Failed to communicate with daemon: {}", e);
411 }
412 }
413
414 Ok(())
415}
416
417async fn stop_session() -> Result<()> {
418 if !is_daemon_running() {
419 println!("Daemon is not running");
420 return Ok(());
421 }
422
423 let socket_path = get_socket_path()?;
424 let mut client = IpcClient::connect(&socket_path).await?;
425
426 match client.send_message(&IpcMessage::StopSession).await {
427 Ok(IpcResponse::Ok) => {
428 println!("Session stopped");
429 }
430 Ok(IpcResponse::Error(message)) => {
431 println!("Failed to stop session: {}", message);
432 }
433 Ok(other) => {
434 println!("Unexpected response: {:?}", other);
435 }
436 Err(e) => {
437 println!("Failed to communicate with daemon: {}", e);
438 }
439 }
440
441 Ok(())
442}
443
444async fn pause_session() -> Result<()> {
445 if !is_daemon_running() {
446 println!("Daemon is not running");
447 return Ok(());
448 }
449
450 let socket_path = get_socket_path()?;
451 let mut client = IpcClient::connect(&socket_path).await?;
452
453 match client.send_message(&IpcMessage::PauseSession).await {
454 Ok(IpcResponse::Ok) => {
455 println!("Session paused");
456 }
457 Ok(IpcResponse::Error(message)) => {
458 println!("Failed to pause session: {}", message);
459 }
460 Ok(other) => {
461 println!("Unexpected response: {:?}", other);
462 }
463 Err(e) => {
464 println!("Failed to communicate with daemon: {}", e);
465 }
466 }
467
468 Ok(())
469}
470
471async fn resume_session() -> Result<()> {
472 if !is_daemon_running() {
473 println!("Daemon is not running");
474 return Ok(());
475 }
476
477 let socket_path = get_socket_path()?;
478 let mut client = IpcClient::connect(&socket_path).await?;
479
480 match client.send_message(&IpcMessage::ResumeSession).await {
481 Ok(IpcResponse::Ok) => {
482 println!("Session resumed");
483 }
484 Ok(IpcResponse::Error(message)) => {
485 println!("Failed to resume session: {}", message);
486 }
487 Ok(other) => {
488 println!("Unexpected response: {:?}", other);
489 }
490 Err(e) => {
491 println!("Failed to communicate with daemon: {}", e);
492 }
493 }
494
495 Ok(())
496}
497
498async fn current_session() -> Result<()> {
499 if !is_daemon_running() {
500 print_daemon_not_running();
501 return Ok(());
502 }
503
504 let socket_path = get_socket_path()?;
505 let mut client = IpcClient::connect(&socket_path).await?;
506
507 match client.send_message(&IpcMessage::GetActiveSession).await {
508 Ok(IpcResponse::SessionInfo(session)) => {
509 print_formatted_session(&session)?;
510 }
511 Ok(IpcResponse::Error(message)) => {
512 print_no_active_session(&message);
513 }
514 Ok(other) => {
515 println!("Unexpected response: {:?}", other);
516 }
517 Err(e) => {
518 println!("Failed to communicate with daemon: {}", e);
519 }
520 }
521
522 Ok(())
523}
524
525async fn generate_report(
527 project: Option<String>,
528 from: Option<String>,
529 to: Option<String>,
530 format: Option<String>,
531 group: Option<String>,
532) -> Result<()> {
533 println!("Generating time report...");
534
535 let generator = ReportGenerator::new()?;
536 let report = generator.generate_report(project, from, to, group)?;
537
538 match format.as_deref() {
539 Some("csv") => {
540 let output_path = PathBuf::from("tempo-report.csv");
541 generator.export_csv(&report, &output_path)?;
542 println!("Report exported to: {:?}", output_path);
543 }
544 Some("json") => {
545 let output_path = PathBuf::from("tempo-report.json");
546 generator.export_json(&report, &output_path)?;
547 println!("Report exported to: {:?}", output_path);
548 }
549 _ => {
550 print_formatted_report(&report)?;
552 }
553 }
554
555 Ok(())
556}
557
558fn print_formatted_session(session: &crate::utils::ipc::SessionInfo) -> Result<()> {
560 let context_color = match session.context.as_str() {
562 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", };
568
569 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
570 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
571 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
572 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m●\x1b[0m \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m");
573 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_name, 25));
574 println!("\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(session.duration));
575 println!("\x1b[36m│\x1b[0m Started: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", session.start_time.format("%H:%M:%S").to_string());
576 println!("\x1b[36m│\x1b[0m Context: {}{:<25}\x1b[0m \x1b[36m│\x1b[0m", context_color, truncate_string(&session.context, 25));
577 println!("\x1b[36m│\x1b[0m Path: \x1b[2;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_path.to_string_lossy(), 25));
578 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
579 Ok(())
580}
581
582fn print_formatted_report(report: &crate::cli::reports::TimeReport) -> Result<()> {
583 let get_context_color = |context: &str| -> &str {
585 match context {
586 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", }
592 };
593
594 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
595 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Report\x1b[0m \x1b[36m│\x1b[0m");
596 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
597
598 for (project_name, project_summary) in &report.projects {
599 println!("\x1b[36m│\x1b[0m \x1b[1;33m{:<20}\x1b[0m \x1b[1;32m{:>15}\x1b[0m \x1b[36m│\x1b[0m",
600 truncate_string(project_name, 20),
601 format_duration_fancy(project_summary.total_duration)
602 );
603
604 for (context, duration) in &project_summary.contexts {
605 let context_color = get_context_color(context);
606 println!("\x1b[36m│\x1b[0m {}{:<15}\x1b[0m \x1b[32m{:>20}\x1b[0m \x1b[36m│\x1b[0m",
607 context_color,
608 truncate_string(context, 15),
609 format_duration_fancy(*duration)
610 );
611 }
612 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
613 }
614
615 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
616 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal Time:\x1b[0m \x1b[1;32m{:>26}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(report.total_duration));
617 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
618 Ok(())
619}
620
621fn format_duration_fancy(seconds: i64) -> String {
622 let hours = seconds / 3600;
623 let minutes = (seconds % 3600) / 60;
624 let secs = seconds % 60;
625
626 if hours > 0 {
627 format!("{}h {}m {}s", hours, minutes, secs)
628 } else if minutes > 0 {
629 format!("{}m {}s", minutes, secs)
630 } else {
631 format!("{}s", secs)
632 }
633}
634
635fn truncate_string(s: &str, max_len: usize) -> String {
636 if s.len() <= max_len {
637 format!("{:<width$}", s, width = max_len)
638 } else {
639 format!("{:.width$}...", s, width = max_len.saturating_sub(3))
640 }
641}
642
643fn print_daemon_not_running() {
645 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
646 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
647 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
648 println!("\x1b[36m│\x1b[0m Status: \x1b[1;31m●\x1b[0m \x1b[31mOffline\x1b[0m \x1b[36m│\x1b[0m");
649 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
650 println!("\x1b[36m│\x1b[0m \x1b[33mDaemon is not running.\x1b[0m \x1b[36m│\x1b[0m");
651 println!("\x1b[36m│\x1b[0m \x1b[37mStart it with:\x1b[0m \x1b[96mtempo start\x1b[0m \x1b[36m│\x1b[0m");
652 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
653}
654
655fn print_no_active_session(message: &str) {
656 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
657 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
658 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
659 println!("\x1b[36m│\x1b[0m Status: \x1b[1;33m○\x1b[0m \x1b[33mIdle\x1b[0m \x1b[36m│\x1b[0m");
660 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
661 println!("\x1b[36m│\x1b[0m \x1b[90m{:<37}\x1b[0m \x1b[36m│\x1b[0m", message);
662 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
663 println!("\x1b[36m│\x1b[0m \x1b[37mStart tracking:\x1b[0m \x1b[36m│\x1b[0m");
664 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
665 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
666}
667
668fn print_daemon_status(uptime: u64, active_session: Option<&crate::utils::ipc::SessionInfo>) {
669 let uptime_formatted = format_duration_fancy(uptime as i64);
670
671 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
672 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
673 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
674 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m●\x1b[0m \x1b[32mOnline\x1b[0m \x1b[36m│\x1b[0m");
675 println!("\x1b[36m│\x1b[0m Uptime: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", uptime_formatted);
676
677 if let Some(session) = active_session {
678 let context_color = match session.context.as_str() {
679 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m",
680 "manual" => "\x1b[94m", _ => "\x1b[97m",
681 };
682
683 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
684 println!("\x1b[36m│\x1b[0m \x1b[1;37mActive Session:\x1b[0m \x1b[36m│\x1b[0m");
685 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_name, 23));
686 println!("\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<22}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(session.duration));
687 println!("\x1b[36m│\x1b[0m Context: {}{:<23}\x1b[0m \x1b[36m│\x1b[0m", context_color, session.context);
688 } else {
689 println!("\x1b[36m│\x1b[0m Session: \x1b[33mNo active session\x1b[0m \x1b[36m│\x1b[0m");
690 }
691
692 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
693}
694
695async fn init_project(name: Option<String>, path: Option<PathBuf>, description: Option<String>) -> Result<()> {
697 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
698 let canonical_path = canonicalize_path(&project_path)?;
699
700 let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
701
702 let db_path = get_database_path()?;
704 let db = Database::new(&db_path)?;
705
706 if let Some(existing) = ProjectQueries::find_by_path(&db.connection, &canonical_path)? {
708 println!("\x1b[33m⚠ Project already exists:\x1b[0m {}", existing.name);
709 return Ok(());
710 }
711
712 let git_hash = if is_git_repository(&canonical_path) {
714 get_git_hash(&canonical_path)
715 } else {
716 None
717 };
718
719 let mut project = Project::new(project_name.clone(), canonical_path.clone())
721 .with_git_hash(git_hash.clone())
722 .with_description(description.clone());
723
724 let project_id = ProjectQueries::create(&db.connection, &project)?;
726 project.id = Some(project_id);
727
728 let marker_path = canonical_path.join(".tempo");
730 if !marker_path.exists() {
731 std::fs::write(&marker_path, format!("# Tempo time tracking project\nname: {}\n", project_name))?;
732 }
733
734 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
735 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Initialized\x1b[0m \x1b[36m│\x1b[0m");
736 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
737 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
738 println!("\x1b[36m│\x1b[0m Path: \x1b[37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&canonical_path.to_string_lossy(), 27));
739 if let Some(desc) = &description {
740 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
741 }
742 if git_hash.is_some() {
743 println!("\x1b[36m│\x1b[0m Type: \x1b[32mGit Repository\x1b[0m \x1b[36m│\x1b[0m");
744 } else {
745 println!("\x1b[36m│\x1b[0m Type: \x1b[37mStandard Project\x1b[0m \x1b[36m│\x1b[0m");
746 }
747 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", project_id);
748 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
749 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project created successfully\x1b[0m \x1b[36m│\x1b[0m");
750 println!("\x1b[36m│\x1b[0m \x1b[32m✓ .tempo marker file added\x1b[0m \x1b[36m│\x1b[0m");
751 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
752 println!("\x1b[36m│\x1b[0m \x1b[37mStart tracking:\x1b[0m \x1b[36m│\x1b[0m");
753 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
754 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
755
756 Ok(())
757}
758
759async fn list_projects(include_archived: bool, tag_filter: Option<String>) -> Result<()> {
760 let db_path = get_database_path()?;
762 let db = Database::new(&db_path)?;
763
764 let projects = ProjectQueries::list_all(&db.connection, include_archived)?;
766
767 if projects.is_empty() {
768 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
769 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Projects\x1b[0m \x1b[36m│\x1b[0m");
770 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
771 println!("\x1b[36m│\x1b[0m No projects found. \x1b[36m│\x1b[0m");
772 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
773 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a project:\x1b[0m \x1b[36m│\x1b[0m");
774 println!("\x1b[36m│\x1b[0m \x1b[96mtempo init [project-name]\x1b[0m \x1b[36m│\x1b[0m");
775 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
776 return Ok(());
777 }
778
779 let filtered_projects = if let Some(_tag) = tag_filter {
781 projects
783 } else {
784 projects
785 };
786
787 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
788 println!("\x1b[36m│\x1b[0m \x1b[1;37mProjects\x1b[0m \x1b[36m│\x1b[0m");
789 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
790
791 for project in &filtered_projects {
792 let status_icon = if project.is_archived { "📦" } else { "📁" };
793 let status_color = if project.is_archived { "\x1b[90m" } else { "\x1b[37m" };
794 let git_indicator = if project.git_hash.is_some() { " (git)" } else { "" };
795
796 println!("\x1b[36m│\x1b[0m {} {}{:<25}\x1b[0m \x1b[36m│\x1b[0m",
797 status_icon,
798 status_color,
799 format!("{}{}", truncate_string(&project.name, 20), git_indicator)
800 );
801
802 if let Some(description) = &project.description {
803 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(description, 35));
804 }
805
806 let path_display = project.path.to_string_lossy();
807 if path_display.len() > 35 {
808 let home_dir = dirs::home_dir();
809 let display_path = if let Some(home) = home_dir {
810 if let Ok(stripped) = project.path.strip_prefix(&home) {
811 format!("~/{}", stripped.display())
812 } else {
813 path_display.to_string()
814 }
815 } else {
816 path_display.to_string()
817 };
818 println!("\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&display_path, 35));
819 } else {
820 println!("\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&path_display, 35));
821 }
822
823 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
824 }
825
826 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
827 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
828 format!("{} projects", filtered_projects.len())
829 );
830 if include_archived {
831 let active_count = filtered_projects.iter().filter(|p| !p.is_archived).count();
832 let archived_count = filtered_projects.iter().filter(|p| p.is_archived).count();
833 println!("\x1b[36m│\x1b[0m \x1b[37mActive:\x1b[0m {:<15} \x1b[90mArchived:\x1b[0m {:<8} \x1b[36m│\x1b[0m",
834 active_count, archived_count
835 );
836 }
837 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
838
839 Ok(())
840}
841
842async fn create_tag(name: String, color: Option<String>, description: Option<String>) -> Result<()> {
844 let db_path = get_database_path()?;
846 let db = Database::new(&db_path)?;
847
848 let mut tag = Tag::new(name.clone());
850 if let Some(c) = color {
851 tag = tag.with_color(c);
852 }
853 if let Some(d) = description {
854 tag = tag.with_description(d);
855 }
856
857 tag.validate()?;
859
860 let existing_tags = TagQueries::list_all(&db.connection)?;
862 if existing_tags.iter().any(|t| t.name == tag.name) {
863 println!("\x1b[33m⚠ Tag already exists:\x1b[0m {}", tag.name);
864 return Ok(());
865 }
866
867 let tag_id = TagQueries::create(&db.connection, &tag)?;
869
870 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
871 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Created\x1b[0m \x1b[36m│\x1b[0m");
872 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
873 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&tag.name, 27));
874 if let Some(color_val) = &tag.color {
875 println!("\x1b[36m│\x1b[0m Color: \x1b[37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(color_val, 27));
876 }
877 if let Some(desc) = &tag.description {
878 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
879 }
880 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", tag_id);
881 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
882 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag created successfully\x1b[0m \x1b[36m│\x1b[0m");
883 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
884
885 Ok(())
886}
887
888async fn list_tags() -> Result<()> {
889 let db_path = get_database_path()?;
891 let db = Database::new(&db_path)?;
892
893 let tags = TagQueries::list_all(&db.connection)?;
895
896 if tags.is_empty() {
897 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
898 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Tags\x1b[0m \x1b[36m│\x1b[0m");
899 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
900 println!("\x1b[36m│\x1b[0m No tags found. \x1b[36m│\x1b[0m");
901 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
902 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a tag:\x1b[0m \x1b[36m│\x1b[0m");
903 println!("\x1b[36m│\x1b[0m \x1b[96mtempo tag create <name>\x1b[0m \x1b[36m│\x1b[0m");
904 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
905 return Ok(());
906 }
907
908 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
909 println!("\x1b[36m│\x1b[0m \x1b[1;37mTags\x1b[0m \x1b[36m│\x1b[0m");
910 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
911
912 for tag in &tags {
913 let color_indicator = if let Some(color) = &tag.color {
914 format!(" ({})", color)
915 } else {
916 "".to_string()
917 };
918
919 println!("\x1b[36m│\x1b[0m 🏷️ \x1b[1;33m{:<30}\x1b[0m \x1b[36m│\x1b[0m",
920 format!("{}{}", truncate_string(&tag.name, 25), color_indicator)
921 );
922
923 if let Some(description) = &tag.description {
924 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<33}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(description, 33));
925 }
926
927 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
928 }
929
930 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
931 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
932 format!("{} tags", tags.len())
933 );
934 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
935
936 Ok(())
937}
938
939async fn delete_tag(name: String) -> Result<()> {
940 let db_path = get_database_path()?;
942 let db = Database::new(&db_path)?;
943
944 if TagQueries::find_by_name(&db.connection, &name)?.is_none() {
946 println!("\x1b[31m✗ Tag '{}' not found\x1b[0m", name);
947 return Ok(());
948 }
949
950 let deleted = TagQueries::delete_by_name(&db.connection, &name)?;
952
953 if deleted {
954 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
955 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Deleted\x1b[0m \x1b[36m│\x1b[0m");
956 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
957 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
958 println!("\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m");
959 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
960 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag deleted successfully\x1b[0m \x1b[36m│\x1b[0m");
961 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
962 } else {
963 println!("\x1b[31m✗ Failed to delete tag '{}'\x1b[0m", name);
964 }
965
966 Ok(())
967}
968
969async fn show_config() -> Result<()> {
971 let config = load_config()?;
972
973 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
974 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration\x1b[0m \x1b[36m│\x1b[0m");
975 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
976 println!("\x1b[36m│\x1b[0m idle_timeout_minutes: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.idle_timeout_minutes);
977 println!("\x1b[36m│\x1b[0m auto_pause_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.auto_pause_enabled);
978 println!("\x1b[36m│\x1b[0m default_context: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.default_context);
979 println!("\x1b[36m│\x1b[0m max_session_hours: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.max_session_hours);
980 println!("\x1b[36m│\x1b[0m backup_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.backup_enabled);
981 println!("\x1b[36m│\x1b[0m log_level: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.log_level);
982
983 if !config.custom_settings.is_empty() {
984 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
985 println!("\x1b[36m│\x1b[0m \x1b[1;37mCustom Settings:\x1b[0m \x1b[36m│\x1b[0m");
986 for (key, value) in &config.custom_settings {
987 println!("\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
988 truncate_string(key, 20),
989 truncate_string(value, 16)
990 );
991 }
992 }
993
994 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
995
996 Ok(())
997}
998
999async fn get_config(key: String) -> Result<()> {
1000 let config = load_config()?;
1001
1002 let value = match key.as_str() {
1003 "idle_timeout_minutes" => Some(config.idle_timeout_minutes.to_string()),
1004 "auto_pause_enabled" => Some(config.auto_pause_enabled.to_string()),
1005 "default_context" => Some(config.default_context),
1006 "max_session_hours" => Some(config.max_session_hours.to_string()),
1007 "backup_enabled" => Some(config.backup_enabled.to_string()),
1008 "log_level" => Some(config.log_level),
1009 _ => config.custom_settings.get(&key).cloned(),
1010 };
1011
1012 match value {
1013 Some(val) => {
1014 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1015 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Value\x1b[0m \x1b[36m│\x1b[0m");
1016 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1017 println!("\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1018 truncate_string(&key, 20),
1019 truncate_string(&val, 16)
1020 );
1021 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1022 }
1023 None => {
1024 println!("\x1b[31m✗ Configuration key not found:\x1b[0m {}", key);
1025 }
1026 }
1027
1028 Ok(())
1029}
1030
1031async fn set_config(key: String, value: String) -> Result<()> {
1032 let mut config = load_config()?;
1033
1034 let display_value = value.clone(); match key.as_str() {
1037 "idle_timeout_minutes" => {
1038 config.idle_timeout_minutes = value.parse()?;
1039 }
1040 "auto_pause_enabled" => {
1041 config.auto_pause_enabled = value.parse()?;
1042 }
1043 "default_context" => {
1044 config.default_context = value;
1045 }
1046 "max_session_hours" => {
1047 config.max_session_hours = value.parse()?;
1048 }
1049 "backup_enabled" => {
1050 config.backup_enabled = value.parse()?;
1051 }
1052 "log_level" => {
1053 config.log_level = value;
1054 }
1055 _ => {
1056 config.set_custom(key.clone(), value);
1057 }
1058 }
1059
1060 config.validate()?;
1061 save_config(&config)?;
1062
1063 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1064 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Updated\x1b[0m \x1b[36m│\x1b[0m");
1065 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1066 println!("\x1b[36m│\x1b[0m {:<20} \x1b[32m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1067 truncate_string(&key, 20),
1068 truncate_string(&display_value, 16)
1069 );
1070 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1071 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Configuration saved successfully\x1b[0m \x1b[36m│\x1b[0m");
1072 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1073
1074 Ok(())
1075}
1076
1077async fn reset_config() -> Result<()> {
1078 let default_config = crate::models::Config::default();
1079 save_config(&default_config)?;
1080
1081 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1082 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Reset\x1b[0m \x1b[36m│\x1b[0m");
1083 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1084 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Configuration reset to defaults\x1b[0m \x1b[36m│\x1b[0m");
1085 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1086 println!("\x1b[36m│\x1b[0m \x1b[37mView current config:\x1b[0m \x1b[36m│\x1b[0m");
1087 println!("\x1b[36m│\x1b[0m \x1b[96mtempo config show\x1b[0m \x1b[36m│\x1b[0m");
1088 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1089
1090 Ok(())
1091}
1092
1093async fn list_sessions(limit: Option<usize>, project_filter: Option<String>) -> Result<()> {
1095 let db_path = get_database_path()?;
1097 let db = Database::new(&db_path)?;
1098
1099 let session_limit = limit.unwrap_or(10);
1100
1101 let project_id = if let Some(project_name) = &project_filter {
1103 match ProjectQueries::find_by_name(&db.connection, project_name)? {
1104 Some(project) => Some(project.id.unwrap()),
1105 None => {
1106 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1107 return Ok(());
1108 }
1109 }
1110 } else {
1111 None
1112 };
1113
1114 let sessions = SessionQueries::list_with_filter(&db.connection, project_id, None, None, Some(session_limit))?;
1115
1116 if sessions.is_empty() {
1117 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1118 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Sessions\x1b[0m \x1b[36m│\x1b[0m");
1119 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1120 println!("\x1b[36m│\x1b[0m No sessions found. \x1b[36m│\x1b[0m");
1121 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1122 println!("\x1b[36m│\x1b[0m \x1b[37mStart a session:\x1b[0m \x1b[36m│\x1b[0m");
1123 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
1124 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1125 return Ok(());
1126 }
1127
1128 let filtered_sessions = if let Some(_project) = project_filter {
1130 sessions
1132 } else {
1133 sessions
1134 };
1135
1136 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1137 println!("\x1b[36m│\x1b[0m \x1b[1;37mRecent Sessions\x1b[0m \x1b[36m│\x1b[0m");
1138 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1139
1140 for session in &filtered_sessions {
1141 let status_icon = if session.end_time.is_some() { "✅" } else { "🔄" };
1142 let duration = if let Some(end) = session.end_time {
1143 (end - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1144 } else {
1145 (Utc::now() - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1146 };
1147
1148 let context_color = match session.context.to_string().as_str() {
1149 "terminal" => "\x1b[96m",
1150 "ide" => "\x1b[95m",
1151 "linked" => "\x1b[93m",
1152 "manual" => "\x1b[94m",
1153 _ => "\x1b[97m",
1154 };
1155
1156 println!("\x1b[36m│\x1b[0m {} \x1b[1;37m{:<32}\x1b[0m \x1b[36m│\x1b[0m",
1157 status_icon,
1158 format!("Session {}", session.id.unwrap_or(0))
1159 );
1160 println!("\x1b[36m│\x1b[0m Duration: \x1b[32m{:<24}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(duration));
1161 println!("\x1b[36m│\x1b[0m Context: {}{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1162 context_color,
1163 session.context.to_string()
1164 );
1165 println!("\x1b[36m│\x1b[0m Started: \x1b[37m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1166 session.start_time.format("%Y-%m-%d %H:%M:%S").to_string()
1167 );
1168 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1169 }
1170
1171 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1172 println!("\x1b[36m│\x1b[0m \x1b[1;37mShowing:\x1b[0m {:<28} \x1b[36m│\x1b[0m",
1173 format!("{} recent sessions", filtered_sessions.len())
1174 );
1175 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1176
1177 Ok(())
1178}
1179
1180async fn edit_session(id: i64, start: Option<String>, end: Option<String>, project: Option<String>, reason: Option<String>) -> Result<()> {
1181 let db_path = get_database_path()?;
1183 let db = Database::new(&db_path)?;
1184
1185 let session = SessionQueries::find_by_id(&db.connection, id)?;
1187 let session = match session {
1188 Some(s) => s,
1189 None => {
1190 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1191 return Ok(());
1192 }
1193 };
1194
1195 let original_start = session.start_time;
1196 let original_end = session.end_time;
1197
1198 let mut new_start = original_start;
1200 let mut new_end = original_end;
1201 let mut new_project_id = session.project_id;
1202
1203 if let Some(start_str) = &start {
1205 new_start = match chrono::DateTime::parse_from_rfc3339(start_str) {
1206 Ok(dt) => dt.with_timezone(&chrono::Utc),
1207 Err(_) => {
1208 match chrono::NaiveDateTime::parse_from_str(start_str, "%Y-%m-%d %H:%M:%S") {
1209 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1210 Err(_) => return Err(anyhow::anyhow!("Invalid start time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"))
1211 }
1212 }
1213 };
1214 }
1215
1216 if let Some(end_str) = &end {
1218 if end_str.to_lowercase() == "null" || end_str.to_lowercase() == "none" {
1219 new_end = None;
1220 } else {
1221 new_end = Some(match chrono::DateTime::parse_from_rfc3339(end_str) {
1222 Ok(dt) => dt.with_timezone(&chrono::Utc),
1223 Err(_) => {
1224 match chrono::NaiveDateTime::parse_from_str(end_str, "%Y-%m-%d %H:%M:%S") {
1225 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1226 Err(_) => return Err(anyhow::anyhow!("Invalid end time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"))
1227 }
1228 }
1229 });
1230 }
1231 }
1232
1233 if let Some(project_name) = &project {
1235 if let Some(proj) = ProjectQueries::find_by_name(&db.connection, project_name)? {
1236 new_project_id = proj.id.unwrap();
1237 } else {
1238 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1239 return Ok(());
1240 }
1241 }
1242
1243 if new_start >= new_end.unwrap_or(chrono::Utc::now()) {
1245 println!("\x1b[31m✗ Start time must be before end time\x1b[0m");
1246 return Ok(());
1247 }
1248
1249 SessionEditQueries::create_edit_record(
1251 &db.connection,
1252 id,
1253 original_start,
1254 original_end,
1255 new_start,
1256 new_end,
1257 reason.clone()
1258 )?;
1259
1260 SessionQueries::update_session(
1262 &db.connection,
1263 id,
1264 if start.is_some() { Some(new_start) } else { None },
1265 if end.is_some() { Some(new_end) } else { None },
1266 if project.is_some() { Some(new_project_id) } else { None },
1267 None
1268 )?;
1269
1270 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1271 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Updated\x1b[0m \x1b[36m│\x1b[0m");
1272 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1273 println!("\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", id);
1274
1275 if start.is_some() {
1276 println!("\x1b[36m│\x1b[0m Start: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1277 truncate_string(&new_start.format("%Y-%m-%d %H:%M:%S").to_string(), 27)
1278 );
1279 }
1280
1281 if end.is_some() {
1282 let end_str = if let Some(e) = new_end {
1283 e.format("%Y-%m-%d %H:%M:%S").to_string()
1284 } else {
1285 "Ongoing".to_string()
1286 };
1287 println!("\x1b[36m│\x1b[0m End: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&end_str, 27));
1288 }
1289
1290 if let Some(r) = &reason {
1291 println!("\x1b[36m│\x1b[0m Reason: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(r, 27));
1292 }
1293
1294 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1295 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session updated with audit trail\x1b[0m \x1b[36m│\x1b[0m");
1296 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1297
1298 Ok(())
1299}
1300
1301async fn delete_session(id: i64, force: bool) -> Result<()> {
1302 let db_path = get_database_path()?;
1304 let db = Database::new(&db_path)?;
1305
1306 let session = SessionQueries::find_by_id(&db.connection, id)?;
1308 let session = match session {
1309 Some(s) => s,
1310 None => {
1311 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1312 return Ok(());
1313 }
1314 };
1315
1316 if session.end_time.is_none() && !force {
1318 println!("\x1b[33m⚠ Cannot delete active session without --force flag\x1b[0m");
1319 println!(" Use: tempo session delete {} --force", id);
1320 return Ok(());
1321 }
1322
1323 SessionQueries::delete_session(&db.connection, id)?;
1325
1326 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1327 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Deleted\x1b[0m \x1b[36m│\x1b[0m");
1328 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1329 println!("\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", id);
1330 println!("\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m");
1331
1332 if session.end_time.is_none() {
1333 println!("\x1b[36m│\x1b[0m Type: \x1b[33mActive session (forced)\x1b[0m \x1b[36m│\x1b[0m");
1334 } else {
1335 println!("\x1b[36m│\x1b[0m Type: \x1b[37mCompleted session\x1b[0m \x1b[36m│\x1b[0m");
1336 }
1337
1338 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1339 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session and audit trail removed\x1b[0m \x1b[36m│\x1b[0m");
1340 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1341
1342 Ok(())
1343}
1344
1345async fn archive_project(project_name: String) -> Result<()> {
1347 let db_path = get_database_path()?;
1348 let db = Database::new(&db_path)?;
1349
1350 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1351 Some(p) => p,
1352 None => {
1353 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1354 return Ok(());
1355 }
1356 };
1357
1358 if project.is_archived {
1359 println!("\x1b[33m⚠ Project '{}' is already archived\x1b[0m", project_name);
1360 return Ok(());
1361 }
1362
1363 let success = ProjectQueries::archive_project(&db.connection, project.id.unwrap())?;
1364
1365 if success {
1366 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1367 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Archived\x1b[0m \x1b[36m│\x1b[0m");
1368 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1369 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1370 println!("\x1b[36m│\x1b[0m Status: \x1b[90mArchived\x1b[0m \x1b[36m│\x1b[0m");
1371 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1372 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project archived successfully\x1b[0m \x1b[36m│\x1b[0m");
1373 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1374 } else {
1375 println!("\x1b[31m✗ Failed to archive project '{}'\x1b[0m", project_name);
1376 }
1377
1378 Ok(())
1379}
1380
1381async fn unarchive_project(project_name: String) -> Result<()> {
1382 let db_path = get_database_path()?;
1383 let db = Database::new(&db_path)?;
1384
1385 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1386 Some(p) => p,
1387 None => {
1388 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1389 return Ok(());
1390 }
1391 };
1392
1393 if !project.is_archived {
1394 println!("\x1b[33m⚠ Project '{}' is not archived\x1b[0m", project_name);
1395 return Ok(());
1396 }
1397
1398 let success = ProjectQueries::unarchive_project(&db.connection, project.id.unwrap())?;
1399
1400 if success {
1401 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1402 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Unarchived\x1b[0m \x1b[36m│\x1b[0m");
1403 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1404 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1405 println!("\x1b[36m│\x1b[0m Status: \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m");
1406 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1407 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project unarchived successfully\x1b[0m \x1b[36m│\x1b[0m");
1408 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1409 } else {
1410 println!("\x1b[31m✗ Failed to unarchive project '{}'\x1b[0m", project_name);
1411 }
1412
1413 Ok(())
1414}
1415
1416async fn update_project_path(project_name: String, new_path: PathBuf) -> Result<()> {
1417 let db_path = get_database_path()?;
1418 let db = Database::new(&db_path)?;
1419
1420 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1421 Some(p) => p,
1422 None => {
1423 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1424 return Ok(());
1425 }
1426 };
1427
1428 let canonical_path = canonicalize_path(&new_path)?;
1429 let success = ProjectQueries::update_project_path(&db.connection, project.id.unwrap(), &canonical_path)?;
1430
1431 if success {
1432 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1433 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Path Updated\x1b[0m \x1b[36m│\x1b[0m");
1434 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1435 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1436 println!("\x1b[36m│\x1b[0m Old Path: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project.path.to_string_lossy(), 27));
1437 println!("\x1b[36m│\x1b[0m New Path: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&canonical_path.to_string_lossy(), 27));
1438 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1439 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Path updated successfully\x1b[0m \x1b[36m│\x1b[0m");
1440 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1441 } else {
1442 println!("\x1b[31m✗ Failed to update path for project '{}'\x1b[0m", project_name);
1443 }
1444
1445 Ok(())
1446}
1447
1448async fn add_tag_to_project(project_name: String, tag_name: String) -> Result<()> {
1449 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1450 println!("Would add tag '{}' to project '{}'", tag_name, project_name);
1451 println!("This requires implementing project_tags table operations.");
1452 Ok(())
1453}
1454
1455async fn remove_tag_from_project(project_name: String, tag_name: String) -> Result<()> {
1456 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1457 println!("Would remove tag '{}' from project '{}'", tag_name, project_name);
1458 println!("This requires implementing project_tags table operations.");
1459 Ok(())
1460}
1461
1462async fn bulk_update_sessions_project(session_ids: Vec<i64>, new_project_name: String) -> Result<()> {
1464 let db_path = get_database_path()?;
1465 let db = Database::new(&db_path)?;
1466
1467 let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1469 Some(p) => p,
1470 None => {
1471 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1472 return Ok(());
1473 }
1474 };
1475
1476 let updated = SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1477
1478 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1479 println!("\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Update\x1b[0m \x1b[36m│\x1b[0m");
1480 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1481 println!("\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", updated);
1482 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&new_project_name, 27));
1483 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1484 println!("\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m", updated, "");
1485 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1486
1487 Ok(())
1488}
1489
1490async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1491 let db_path = get_database_path()?;
1492 let db = Database::new(&db_path)?;
1493
1494 let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1495
1496 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1497 println!("\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Delete\x1b[0m \x1b[36m│\x1b[0m");
1498 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1499 println!("\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", session_ids.len());
1500 println!("\x1b[36m│\x1b[0m Deleted: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", deleted);
1501 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1502 println!("\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m", deleted, "");
1503 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1504
1505 Ok(())
1506}
1507
1508async fn launch_dashboard() -> Result<()> {
1509 enable_raw_mode()?;
1511 let mut stdout = io::stdout();
1512 execute!(stdout, EnterAlternateScreen)?;
1513 let backend = CrosstermBackend::new(stdout);
1514 let mut terminal = Terminal::new(backend)?;
1515
1516 let result = async {
1518 let mut dashboard = Dashboard::new().await?;
1519 dashboard.run(&mut terminal).await
1520 };
1521
1522 let result = tokio::task::block_in_place(|| {
1523 Handle::current().block_on(result)
1524 });
1525
1526 disable_raw_mode()?;
1528 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1529 terminal.show_cursor()?;
1530
1531 result
1532}
1533
1534async fn launch_timer() -> Result<()> {
1535 enable_raw_mode()?;
1537 let mut stdout = io::stdout();
1538 execute!(stdout, EnterAlternateScreen)?;
1539 let backend = CrosstermBackend::new(stdout);
1540 let mut terminal = Terminal::new(backend)?;
1541
1542 let result = async {
1544 let mut timer = InteractiveTimer::new().await?;
1545 timer.run(&mut terminal).await
1546 };
1547
1548 let result = tokio::task::block_in_place(|| {
1549 Handle::current().block_on(result)
1550 });
1551
1552 disable_raw_mode()?;
1554 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1555 terminal.show_cursor()?;
1556
1557 result
1558}
1559
1560async fn merge_sessions(session_ids_str: String, project_name: Option<String>, notes: Option<String>) -> Result<()> {
1561 let session_ids: Result<Vec<i64>, _> = session_ids_str
1563 .split(',')
1564 .map(|s| s.trim().parse::<i64>())
1565 .collect();
1566
1567 let session_ids = session_ids.map_err(|_| anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'"))?;
1568
1569 if session_ids.len() < 2 {
1570 return Err(anyhow::anyhow!("At least 2 sessions are required for merging"));
1571 }
1572
1573 let mut target_project_id = None;
1575 if let Some(project) = project_name {
1576 let db_path = get_database_path()?;
1577 let db = Database::new(&db_path)?;
1578
1579 if let Ok(project_id) = project.parse::<i64>() {
1581 if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1582 target_project_id = Some(project_id);
1583 }
1584 } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1585 target_project_id = proj.id;
1586 }
1587
1588 if target_project_id.is_none() {
1589 return Err(anyhow::anyhow!("Project '{}' not found", project));
1590 }
1591 }
1592
1593 let db_path = get_database_path()?;
1595 let db = Database::new(&db_path)?;
1596
1597 let merged_id = SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1598
1599 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1600 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Merge Complete\x1b[0m \x1b[36m│\x1b[0m");
1601 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1602 println!("\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m", session_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", "));
1603 println!("\x1b[36m│\x1b[0m New session ID: \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m", merged_id);
1604 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1605 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m \x1b[36m│\x1b[0m");
1606 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1607
1608 Ok(())
1609}
1610
1611async fn split_session(session_id: i64, split_times_str: String, notes: Option<String>) -> Result<()> {
1612 let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
1614 let mut split_times = Vec::new();
1615
1616 for time_str in split_time_strings {
1617 let datetime = if time_str.contains(':') {
1619 let today = chrono::Local::now().date_naive();
1621 let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
1622 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
1623 .map_err(|_| anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str))?;
1624 today.and_time(time).and_utc()
1625 } else {
1626 chrono::DateTime::parse_from_rfc3339(time_str)
1628 .map_err(|_| anyhow::anyhow!("Invalid datetime format '{}'. Use HH:MM or RFC3339 format", time_str))?
1629 .to_utc()
1630 };
1631
1632 split_times.push(datetime);
1633 }
1634
1635 if split_times.is_empty() {
1636 return Err(anyhow::anyhow!("No valid split times provided"));
1637 }
1638
1639 let notes_list = notes.map(|n| {
1641 n.split(',').map(|s| s.trim().to_string()).collect::<Vec<String>>()
1642 });
1643
1644 let db_path = get_database_path()?;
1646 let db = Database::new(&db_path)?;
1647
1648 let new_session_ids = SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
1649
1650 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1651 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Split Complete\x1b[0m \x1b[36m│\x1b[0m");
1652 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1653 println!("\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m", session_id);
1654 println!("\x1b[36m│\x1b[0m Split points: \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m", split_times.len());
1655 println!("\x1b[36m│\x1b[0m New sessions: \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m", new_session_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", "));
1656 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1657 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m \x1b[36m│\x1b[0m");
1658 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1659
1660 Ok(())
1661}
1662
1663async fn launch_history() -> Result<()> {
1664 enable_raw_mode()?;
1665 let mut stdout = io::stdout();
1666 execute!(stdout, EnterAlternateScreen)?;
1667 let backend = CrosstermBackend::new(stdout);
1668 let mut terminal = Terminal::new(backend)?;
1669
1670 let result = async {
1671 let mut browser = SessionHistoryBrowser::new().await?;
1672 browser.run(&mut terminal).await
1673 };
1674
1675 let result = tokio::task::block_in_place(|| {
1676 Handle::current().block_on(result)
1677 });
1678
1679 disable_raw_mode()?;
1680 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1681 terminal.show_cursor()?;
1682
1683 result
1684}
1685
1686async fn handle_goal_action(action: GoalAction) -> Result<()> {
1687 match action {
1688 GoalAction::Create { name, target_hours, project, description, start_date, end_date } => {
1689 create_goal(name, target_hours, project, description, start_date, end_date).await
1690 }
1691 GoalAction::List { project } => {
1692 list_goals(project).await
1693 }
1694 GoalAction::Update { id, hours } => {
1695 update_goal_progress(id, hours).await
1696 }
1697 }
1698}
1699
1700async fn create_goal(name: String, target_hours: f64, project: Option<String>, description: Option<String>, start_date: Option<String>, end_date: Option<String>) -> Result<()> {
1701 let db_path = get_database_path()?;
1702 let db = Database::new(&db_path)?;
1703
1704 let project_id = if let Some(proj_name) = project {
1705 match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
1706 Some(p) => p.id,
1707 None => {
1708 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
1709 return Ok(());
1710 }
1711 }
1712 } else {
1713 None
1714 };
1715
1716 let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1717 let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1718
1719 let mut goal = Goal::new(name.clone(), target_hours);
1720 if let Some(pid) = project_id {
1721 goal = goal.with_project(pid);
1722 }
1723 if let Some(desc) = description {
1724 goal = goal.with_description(desc);
1725 }
1726 goal = goal.with_dates(start, end);
1727
1728 goal.validate()?;
1729 let goal_id = GoalQueries::create(&db.connection, &goal)?;
1730
1731 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1732 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoal Created\x1b[0m \x1b[36m│\x1b[0m");
1733 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1734 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
1735 println!("\x1b[36m│\x1b[0m Target: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{} hours", target_hours));
1736 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", goal_id);
1737 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1738 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m \x1b[36m│\x1b[0m");
1739 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1740
1741 Ok(())
1742}
1743
1744async fn list_goals(project: Option<String>) -> Result<()> {
1745 let db_path = get_database_path()?;
1746 let db = Database::new(&db_path)?;
1747
1748 let project_id = if let Some(proj_name) = &project {
1749 match ProjectQueries::find_by_name(&db.connection, proj_name)? {
1750 Some(p) => p.id,
1751 None => {
1752 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
1753 return Ok(());
1754 }
1755 }
1756 } else {
1757 None
1758 };
1759
1760 let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
1761
1762 if goals.is_empty() {
1763 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1764 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Goals\x1b[0m \x1b[36m│\x1b[0m");
1765 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1766 return Ok(());
1767 }
1768
1769 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1770 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoals\x1b[0m \x1b[36m│\x1b[0m");
1771 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1772
1773 for goal in &goals {
1774 let progress_pct = goal.progress_percentage();
1775 println!("\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&goal.name, 25));
1776 println!("\x1b[36m│\x1b[0m Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h) \x1b[36m│\x1b[0m",
1777 progress_pct, goal.current_progress, goal.target_hours);
1778 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1779 }
1780
1781 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1782 Ok(())
1783}
1784
1785async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
1786 let db_path = get_database_path()?;
1787 let db = Database::new(&db_path)?;
1788
1789 GoalQueries::update_progress(&db.connection, id, hours)?;
1790 println!("\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m", id, hours);
1791 Ok(())
1792}
1793
1794async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
1795 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1796 println!("\x1b[36m│\x1b[0m \x1b[1;37mProductivity Insights\x1b[0m \x1b[36m│\x1b[0m");
1797 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1798 println!("\x1b[36m│\x1b[0m Period: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", period.as_deref().unwrap_or("all"));
1799 if let Some(proj) = project {
1800 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&proj, 27));
1801 }
1802 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1803 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Insights calculation in progress...\x1b[0m \x1b[36m│\x1b[0m");
1804 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1805 Ok(())
1806}
1807
1808async fn show_summary(period: String, from: Option<String>) -> Result<()> {
1809 let db_path = get_database_path()?;
1810 let db = Database::new(&db_path)?;
1811
1812 let start_date = if let Some(from_str) = from {
1813 chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
1814 } else {
1815 match period.as_str() {
1816 "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
1817 "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
1818 _ => chrono::Local::now().date_naive(),
1819 }
1820 };
1821
1822 let insight_data = match period.as_str() {
1823 "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
1824 "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
1825 _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
1826 };
1827
1828 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1829 println!("\x1b[36m│\x1b[0m \x1b[1;37m{} Summary\x1b[0m \x1b[36m│\x1b[0m", period);
1830 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1831 println!("\x1b[36m│\x1b[0m Total Hours: \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", insight_data.total_hours));
1832 println!("\x1b[36m│\x1b[0m Sessions: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", insight_data.sessions_count);
1833 println!("\x1b[36m│\x1b[0m Avg Session: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", insight_data.avg_session_duration));
1834 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1835 Ok(())
1836}
1837
1838async fn compare_projects(projects: String, _from: Option<String>, _to: Option<String>) -> Result<()> {
1839 let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
1840
1841 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1842 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Comparison\x1b[0m \x1b[36m│\x1b[0m");
1843 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1844 println!("\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&projects, 27));
1845 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Comparison feature in development\x1b[0m \x1b[36m│\x1b[0m");
1846 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1847 Ok(())
1848}
1849
1850async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
1851 match action {
1852 EstimateAction::Create { project, task, hours, due_date } => {
1853 create_estimate(project, task, hours, due_date).await
1854 }
1855 EstimateAction::Record { id, hours } => {
1856 record_actual_time(id, hours).await
1857 }
1858 EstimateAction::List { project } => {
1859 list_estimates(project).await
1860 }
1861 }
1862}
1863
1864async fn create_estimate(project: String, task: String, hours: f64, due_date: Option<String>) -> Result<()> {
1865 let db_path = get_database_path()?;
1866 let db = Database::new(&db_path)?;
1867
1868 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1869 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1870
1871 let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1872
1873 let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
1874 estimate.due_date = due;
1875
1876 let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
1877
1878 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1879 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimate Created\x1b[0m \x1b[36m│\x1b[0m");
1880 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1881 println!("\x1b[36m│\x1b[0m Task: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&task, 27));
1882 println!("\x1b[36m│\x1b[0m Estimate: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{} hours", hours));
1883 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", estimate_id);
1884 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1885 Ok(())
1886}
1887
1888async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
1889 let db_path = get_database_path()?;
1890 let db = Database::new(&db_path)?;
1891
1892 TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
1893 println!("\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m", hours, id);
1894 Ok(())
1895}
1896
1897async fn list_estimates(project: String) -> Result<()> {
1898 let db_path = get_database_path()?;
1899 let db = Database::new(&db_path)?;
1900
1901 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1902 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1903
1904 let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
1905
1906 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1907 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimates\x1b[0m \x1b[36m│\x1b[0m");
1908 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1909
1910 for est in &estimates {
1911 let variance = est.variance();
1912 let variance_str = if let Some(v) = variance {
1913 if v > 0.0 {
1914 format!("\x1b[31m+{:.1}h over\x1b[0m", v)
1915 } else {
1916 format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
1917 }
1918 } else {
1919 "N/A".to_string()
1920 };
1921
1922 println!("\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&est.task_name, 25));
1923 let actual_str = est.actual_hours.map(|h| format!("{:.1}h", h)).unwrap_or_else(|| "N/A".to_string());
1924 println!("\x1b[36m│\x1b[0m Est: {}h | Actual: {} | {} \x1b[36m│\x1b[0m",
1925 est.estimated_hours,
1926 actual_str,
1927 variance_str
1928 );
1929 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1930 }
1931
1932 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1933 Ok(())
1934}
1935
1936async fn handle_branch_action(action: BranchAction) -> Result<()> {
1937 match action {
1938 BranchAction::List { project } => {
1939 list_branches(project).await
1940 }
1941 BranchAction::Stats { project, branch } => {
1942 show_branch_stats(project, branch).await
1943 }
1944 }
1945}
1946
1947async fn list_branches(project: String) -> Result<()> {
1948 let db_path = get_database_path()?;
1949 let db = Database::new(&db_path)?;
1950
1951 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1952 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1953
1954 let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
1955
1956 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1957 println!("\x1b[36m│\x1b[0m \x1b[1;37mGit Branches\x1b[0m \x1b[36m│\x1b[0m");
1958 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1959
1960 for branch in &branches {
1961 println!("\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&branch.branch_name, 25));
1962 println!("\x1b[36m│\x1b[0m Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", branch.total_hours()));
1963 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1964 }
1965
1966 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1967 Ok(())
1968}
1969
1970async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
1971 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1972 println!("\x1b[36m│\x1b[0m \x1b[1;37mBranch Statistics\x1b[0m \x1b[36m│\x1b[0m");
1973 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1974 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project, 27));
1975 if let Some(b) = branch {
1976 println!("\x1b[36m│\x1b[0m Branch: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&b, 27));
1977 }
1978 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Branch stats in development\x1b[0m \x1b[36m│\x1b[0m");
1979 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1980 Ok(())
1981}
1982
1983async fn handle_template_action(action: TemplateAction) -> Result<()> {
1985 match action {
1986 TemplateAction::Create { name, description, tags, workspace_path } => {
1987 create_template(name, description, tags, workspace_path).await
1988 }
1989 TemplateAction::List => {
1990 list_templates().await
1991 }
1992 TemplateAction::Delete { template } => {
1993 delete_template(template).await
1994 }
1995 TemplateAction::Use { template, project_name, path } => {
1996 use_template(template, project_name, path).await
1997 }
1998 }
1999}
2000
2001async fn create_template(name: String, description: Option<String>, tags: Option<String>, workspace_path: Option<PathBuf>) -> Result<()> {
2002 let db_path = get_database_path()?;
2003 let db = Database::new(&db_path)?;
2004
2005 let default_tags = tags
2006 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2007 .unwrap_or_default();
2008
2009 let mut template = ProjectTemplate::new(name.clone())
2010 .with_tags(default_tags);
2011
2012 let desc_clone = description.clone();
2013 if let Some(desc) = description {
2014 template = template.with_description(desc);
2015 }
2016 if let Some(path) = workspace_path {
2017 template = template.with_workspace_path(path);
2018 }
2019
2020 let _template_id = TemplateQueries::create(&db.connection, &template)?;
2021
2022 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2023 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplate Created\x1b[0m \x1b[36m│\x1b[0m");
2024 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2025 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
2026 if let Some(desc) = &desc_clone {
2027 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2028 }
2029 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2030 Ok(())
2031}
2032
2033async fn list_templates() -> Result<()> {
2034 let db_path = get_database_path()?;
2035 let db = Database::new(&db_path)?;
2036
2037 let templates = TemplateQueries::list_all(&db.connection)?;
2038
2039 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2040 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplates\x1b[0m \x1b[36m│\x1b[0m");
2041 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2042
2043 if templates.is_empty() {
2044 println!("\x1b[36m│\x1b[0m No templates found. \x1b[36m│\x1b[0m");
2045 } else {
2046 for template in &templates {
2047 println!("\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&template.name, 25));
2048 if let Some(desc) = &template.description {
2049 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2050 }
2051 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2052 }
2053 }
2054
2055 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2056 Ok(())
2057}
2058
2059async fn delete_template(_template: String) -> Result<()> {
2060 println!("\x1b[33m⚠ Template deletion not yet implemented\x1b[0m");
2061 Ok(())
2062}
2063
2064async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2065 let db_path = get_database_path()?;
2066 let db = Database::new(&db_path)?;
2067
2068 let templates = TemplateQueries::list_all(&db.connection)?;
2069 let selected_template = templates.iter()
2070 .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2071 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2072
2073 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2075 let canonical_path = canonicalize_path(&project_path)?;
2076
2077 if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2079 return Err(anyhow::anyhow!("Project already exists at this path"));
2080 }
2081
2082 let git_hash = if is_git_repository(&canonical_path) {
2083 get_git_hash(&canonical_path)
2084 } else {
2085 None
2086 };
2087
2088 let template_desc = selected_template.description.clone();
2089 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2090 .with_git_hash(git_hash)
2091 .with_description(template_desc);
2092
2093 let project_id = ProjectQueries::create(&db.connection, &project)?;
2094 project.id = Some(project_id);
2095
2096 for goal_def in &selected_template.default_goals {
2101 let mut goal = Goal::new(goal_def.name.clone(), goal_def.target_hours)
2102 .with_project(project_id);
2103 if let Some(desc) = &goal_def.description {
2104 goal = goal.with_description(desc.clone());
2105 }
2106 GoalQueries::create(&db.connection, &goal)?;
2107 }
2108
2109 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2110 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Created from Template\x1b[0m \x1b[36m│\x1b[0m");
2111 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2112 println!("\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&selected_template.name, 27));
2113 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
2114 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2115 Ok(())
2116}
2117
2118async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2120 match action {
2121 WorkspaceAction::Create { name, description, path } => {
2122 create_workspace(name, description, path).await
2123 }
2124 WorkspaceAction::List => {
2125 list_workspaces().await
2126 }
2127 WorkspaceAction::AddProject { workspace, project } => {
2128 add_project_to_workspace(workspace, project).await
2129 }
2130 WorkspaceAction::RemoveProject { workspace, project } => {
2131 remove_project_from_workspace(workspace, project).await
2132 }
2133 WorkspaceAction::Projects { workspace } => {
2134 list_workspace_projects(workspace).await
2135 }
2136 WorkspaceAction::Delete { workspace } => {
2137 delete_workspace(workspace).await
2138 }
2139 }
2140}
2141
2142async fn create_workspace(name: String, description: Option<String>, path: Option<PathBuf>) -> Result<()> {
2143 let db_path = get_database_path()?;
2144 let db = Database::new(&db_path)?;
2145
2146 let mut workspace = Workspace::new(name.clone());
2147 let desc_clone = description.clone();
2148 if let Some(desc) = description {
2149 workspace = workspace.with_description(desc);
2150 }
2151 if let Some(p) = path {
2152 workspace = workspace.with_path(p);
2153 }
2154
2155 let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2156
2157 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2158 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Created\x1b[0m \x1b[36m│\x1b[0m");
2159 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2160 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
2161 if let Some(desc) = &desc_clone {
2162 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2163 }
2164 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2165 Ok(())
2166}
2167
2168async fn list_workspaces() -> Result<()> {
2169 let db_path = get_database_path()?;
2170 let db = Database::new(&db_path)?;
2171
2172 let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2173
2174 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2175 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspaces\x1b[0m \x1b[36m│\x1b[0m");
2176 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2177
2178 if workspaces.is_empty() {
2179 println!("\x1b[36m│\x1b[0m No workspaces found. \x1b[36m│\x1b[0m");
2180 } else {
2181 for workspace in &workspaces {
2182 println!("\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&workspace.name, 25));
2183 if let Some(desc) = &workspace.description {
2184 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2185 }
2186 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2187 }
2188 }
2189
2190 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2191 Ok(())
2192}
2193
2194async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2195 let db_path = get_database_path()?;
2196 let db = Database::new(&db_path)?;
2197
2198 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2200 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2201
2202 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2204 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2205
2206 let workspace_id = workspace_obj.id.unwrap();
2207 let project_id = project_obj.id.unwrap();
2208
2209 if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2210 println!("\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2211 } else {
2212 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2213 }
2214
2215 Ok(())
2216}
2217
2218async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2219 let db_path = get_database_path()?;
2220 let db = Database::new(&db_path)?;
2221
2222 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2224 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2225
2226 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2228 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2229
2230 let workspace_id = workspace_obj.id.unwrap();
2231 let project_id = project_obj.id.unwrap();
2232
2233 if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2234 println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2235 } else {
2236 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2237 }
2238
2239 Ok(())
2240}
2241
2242async fn list_workspace_projects(workspace: String) -> Result<()> {
2243 let db_path = get_database_path()?;
2244 let db = Database::new(&db_path)?;
2245
2246 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2248 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2249
2250 let workspace_id = workspace_obj.id.unwrap();
2251 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2252
2253 if projects.is_empty() {
2254 println!("\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'", workspace);
2255 return Ok(());
2256 }
2257
2258 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2259 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Projects\x1b[0m \x1b[36m│\x1b[0m");
2260 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2261 println!("\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&workspace, 25));
2262 println!("\x1b[36m│\x1b[0m Projects: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", format!("{} projects", projects.len()));
2263 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2264
2265 for project in &projects {
2266 let status_indicator = if !project.is_archived { "\x1b[32m●\x1b[0m" } else { "\x1b[31m○\x1b[0m" };
2267 println!("\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2268 status_indicator,
2269 truncate_string(&project.name, 33));
2270 }
2271
2272 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2273 Ok(())
2274}
2275
2276async fn delete_workspace(workspace: String) -> Result<()> {
2277 let db_path = get_database_path()?;
2278 let db = Database::new(&db_path)?;
2279
2280 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2282 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2283
2284 let workspace_id = workspace_obj.id.unwrap();
2285
2286 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2288 if !projects.is_empty() {
2289 println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.",
2290 workspace, projects.len());
2291 return Ok(());
2292 }
2293
2294 if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2295 println!("\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'", workspace);
2296 } else {
2297 println!("\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'", workspace);
2298 }
2299
2300 Ok(())
2301}
2302
2303async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2305 match action {
2306 CalendarAction::Add { name, start, end, event_type, project, description } => {
2307 add_calendar_event(name, start, end, event_type, project, description).await
2308 }
2309 CalendarAction::List { from, to, project } => {
2310 list_calendar_events(from, to, project).await
2311 }
2312 CalendarAction::Delete { id } => {
2313 delete_calendar_event(id).await
2314 }
2315 }
2316}
2317
2318async fn add_calendar_event(_name: String, _start: String, _end: Option<String>, _event_type: Option<String>, _project: Option<String>, _description: Option<String>) -> Result<()> {
2319 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2320 Ok(())
2321}
2322
2323async fn list_calendar_events(_from: Option<String>, _to: Option<String>, _project: Option<String>) -> Result<()> {
2324 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2325 Ok(())
2326}
2327
2328async fn delete_calendar_event(_id: i64) -> Result<()> {
2329 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2330 Ok(())
2331}
2332
2333async fn handle_issue_action(action: IssueAction) -> Result<()> {
2335 match action {
2336 IssueAction::Sync { project, tracker_type } => {
2337 sync_issues(project, tracker_type).await
2338 }
2339 IssueAction::List { project, status } => {
2340 list_issues(project, status).await
2341 }
2342 IssueAction::Link { session_id, issue_id } => {
2343 link_session_to_issue(session_id, issue_id).await
2344 }
2345 }
2346}
2347
2348async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2349 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2350 Ok(())
2351}
2352
2353async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2354 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2355 Ok(())
2356}
2357
2358async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2359 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2360 Ok(())
2361}
2362
2363async fn handle_client_action(action: ClientAction) -> Result<()> {
2365 match action {
2366 ClientAction::Generate { client, from, to, projects, format } => {
2367 generate_client_report(client, from, to, projects, format).await
2368 }
2369 ClientAction::List { client } => {
2370 list_client_reports(client).await
2371 }
2372 ClientAction::View { id } => {
2373 view_client_report(id).await
2374 }
2375 }
2376}
2377
2378async fn generate_client_report(_client: String, _from: String, _to: String, _projects: Option<String>, _format: Option<String>) -> Result<()> {
2379 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2380 Ok(())
2381}
2382
2383async fn list_client_reports(_client: Option<String>) -> Result<()> {
2384 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2385 Ok(())
2386}
2387
2388async fn view_client_report(_id: i64) -> Result<()> {
2389 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2390 Ok(())
2391}
2392
2393fn should_quit(event: crossterm::event::Event) -> bool {
2394 match event {
2395 crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
2396 matches!(key.code, crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc)
2397 }
2398 _ => false,
2399 }
2400}