using Microsoft.Data.Sqlite; using System.ComponentModel; using trakker.Data; using trakker.Forms; using trakker.Interfaces; using trakker.Models; using trakker.Services; using trakker.Utilities; namespace trakker { public partial class MainForm : Form, IMainForm { private const string _AppVersion = "[n.n.n]"; private static readonly string _empty = string.Empty; private string _ConnectionString = _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(); Text = $"Project Trakker - v{_AppVersion} / pragmattica.com"; tabControlMainForm.TabPages[0].Text = " Home "; tabControlMainForm.TabPages[1].Text = " Clients "; tabControlMainForm.TabPages[2].Text = " Projects "; _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 Client_DataGridViewClients_Init(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) => { Client_DataGridViewClients_DoubleClick(s!, e); }; dataGridViewClients.SelectionChanged += (s, e) => { Client_DataGridViewClients_SelectionChanged(s!, e); }; } public void Client_DataGridViewClients_DoubleClick(object sender, EventArgs 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); } } } } } public void Client_DataGridViewClients_SelectionChanged(object sender, EventArgs 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 Project_DataGridViewProjects_Fill(BindingSource projects) { int idx = 0; if (dataGridViewProjects.CurrentRow != null) { idx = dataGridViewProjects.CurrentRow.Index; } dataGridViewProjects.DataSource = projects; dataGridViewProjects.Rows[idx].Selected = true; } public void Project_DataGridViewProjects_Init() { 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 = "Actuals", Name = "Actuals", 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) => { Project_DataGridViewProjects_DoubleClick(s!, e); }; dataGridViewProjects.SelectionChanged += (s, e) => { Project_DataGridViewProjects_SelectionChanged(s!, e); }; } public void Project_DataGridViewProjects_DoubleClick(object sender, EventArgs 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; } } } public void Project_DataGridViewProjects_SelectionChanged(object sender, EventArgs e) { if (_ctrl == null) return; // Safety check to prevent null reference exceptions during initialization) if (dataGridViewProjects.SelectedRows.Count > 0) { var selectedProject = dataGridViewProjects.SelectedRows[0].DataBoundItem as Project; if (selectedProject != null) { // Handle the selected project as needed //MessageBox.Show($"Project ID: {selectedProject.ProjectId}, Selected Project: {selectedProject.ProjectName}", "Project Selected", MessageBoxButtons.OK, MessageBoxIcon.Information ); _ctrl.LoadTasks(selectedProject.ProjectId); // Load tasks for the selected project InitProjectTasks(); } } } public void InitProjectTasks() { //dataGridViewProjectTasks.Rows.Clear(); //richTextBoxTaskComments.Text = string.Empty; richTextBoxTaskComments.WordWrap = false; richTextBoxTaskComments.ReadOnly = true; } public void PTask_TreeViewTasks1_Init() { // Basic TreeView configuration treeViewTasks1.BeginUpdate(); treeViewTasks1.Nodes.Clear(); treeViewTasks1.ShowLines = true; treeViewTasks1.ShowRootLines = true; treeViewTasks1.HideSelection = false; treeViewTasks1.FullRowSelect = true; treeViewTasks1.EndUpdate(); // When a tree node is clicked, fetch and show notebooks for that folder treeViewTasks1.NodeMouseClick += (sender, e) => { PTask_TreeViewTasks1_NodeMouseClick(e.Node, e.Button); }; } public void PTask_TreeViewTasks1_NodeMouseClick(TreeNode node, MouseButtons button = MouseButtons.Left) { if (node == null) return; // Put all your NodeMouseClick logic here treeViewTasks1.SelectedNode = node; // Usually what you want node.EnsureVisible(); // ... your other code (open form, load data, etc.) PTaskFS? selectedNode = (PTaskFS?)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 PTask_TreeViewTasks1_Fill(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 = "/Project"; root.Parent = _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(); treeViewTasks1.EndUpdate(); // End the update TreeNode node = rootNode.FirstNode; PTask_TreeViewTasks1_NodeMouseClick(node, MouseButtons.Left); //treeViewTasks1.SelectedNode = node; //treeViewTasks1.Focus(); } private void PTask_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 = _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 PTask_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 PTask_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 ?? _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 PTask_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 Comment"); DialogResult result = textAreaForm.ShowDialog(this); if (result == DialogResult.OK) { if (string.IsNullOrEmpty(textAreaForm.BasicText)) return; TaskData taskData = new TaskData(_ConnectionString); try { PTaskComment comment = new() { TaskId = selectedTask?.GUID ?? _empty, Comment = textAreaForm.BasicText ?? _empty, }; taskData.SaveComment(comment); DialogExtensions.GenericSuccess($"Comment saved successfully for task '{selectedTask!.Node}'."); PTask_DataGridViewProjectTasks_SelectionChanged(null, null); } catch (Exception ex) { MessageBox.Show($"Error deleting task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } public void PTask_DataGridViewProjectTasks_Init() { 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) => { PTask_DataGridViewProjectTasks_SelectionChanged(s, e); }; dataGridViewProjectTasks.DoubleClick += (s, e) => { PTask_DataGridViewProjectTasks_DoubleClick(s, e); }; } public void PTask_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) { TaskData taskData = new TaskData(_ConnectionString); List comments = taskData.GetComments(selectedTask.TaskId ?? _empty); richTextBoxTaskComments.Clear(); foreach (var comment in comments) { richTextBoxTaskComments.AppendText($"{comment.CreatedAt}\n---\n{comment.Comment}\n\n"); } } } } public void PTask_DataGridViewProjectTasks_DoubleClick(object? sender, EventArgs? e) { var projectIdx = dataGridViewProjects.SelectedRows[0].Index; var taskIdx = dataGridViewProjectTasks.SelectedRows[0].Index; var row = dataGridViewProjectTasks.SelectedRows[0]; if (row != null) { var task = dataGridViewProjectTasks.SelectedRows[0].DataBoundItem as PTask; 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; try { TaskData taskData = new TaskData(_ConnectionString); taskData.Upsert(task); //_ctrl.LoadTasks(task.ProjectId ?? string.Empty); // Reload tasks to update the DataGridView with any changes _ctrl.LoadProjects(); // Reload projects to update the DataGridView with any changes _ctrl.LoadTasksRecursive(task.TaskId ?? _empty, PTask.RecursiveRoot.TASK_ID); // Reload tasks to update the tree view with any changes dataGridViewProjects.ClearSelection(); if (projectIdx >= 0 && projectIdx < dataGridViewProjects.Rows.Count) { dataGridViewProjects.Rows[projectIdx].Selected = true; } dataGridViewProjectTasks.ClearSelection(); if (taskIdx >= 0 && taskIdx < dataGridViewProjectTasks.Rows.Count) { dataGridViewProjectTasks.Rows[taskIdx].Selected = true; } } catch (Exception ex) { MessageBox.Show($"Error saving task: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } public void PTask_DataGridViewProjectTasks_Fill(BindingSource tasks) { //dataGridViewProjectTasks.Rows.Clear(); dataGridViewProjectTasks.DataSource = tasks; //dataGridViewProjectTasks.Refresh(); } private void PTask_RichTextBoxTaskComments_LinkClicked(object sender, LinkClickedEventArgs e) { try { // Open the URL in the default browser System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = e.LinkText, UseShellExecute = true }); } catch (Exception ex) { DialogExtensions.GenericError(String.Format($"Failed to open link: {ex.Message}")); } } } }