using Microsoft.Data.Sqlite; using newcle.us.Forms; using newcle.us.Utilities; using System.ComponentModel; using trakker.Data; using trakker.Forms; using trakker.Interfaces; using trakker.Models; using trakker.Services; namespace trakker { public partial class MainForm : Form, IMainForm { //private readonly string _dbversion = "[N.N.N]"; private string connectionString = string.Empty; readonly MainCtrl _ctrl; /// /// Initializes a new instance of the class. /// Sets up the form's controls and event handlers by calling /// which is generated by the designer. /// public MainForm() { InitializeComponent(); // build connection string that will be used for database connections // ------------------------------------------------------------------------ var dbPath = Path.Combine(AppContext.BaseDirectory, "trakker.db"); connectionString = new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = SqliteOpenMode.ReadWriteCreate, Cache = SqliteCacheMode.Shared }.ToString(); tabControlMainForm.TabPages[0].Text = " Home "; tabControlMainForm.TabPages[1].Text = " Clients "; tabControlMainForm.TabPages[2].Text = " Projects "; tabControlMainForm.TabPages[3].Text = " Tasks "; _ctrl = new Services.MainCtrl(this, connectionString); } /// /// Handles the Click event of the Exit menu item. When invoked, this /// method terminates the application. /// /// The source of the event. /// Event data associated with the click event. private void MainForm_Exit_MenuItem_Click(object sender, EventArgs e) { Application.Exit(); } public void InitDataGridViewClients(BindingList clients) { dataGridViewClients.AllowUserToAddRows = true; dataGridViewClients.AllowUserToDeleteRows = true; dataGridViewClients.AutoGenerateColumns = false; dataGridViewClients.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; dataGridViewClients.BackgroundColor = Color.White; dataGridViewClients.ColumnHeadersVisible = true; dataGridViewClients.DataSource = clients; dataGridViewClients.MultiSelect = false; dataGridViewClients.RowHeadersVisible = false; dataGridViewClients.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dataGridViewClients.Columns.Clear(); { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "Name", Name = "Name", Visible = true, }; dataGridViewClients.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "Company", Name = "Company", Visible = true, }; dataGridViewClients.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "Email", Name = "Email", Visible = true, }; dataGridViewClients.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "Phone", Name = "Phone", Visible = true, }; dataGridViewClients.Columns.Add(textColumn); } dataGridViewClients.DoubleClick += (s, e) => { if (dataGridViewClients.SelectedRows.Count > 0) { var selectedClient = dataGridViewClients.SelectedRows[0].DataBoundItem as Client; if (selectedClient != null) { var dialog = new ClientForm(selectedClient, _ctrl.GetLOV("state")); if (dialog.ShowDialog(this) == DialogResult.OK) { Client client = dialog.Client; ClientData clientData = new ClientData(connectionString); try { clientData.Upsert(client); dataGridViewClients.Refresh(); // Refresh the DataGridView to reflect changes } catch (Exception ex) { MessageBox.Show($"Error saving client: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } }; dataGridViewClients.SelectionChanged += (s, e) => { if (dataGridViewClients.SelectedRows.Count > 0) { var selectedClient = dataGridViewClients.SelectedRows[0].DataBoundItem as Client; if (selectedClient != null) { // Handle the selected client as needed // MessageBox.Show($"Selected Client: {selectedClient.AddressStreet}", "Client Selected", MessageBoxButtons.OK, MessageBoxIcon.Information ); } } }; } public void FillDataGridViewProjects(BindingSource projects) { dataGridViewProjects.DataSource = projects; } public void InitDataGridViewProjects() { dataGridViewProjects.AllowUserToAddRows = true; dataGridViewProjects.AllowUserToDeleteRows = true; dataGridViewProjects.AutoGenerateColumns = false; dataGridViewProjects.BackgroundColor = Color.White; dataGridViewProjects.ColumnHeadersVisible = true; dataGridViewProjects.MultiSelect = false; dataGridViewProjects.RowHeadersVisible = false; dataGridViewProjects.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dataGridViewProjects.Columns.Clear(); { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.None, DataPropertyName = "ProjectName", Name = "Project Name", Visible = true, Width = 350, }; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "ClientName", Name = "Client Name", Visible = true, }; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "StartDate", DefaultCellStyle = { Format = "MM/dd/yyyy" }, Name = "Start Date", Visible = true, }; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "EndDate", DefaultCellStyle = { Format = "MM/dd/yyyy" }, Name = "End Date", Visible = true, }; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "HourlyRate", Name = "Hourly Rate", Visible = true, }; textColumn.DefaultCellStyle.Format = "$#,##0.00"; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "Budget", Name = "Budget", Visible = true, }; textColumn.DefaultCellStyle.Format = "$#,##0.00"; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjects.Columns.Add(textColumn); } { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "StatusName", Name = "Status", Visible = true, }; dataGridViewProjects.Columns.Add(textColumn); } dataGridViewProjects.DoubleClick += (s, e) => { if (dataGridViewProjects.SelectedRows.Count > 0) { var selectedIdx = dataGridViewProjects.SelectedRows[0].Index; var selectedProject = dataGridViewProjects.SelectedRows[0].DataBoundItem as Project; if (selectedProject != null) { var dialog = new ProjectForm(selectedProject, _ctrl.GetClients(), _ctrl.GetLOV("project.status")); if (dialog.ShowDialog(this) == DialogResult.OK) { Project project = dialog.Project; ProjectData projectData = new ProjectData(connectionString); try { projectData.Upsert(project); _ctrl.LoadProjects(); // Reload projects to update the DataGridView with any changes dataGridViewProjects.ClearSelection(); if (selectedIdx >= 0 && selectedIdx < dataGridViewProjects.Rows.Count) { dataGridViewProjects.Rows[selectedIdx].Selected = true; } } catch (Exception ex) { MessageBox.Show($"Error saving project: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } return; } } }; dataGridViewClients.SelectionChanged += (s, e) => { if (dataGridViewClients.SelectedRows.Count > 0) { var selectedClient = dataGridViewClients.SelectedRows[0].DataBoundItem as Client; if (selectedClient != null) { // Handle the selected client as needed // MessageBox.Show($"Selected Client: {selectedClient.AddressStreet}", "Client Selected", MessageBoxButtons.OK, MessageBoxIcon.Information ); } } }; } public void InitTreeViewTasks() { // Basic TreeView configuration treeViewTasks1.BeginUpdate(); treeViewTasks1.Nodes.Clear(); treeViewTasks1.ShowLines = true; treeViewTasks1.ShowRootLines = true; treeViewTasks1.HideSelection = false; treeViewTasks1.EndUpdate(); // When a tree node is clicked, fetch and show notebooks for that folder treeViewTasks1.NodeMouseClick += (sender, e) => { PTaskFS? selectedNode = (PTaskFS?)e.Node.Tag ?? new PTaskFS(); if (selectedNode?.Parent == "/" || selectedNode?.GUID == "/") { _ctrl.LoadTasksRecursive(selectedNode.ProjectId, PTask.RecursiveRoot.PARENT_TASK_ID); // Load all tasks for the project when root node is clicked return; } _ctrl.LoadTasksRecursive(selectedNode?.GUID ?? "/", PTask.RecursiveRoot.TASK_ID); // Load all tasks for the project when root node is clicked }; } public void FillTreeViewTasks(List items) { if (items == null) return; treeViewTasks1.BeginUpdate(); // Improves performance for large trees treeViewTasks1.Nodes.Clear(); // Root node PTaskFS root = new(); root.GUID = "/"; root.Node = "/Projects"; root.Parent = string.Empty; TreeNode rootNode = new TreeNode(root.Node) { Tag = root }; treeViewTasks1.Nodes.Add(rootNode); // Normalize keys so null/empty parents map to root key "/" static string NormalizeKey(string? k) => string.IsNullOrWhiteSpace(k) ? "/" : k!.Trim(); // Build lookup: parentKey -> list of children var lookup = new Dictionary>(); foreach (var it in items) { var key = NormalizeKey(it.Parent); if (!lookup.TryGetValue(key, out var list)) { list = new List(); lookup[key] = list; } list.Add(it); } var keys = lookup.Keys.ToList(); // Recursive adder: attach child nodes to a parent TreeNode. // Uses normalized keys so null/empty parents become "/". void AddChildren(TreeNode parentNode, string parentKey) { if (!lookup.TryGetValue(parentKey, out var children)) return; // Optional: sort children by Node text for deterministic order foreach (var child in children.OrderBy(c => c.Node)) { var text = string.IsNullOrWhiteSpace(child.Node) ? "(unnamed)" : child.Node!.Trim(); var tn = new TreeNode(text) { Tag = child }; //tn.Expand(); parentNode.Nodes.Add(tn); // Recurse using this child's node text as the parent key AddChildren(tn, NormalizeKey(child.GUID)); } } // // Start recursion from root key AddChildren(rootNode, "/"); //rootNode.ExpandAll(); rootNode.Expand(); // Expand first level for better UX treeViewTasks1.EndUpdate(); // End the update } private void addTaskSubtaskToolStripMenuItem_Click(object sender, EventArgs e) { if (treeViewTasks1.SelectedNode == null) return; PTaskFS? selectedNode = (PTaskFS?)treeViewTasks1.SelectedNode.Tag ?? new PTaskFS(); if (selectedNode?.Parent == "") { MessageBox.Show("Cannot add tasks to root node", "Add Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string taskId = Guid.NewGuid().ToString(); PTask task = new() { TaskId = taskId, Title = "New Task", Description = string.Empty, ParentTaskId = selectedNode?.GUID == "/" ? null : selectedNode?.GUID, HourlyRate = selectedNode?.HourlyRate, ProjectId = selectedNode?.ProjectId, // Root node has no project }; TaskForm dialog = new TaskForm(task, _ctrl.GetLOV("task.status"), _ctrl.GetLOV("task.priority")); DialogResult result = dialog.ShowDialog(this); if (result == DialogResult.OK) { task = dialog.Task; TaskData taskData = new TaskData(connectionString); try { taskData.Upsert(task); treeViewTasks1.SelectedNode.Nodes.Add(new TreeNode(task.Title ?? "") { Tag = new PTaskFS { GUID = task.TaskId ?? "", Node = task.Title ?? "", Parent = task.ParentTaskId ?? "", HourlyRate = task.HourlyRate, ProjectId = task.ProjectId ?? "" } }); //_ctrl.LoadTasks(); } catch (Exception ex) { MessageBox.Show($"Error saving task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void editThisTaskSubtaskToolStripMenuItem_Click(object sender, EventArgs e) { if (treeViewTasks1.SelectedNode == null) return; PTaskFS? selectedTask = treeViewTasks1.SelectedNode.Tag as PTaskFS; if (selectedTask?.Parent == "/" || selectedTask?.GUID == "/") { MessageBox.Show("Cannot edit root node", "Edit Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } TaskData taskData = new TaskData(connectionString); PTask task = taskData.Get(selectedTask?.GUID); TaskForm dialog = new TaskForm(task, _ctrl.GetLOV("task.status"), _ctrl.GetLOV("task.priority")); DialogResult result = dialog.ShowDialog(this); if (result == DialogResult.OK) { task = dialog.Task; //MessageBox.Show(task.ToString(), "Task Details", MessageBoxButtons.OK, MessageBoxIcon.Information); try { taskData.Upsert(task); treeViewTasks1.SelectedNode.Text = task.Title ?? ""; // Update the node text in the tree view _ctrl.LoadTasksRecursive(selectedTask!.GUID, PTask.RecursiveRoot.TASK_ID); // Reload tasks to update the tree view with any changes } catch (Exception ex) { MessageBox.Show($"Error saving task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void deleteThisTaskSubtaskToolStripMenuItem_Click(object sender, EventArgs e) { if (treeViewTasks1.SelectedNode == null) return; PTaskFS? selectedTask = treeViewTasks1.SelectedNode.Tag as PTaskFS; //MessageBox.Show(selectedTask != null ? selectedTask.ToString() : "No task selected", "Edit Task", MessageBoxButtons.OK, MessageBoxIcon.Information); if (selectedTask?.Parent == "/" || selectedTask?.GUID == "/") { MessageBox.Show("Cannot delete root node", "Delete Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (treeViewTasks1.SelectedNode.Nodes.Count > 0) { MessageBox.Show("Cannot delete a task with subtasks", "Delete Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } TaskData taskData = new TaskData(connectionString); DialogResult result = MessageBox.Show("Are you sure you want to delete this task?", "Delete Task", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result == DialogResult.Yes) { //MessageBox.Show(task.ToString(), "Task Details", MessageBoxButtons.OK, MessageBoxIcon.Information); try { taskData.Delete(selectedTask?.GUID ?? string.Empty); treeViewTasks1.SelectedNode.Remove(); //_ctrl.LoadTasks(); // Reload tasks to update the DataGridView with any changes } catch (Exception ex) { MessageBox.Show($"Error deleting task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void addACommentToolStripMenuItem_Click(object sender, EventArgs e) { if (treeViewTasks1.SelectedNode == null) return; PTaskFS? selectedTask = treeViewTasks1.SelectedNode.Tag as PTaskFS; //MessageBox.Show(selectedTask != null ? selectedTask.ToString() : "No task selected", "Edit Task", MessageBoxButtons.OK, MessageBoxIcon.Information); if (selectedTask?.Parent == "/" || selectedTask?.GUID == "/") { MessageBox.Show("Cannot comment on root node", "Add Comment", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } TextAreaForm textAreaForm = new TextAreaForm("Add / Edit Comment"); DialogResult result = textAreaForm.ShowDialog(this); if (result == DialogResult.OK) { if (string.IsNullOrEmpty(textAreaForm.Content)) return; TaskData taskData = new TaskData(connectionString); try { PTaskComment comment = new() { TaskId = selectedTask?.GUID ?? string.Empty, Comment = textAreaForm.Content ?? string.Empty, }; taskData.SaveComment(comment); DialogExtensions.GenericSuccess($"Comment saved successfully for task '{selectedTask!.Node}'."); dataGridViewProjectTasks_SelectionChanged(null, null); } catch (Exception ex) { MessageBox.Show($"Error deleting task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } public void InitDataGridViewProjectTasks() { dataGridViewProjectTasks.AllowUserToAddRows = false; dataGridViewProjectTasks.AllowUserToDeleteRows = false; dataGridViewProjectTasks.AutoGenerateColumns = false; dataGridViewProjectTasks.BackgroundColor = Color.White; dataGridViewProjectTasks.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft; dataGridViewProjectTasks.ColumnHeadersDefaultCellStyle.WrapMode = DataGridViewTriState.False; dataGridViewProjectTasks.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; dataGridViewProjectTasks.ColumnHeadersVisible = true; dataGridViewProjectTasks.MultiSelect = false; dataGridViewProjectTasks.ReadOnly = true; dataGridViewProjectTasks.RowHeadersVisible = false; dataGridViewProjectTasks.SelectionMode = DataGridViewSelectionMode.FullRowSelect; // Then add your columns... dataGridViewProjectTasks.Columns.Clear(); { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.None, DataPropertyName = "Title", Name = "Title", Visible = true, Width = 350, }; dataGridViewProjectTasks.Columns.Add(textColumn); } // 1. Status - Most important for quick visual scanning { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "StatusName", Name = "Status", Visible = true, }; dataGridViewProjectTasks.Columns.Add(textColumn); } // 2. Priority - Right after status { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "PriorityName", Name = "Priority", Visible = true, }; dataGridViewProjectTasks.Columns.Add(textColumn); } // 3. Due Date - Time sensitivity { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "DueDate", DefaultCellStyle = { Format = "MM/dd/yyyy" }, Name = "Due Date", Visible = true, }; dataGridViewProjectTasks.Columns.Add(textColumn); } // 4. Estimated Hours { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "EstimatedHours", Name = "Est Hours", Visible = true, }; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjectTasks.Columns.Add(textColumn); } // 5. Actual Hours { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "ActualHours", Name = "Act Hours", Visible = true, }; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjectTasks.Columns.Add(textColumn); } // 6. Hourly Rate - Financial info at the end { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, DataPropertyName = "HourlyRate", Name = "Hourly Rate", Visible = true, }; textColumn.DefaultCellStyle.Format = "$#,##0.00"; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjectTasks.Columns.Add(textColumn); } // 7. Amount - Financial info at the end { var textColumn = new DataGridViewTextBoxColumn { AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, Name = "Amount", DataPropertyName = "Amount", Visible = true, }; textColumn.DefaultCellStyle.Format = "$#,##0.00"; textColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight; textColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight; dataGridViewProjectTasks.Columns.Add(textColumn); } dataGridViewProjectTasks.SelectionChanged += (s, e) => { dataGridViewProjectTasks_SelectionChanged(s, e); }; } public void dataGridViewProjectTasks_SelectionChanged(object? sender, EventArgs? e) { if (dataGridViewProjectTasks.SelectedRows.Count > 0) { var selectedIdx = dataGridViewProjectTasks.SelectedRows[0].Index; var selectedTask = dataGridViewProjectTasks.SelectedRows[0].DataBoundItem as PTask; if (selectedTask != null) { richTextBoxTaskDescription.Text = selectedTask.Description; TaskData taskData = new TaskData(connectionString); List comments = taskData.GetComments(selectedTask.TaskId ?? string.Empty); richTextBoxTaskComments.Clear(); foreach (var comment in comments) { richTextBoxTaskComments.AppendText($"{comment.CreatedAt}\n---\n{comment.Comment}\n\n"); } } } } public void FillDataGridViewProjectTasks(BindingSource tasks) { dataGridViewProjectTasks.DataSource = tasks; dataGridViewProjectTasks.Refresh(); } } }