diff --git a/Data/TaskData.cs b/Data/TaskData.cs index d452eb3..eb78bd2 100644 --- a/Data/TaskData.cs +++ b/Data/TaskData.cs @@ -11,6 +11,123 @@ namespace trakker.Data /// internal class TaskData(string connectionString) : DataAccess(connectionString) { + public BindingList GetRecursive(string? id = null, trakker.Models.Task.RecursiveRoot root = trakker.Models.Task.RecursiveRoot.TASK_ID) + { + var results = new BindingList(); + + string whereClause = "1 = 1"; + switch(root) + { + case trakker.Models.Task.RecursiveRoot.TASK_ID: + if (id != null) + { + whereClause = "task_id = $id"; + } + break; + case trakker.Models.Task.RecursiveRoot.PARENT_TASK_ID: + if (id != null) + { + whereClause = "parent_task_id = $id"; + } + break; + } + + string sql = $@" + WITH RECURSIVE TaskHierarchy AS ( + -- Anchor: starting task(s) + SELECT + task_id, + project_id, + title, + description, + status, + priority, + due_date, + estimated_hours, + actual_hours, + hourly_rate, + parent_task_id, + created_at, + updated_at, + 0 AS level, + title AS path + FROM tasks + WHERE + {whereClause} + UNION ALL + + -- Recursive part: get children + SELECT + t.task_id, + t.project_id, + t.title, + t.description, + t.status, + t.priority, + t.due_date, + t.estimated_hours, + t.actual_hours, + t.hourly_rate, + t.parent_task_id, + t.created_at, + t.updated_at, + th.level + 1, + th.path || ' > ' || t.title + FROM tasks t + JOIN TaskHierarchy th ON t.parent_task_id = th.task_id + ) + SELECT * + FROM TaskHierarchy + ORDER BY path + ; + "; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + if (id != null) + { + cmd.Parameters.AddWithValue("$id", id); + } + using var reader = cmd.ExecuteReader(); + + var _var1 = reader.GetOrdinal("task_id"); + var _var2 = reader.GetOrdinal("project_id"); + var _var3 = reader.GetOrdinal("title"); + var _var4 = reader.GetOrdinal("description"); + var _var5 = reader.GetOrdinal("status"); + var _var6 = reader.GetOrdinal("priority"); + var _var7 = reader.GetOrdinal("due_date"); + var _var8 = reader.GetOrdinal("estimated_hours"); + var _var9 = reader.GetOrdinal("actual_hours"); + var _var10 = reader.GetOrdinal("hourly_rate"); + var _var11 = reader.GetOrdinal("parent_task_id"); + var _var12 = reader.GetOrdinal("created_at"); + var _var13 = reader.GetOrdinal("updated_at"); + + while (reader.Read()) + { + results.Add(new trakker.Models.Task + { + TaskId = reader.GetString(_var1), + ProjectId = reader.GetString(_var2), + Title = reader.GetString(_var3), + Description = reader.GetString(_var4), + Status = reader.GetString(_var5), + Priority = reader.GetString(_var6), + DueDate = reader.IsDBNull(_var7) ? null : reader.GetDateTime(_var7), + EstimatedHours = reader.IsDBNull(_var8) ? null : reader.GetDouble(_var8), + ActualHours = reader.IsDBNull(_var9) ? null : reader.GetDouble(_var9), + HourlyRate = reader.IsDBNull(_var10) ? null : reader.GetDouble(_var10), + ParentTaskId = reader.IsDBNull(_var11) ? null : reader.GetString(_var11), + CreatedAt = reader.IsDBNull(_var12) ? null : reader.GetDateTime(_var12), + UpdatedAt = reader.IsDBNull(_var13) ? null : reader.GetDateTime(_var13), + }); + } + + return results; + } public trakker.Models.Task Get(string? taskId = null) { diff --git a/Forms/MainForm.Designer.cs b/Forms/MainForm.Designer.cs index f824361..4cbfbff 100644 --- a/Forms/MainForm.Designer.cs +++ b/Forms/MainForm.Designer.cs @@ -51,6 +51,8 @@ deleteThisTaskSubtaskToolStripMenuItem = new ToolStripMenuItem(); toolStripSeparator1 = new ToolStripSeparator(); addACommentToolStripMenuItem = new ToolStripMenuItem(); + splitContainerTasks2 = new SplitContainer(); + dataGridViewProjectTasks = new DataGridView(); MainForm_MenuStrip.SuspendLayout(); tabControlMainForm.SuspendLayout(); MainForm_TabPage2.SuspendLayout(); @@ -63,8 +65,13 @@ tableLayoutPanelTasks1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainerTasks1).BeginInit(); splitContainerTasks1.Panel1.SuspendLayout(); + splitContainerTasks1.Panel2.SuspendLayout(); splitContainerTasks1.SuspendLayout(); contextMenuStripTreeviewTasks.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)splitContainerTasks2).BeginInit(); + splitContainerTasks2.Panel1.SuspendLayout(); + splitContainerTasks2.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)dataGridViewProjectTasks).BeginInit(); SuspendLayout(); // // MainForm_MenuStrip @@ -234,6 +241,10 @@ // splitContainerTasks1.Panel1 // splitContainerTasks1.Panel1.Controls.Add(treeViewTasks1); + // + // splitContainerTasks1.Panel2 + // + splitContainerTasks1.Panel2.Controls.Add(splitContainerTasks2); splitContainerTasks1.Size = new Size(1856, 960); splitContainerTasks1.SplitterDistance = 618; splitContainerTasks1.TabIndex = 0; @@ -252,7 +263,7 @@ contextMenuStripTreeviewTasks.ImageScalingSize = new Size(32, 32); contextMenuStripTreeviewTasks.Items.AddRange(new ToolStripItem[] { addTaskSubtaskToolStripMenuItem, editThisTaskSubtaskToolStripMenuItem, deleteThisTaskSubtaskToolStripMenuItem, toolStripSeparator1, addACommentToolStripMenuItem }); contextMenuStripTreeviewTasks.Name = "contextMenuStripTreeviewTasks"; - contextMenuStripTreeviewTasks.Size = new Size(371, 206); + contextMenuStripTreeviewTasks.Size = new Size(371, 162); // // addTaskSubtaskToolStripMenuItem // @@ -287,6 +298,33 @@ addACommentToolStripMenuItem.Text = "Add a comment"; addACommentToolStripMenuItem.Click += addACommentToolStripMenuItem_Click; // + // splitContainerTasks2 + // + splitContainerTasks2.Dock = DockStyle.Fill; + splitContainerTasks2.Location = new Point(0, 0); + splitContainerTasks2.Name = "splitContainerTasks2"; + splitContainerTasks2.Orientation = Orientation.Horizontal; + // + // splitContainerTasks2.Panel1 + // + splitContainerTasks2.Panel1.Controls.Add(dataGridViewProjectTasks); + splitContainerTasks2.Size = new Size(1234, 960); + splitContainerTasks2.SplitterDistance = 411; + splitContainerTasks2.TabIndex = 0; + // + // dataGridViewProjectTasks + // + dataGridViewProjectTasks.AllowUserToAddRows = false; + dataGridViewProjectTasks.AllowUserToDeleteRows = false; + dataGridViewProjectTasks.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + dataGridViewProjectTasks.Dock = DockStyle.Fill; + dataGridViewProjectTasks.Location = new Point(0, 0); + dataGridViewProjectTasks.Name = "dataGridViewProjectTasks"; + dataGridViewProjectTasks.ReadOnly = true; + dataGridViewProjectTasks.RowHeadersWidth = 82; + dataGridViewProjectTasks.Size = new Size(1234, 411); + dataGridViewProjectTasks.TabIndex = 0; + // // MainForm // AutoScaleDimensions = new SizeF(13F, 32F); @@ -310,9 +348,14 @@ MainForm_TabPage4.ResumeLayout(false); tableLayoutPanelTasks1.ResumeLayout(false); splitContainerTasks1.Panel1.ResumeLayout(false); + splitContainerTasks1.Panel2.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)splitContainerTasks1).EndInit(); splitContainerTasks1.ResumeLayout(false); contextMenuStripTreeviewTasks.ResumeLayout(false); + splitContainerTasks2.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)splitContainerTasks2).EndInit(); + splitContainerTasks2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)dataGridViewProjectTasks).EndInit(); ResumeLayout(false); PerformLayout(); } @@ -341,5 +384,7 @@ private ToolStripMenuItem deleteThisTaskSubtaskToolStripMenuItem; private ToolStripSeparator toolStripSeparator1; private ToolStripMenuItem addACommentToolStripMenuItem; + private SplitContainer splitContainerTasks2; + private DataGridView dataGridViewProjectTasks; } } diff --git a/Forms/MainForm.cs b/Forms/MainForm.cs index 3314afc..a808702 100644 --- a/Forms/MainForm.cs +++ b/Forms/MainForm.cs @@ -290,6 +290,30 @@ namespace trakker } }; + } + + 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) => + { + TaskFS? selectedNode = (TaskFS?)e.Node.Tag ?? new TaskFS(); + if (selectedNode?.Parent == "/" || selectedNode?.GUID == "/") + { + _ctrl.LoadTasksRecursive(selectedNode.ProjectId, trakker.Models.Task.RecursiveRoot.PARENT_TASK_ID); // Load all tasks for the project when root node is clicked + return; + } + _ctrl.LoadTasksRecursive(selectedNode?.GUID ?? "/", trakker.Models.Task.RecursiveRoot.TASK_ID); // Load all tasks for the project when root node is clicked + }; + } public void FillTreeViewTasks(List items) { @@ -454,24 +478,166 @@ namespace trakker { } + 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 = "Status", + Name = "Status", + Visible = true, + }; + dataGridViewProjectTasks.Columns.Add(textColumn); + } + + // 2. Priority - Right after status + { + var textColumn = new DataGridViewTextBoxColumn + { + AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, + DataPropertyName = "Priority", + 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); + } + + dataGridViewProjectTasks.Click += (s, e) => + { + if (dataGridViewProjectTasks.SelectedRows.Count > 0) + { + var selectedIdx = dataGridViewProjectTasks.SelectedRows[0].Index; + var selectedProject = dataGridViewProjectTasks.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 + // dataGridViewProjectTasks.ClearSelection(); + // if (selectedIdx >= 0 && selectedIdx < dataGridViewProjectTasks.Rows.Count) + // { + // dataGridViewProjectTasks.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 FillDataGridViewProjectTasks(BindingSource tasks) + { + dataGridViewProjectTasks.DataSource = tasks; + dataGridViewProjectTasks.Refresh(); + } - //private void button1_Click(object sender, EventArgs e) - //{ - // var dialog = new ClientForm(new Client()); - // if (dialog.ShowDialog(this) == DialogResult.OK) - // { - // Client client = dialog.Client; - // ClientData clientData = new ClientData(connectionString); - // try - // { - // clientData.Upsert(client); - // } - // catch (Exception ex) - // { - // MessageBox.Show($"Error saving client: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - // } - // } - //} } } diff --git a/Interfaces/IMainForm.cs b/Interfaces/IMainForm.cs index 6aef320..aee13c2 100644 --- a/Interfaces/IMainForm.cs +++ b/Interfaces/IMainForm.cs @@ -14,5 +14,8 @@ namespace trakker.Interfaces void InitDataGridViewProjects(); void FillDataGridViewProjects(BindingSource projects); void FillTreeViewTasks(List items); + void InitTreeViewTasks(); + void InitDataGridViewProjectTasks(); + void FillDataGridViewProjectTasks(BindingSource tasks); } } diff --git a/Models/Task.cs b/Models/Task.cs index d64837a..9dc8bec 100644 --- a/Models/Task.cs +++ b/Models/Task.cs @@ -1,7 +1,14 @@ namespace trakker.Models { + public class Task { + public enum RecursiveRoot + { + TASK_ID = 0, + PARENT_TASK_ID = 1 + } + public Task() { TaskId = Guid.NewGuid().ToString(); diff --git a/Services/MainCtrl.cs b/Services/MainCtrl.cs index 4595d8f..062f735 100644 --- a/Services/MainCtrl.cs +++ b/Services/MainCtrl.cs @@ -22,7 +22,10 @@ namespace trakker.Services LoadClients(); + _view.InitDataGridViewProjectTasks(); _view.InitDataGridViewProjects(); + _view.InitTreeViewTasks(); + LoadProjects(); } @@ -63,5 +66,13 @@ namespace trakker.Services var dbo = new TaskData(_connectionString); _view.FillTreeViewTasks(dbo.GetFS()); } + internal void LoadTasksRecursive(string id, trakker.Models.Task.RecursiveRoot root) + { + var dbo = new TaskData(_connectionString); + BindingList tasks = dbo.GetRecursive(id, root); + _view.FillDataGridViewProjectTasks(new BindingSource { DataSource = tasks }); + } + + } }