From 94e532986c21bc8bb671af3a46f775ec8b4a243a Mon Sep 17 00:00:00 2001 From: "c0d3.m0nk3y" Date: Mon, 11 May 2026 20:09:35 -0400 Subject: [PATCH] Continued development --- Data/TaskData.cs | 385 ++++++++++++++++++++++++++++++++++ Forms/MainForm.Designer.cs | 167 ++++++++++----- Forms/MainForm.cs | 173 +++++++++++++++- Forms/MainForm.resx | 3 + Forms/TaskForm.Designer.cs | 411 +++++++++++++++++++++++++++++++++++++ Forms/TaskForm.cs | 114 ++++++++++ Forms/TaskForm.resx | 120 +++++++++++ Interfaces/IMainForm.cs | 1 + Models/LOV.cs | 5 +- Models/Task.cs | 94 +++++++++ Services/MainCtrl.cs | 7 + 11 files changed, 1425 insertions(+), 55 deletions(-) create mode 100644 Data/TaskData.cs create mode 100644 Forms/TaskForm.Designer.cs create mode 100644 Forms/TaskForm.cs create mode 100644 Forms/TaskForm.resx create mode 100644 Models/Task.cs diff --git a/Data/TaskData.cs b/Data/TaskData.cs new file mode 100644 index 0000000..d452eb3 --- /dev/null +++ b/Data/TaskData.cs @@ -0,0 +1,385 @@ +using System.ComponentModel; +using trakker.Models; + +namespace trakker.Data +{ + /// + /// Provides data access methods for the entity. + /// This class encapsulates database operations such as upsert, delete and ad-hoc + /// SQL execution for tasks. It inherits from which + /// provides connection management. + /// + internal class TaskData(string connectionString) : DataAccess(connectionString) + { + + public trakker.Models.Task Get(string? taskId = null) + { + var results = new List(); + + string whereClause = "1 = 1"; + if (taskId != null) + { + whereClause = "task_id = $task_id"; + } + + string sql = $@" + SELECT + task_id, + project_id, + title, + description, + status, + priority, + due_date, + estimated_hours, + actual_hours, + hourly_rate, + parent_task_id, + created_at, + updated_at + FROM tasks + WHERE + {whereClause} + ORDER BY + priority DESC, + due_date ASC, + created_at DESC + ; + "; + + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + if (taskId != null) + { + cmd.Parameters.AddWithValue("$task_id", taskId); + } + 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.FirstOrDefault() ?? new trakker.Models.Task(); + } + + /// + /// Inserts a new task record or updates an existing one (upsert) using + /// the provided model. This method executes + /// a single SQL statement inside a transaction and will commit on + /// success or roll back on failure. + /// + /// The model to insert or update. Must not be null. + /// + /// The SQL statement uses an ON CONFLICT clause to perform the update when a + /// matching task_id already exists. Parameter names correspond to the + /// task model property names. + /// + public void Upsert(trakker.Models.Task task) + { + const string sql = @" + INSERT INTO tasks ( + task_id, + project_id, + title, + description, + status, + priority, + due_date, + estimated_hours, + actual_hours, + hourly_rate, + parent_task_id + ) + VALUES ( + $task_id, + $project_id, + $title, + $description, + $status, + $priority, + $due_date, + $estimated_hours, + $actual_hours, + $hourly_rate, + $parent_task_id + ) + ON CONFLICT(task_id) DO UPDATE SET + project_id = excluded.project_id, + title = excluded.title, + description = excluded.description, + status = excluded.status, + priority = excluded.priority, + due_date = excluded.due_date, + estimated_hours = excluded.estimated_hours, + actual_hours = excluded.actual_hours, + hourly_rate = excluded.hourly_rate, + parent_task_id = excluded.parent_task_id, + updated_at = CURRENT_TIMESTAMP + ; + "; + + using var conn = OpenConnection(); + using var tx = conn.BeginTransaction(); + + try + { + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$task_id", task.TaskId); + cmd.Parameters.AddWithValue("$project_id", task.ProjectId); + cmd.Parameters.AddWithValue("$title", task.Title); + cmd.Parameters.AddWithValue("$description", task.Description); + cmd.Parameters.AddWithValue("$status", task.Status); + cmd.Parameters.AddWithValue("$priority", task.Priority); + cmd.Parameters.AddWithValue("$due_date", task.DueDate); + cmd.Parameters.AddWithValue("$estimated_hours", task.EstimatedHours); + cmd.Parameters.AddWithValue("$actual_hours", task.ActualHours); + cmd.Parameters.AddWithValue("$hourly_rate", task.HourlyRate); + cmd.Parameters.AddWithValue("$parent_task_id", task.ParentTaskId); + cmd.ExecuteNonQuery(); + } + + tx.Commit(); + } + catch + { + tx.Rollback(); + throw; + } + + } + /// + /// Deletes the task with the specified from the + /// database. + /// + /// The identifier of the task to delete. + /// An optional integer representing any scalar value returned by the + /// command executed after deletion (if applicable). May be null. + /// + /// The method executes within a transaction. The current implementation attempts + /// to read a scalar value after the delete; that value depends on surrounding + /// database triggers or commands and may be null. + /// + public int? Delete(string taskId) + { + const string sql = @" + DELETE FROM + tasks + WHERE + task_id = $task_id + ; + "; + + using var conn = OpenConnection(); + using var tx = conn.BeginTransaction(); + + int? result = 0; + try + { + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$task_id", taskId); + cmd.ExecuteNonQuery(); + } + + using var idCmd = conn.CreateCommand(); + idCmd.Transaction = tx; + result = (int?)idCmd.ExecuteScalar() ; + + tx.Commit(); + } + catch + { + tx.Rollback(); + throw; + } + + return result; + } + public List GetFS() + { + var results = new List(); + + string sql = $@" + SELECT + a.project_id AS guid, + a.name AS node, + '/' AS parent, + a.hourly_rate, + a.project_id + FROM + projects a + + UNION + + SELECT + b.task_id AS guid, + b.title AS node, + b.parent_task_id AS parent, + b.hourly_rate, + b.project_id + FROM + tasks b + ; + "; + + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var _var1 = reader.GetOrdinal("guid"); + var _var2 = reader.GetOrdinal("node"); + var _var3 = reader.GetOrdinal("parent"); + var _var4 = reader.GetOrdinal("hourly_rate"); + var _var5 = reader.GetOrdinal("project_id"); + + while (reader.Read()) + { + results.Add(new TaskFS + { + GUID = reader.GetString(_var1), + Node = reader.GetString(_var2), + Parent = reader.GetString(_var3), + HourlyRate = reader.IsDBNull(_var4) ? (double?)null : reader.GetDouble(_var4), + ProjectId = reader.GetString(_var5) + }); + } + + return results; + } + public void SaveComment(TaskComment taskComment) + { + const string sql = @" + INSERT INTO task_comments ( + task_comment_id, + task_id, + comment + ) + VALUES ( + $task_comment_id, + $task_id, + $comment + ) + ON CONFLICT(task_comment_id) DO UPDATE SET + task_id = excluded.task_id, + comment = excluded.comment + ; + "; + + using var conn = OpenConnection(); + using var tx = conn.BeginTransaction(); + + try + { + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$task_comment_id", taskComment.TaskCommentId); + cmd.Parameters.AddWithValue("$task_id", taskComment.TaskId); + cmd.Parameters.AddWithValue("$comment", taskComment.Comment); + cmd.ExecuteNonQuery(); + } + + tx.Commit(); + } + catch + { + tx.Rollback(); + throw; + } + + } + public List GetComments(string taskId) + { + var results = new List(); + + string whereClause = "1 = 1"; + if (taskId != null) + { + whereClause = "task_id = $task_id"; + } + + string sql = $@" + SELECT + task_comment_id, + task_id, + comment, + created_at + FROM + task_comments + WHERE + {whereClause} + ORDER BY + created_at DESC + ; + "; + + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var _var1 = reader.GetOrdinal("task_comment_id"); + var _var2 = reader.GetOrdinal("task_id"); + var _var3 = reader.GetOrdinal("comment"); + var _var4 = reader.GetOrdinal("created_at"); + while (reader.Read()) + { + results.Add(new TaskComment + { + TaskCommentId = reader.GetString(_var1), + TaskId = reader.GetString(_var2), + Comment = reader.GetString(_var3), + CreatedAt = reader.IsDBNull(_var4) ? (DateTime?)null : reader.GetDateTime(_var4) + }); + } + + return results; + } + + + } +} diff --git a/Forms/MainForm.Designer.cs b/Forms/MainForm.Designer.cs index e7497bb..f824361 100644 --- a/Forms/MainForm.Designer.cs +++ b/Forms/MainForm.Designer.cs @@ -28,6 +28,7 @@ /// private void InitializeComponent() { + components = new System.ComponentModel.Container(); MainForm_MenuStrip = new MenuStrip(); fileToolStripMenuItem = new ToolStripMenuItem(); MainForm_Exit_MenuItem = new ToolStripMenuItem(); @@ -40,9 +41,16 @@ MainForm_TabPage3 = new TabPage(); tableLayoutPanelProjects1 = new TableLayoutPanel(); dataGridViewProjects = new DataGridView(); - tableLayoutPanelProjects2 = new TableLayoutPanel(); - groupBoxProjectTasks = new GroupBox(); - dataGridViewProjectTasks = new DataGridView(); + MainForm_TabPage4 = new TabPage(); + tableLayoutPanelTasks1 = new TableLayoutPanel(); + splitContainerTasks1 = new SplitContainer(); + treeViewTasks1 = new TreeView(); + contextMenuStripTreeviewTasks = new ContextMenuStrip(components); + addTaskSubtaskToolStripMenuItem = new ToolStripMenuItem(); + editThisTaskSubtaskToolStripMenuItem = new ToolStripMenuItem(); + deleteThisTaskSubtaskToolStripMenuItem = new ToolStripMenuItem(); + toolStripSeparator1 = new ToolStripSeparator(); + addACommentToolStripMenuItem = new ToolStripMenuItem(); MainForm_MenuStrip.SuspendLayout(); tabControlMainForm.SuspendLayout(); MainForm_TabPage2.SuspendLayout(); @@ -51,9 +59,12 @@ MainForm_TabPage3.SuspendLayout(); tableLayoutPanelProjects1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)dataGridViewProjects).BeginInit(); - tableLayoutPanelProjects2.SuspendLayout(); - groupBoxProjectTasks.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)dataGridViewProjectTasks).BeginInit(); + MainForm_TabPage4.SuspendLayout(); + tableLayoutPanelTasks1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)splitContainerTasks1).BeginInit(); + splitContainerTasks1.Panel1.SuspendLayout(); + splitContainerTasks1.SuspendLayout(); + contextMenuStripTreeviewTasks.SuspendLayout(); SuspendLayout(); // // MainForm_MenuStrip @@ -94,6 +105,7 @@ tabControlMainForm.Controls.Add(MainForm_TabPage1); tabControlMainForm.Controls.Add(MainForm_TabPage2); tabControlMainForm.Controls.Add(MainForm_TabPage3); + tabControlMainForm.Controls.Add(MainForm_TabPage4); tabControlMainForm.Dock = DockStyle.Fill; tabControlMainForm.Location = new Point(0, 40); tabControlMainForm.Name = "tabControlMainForm"; @@ -165,14 +177,13 @@ tableLayoutPanelProjects1.ColumnCount = 1; tableLayoutPanelProjects1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); tableLayoutPanelProjects1.Controls.Add(dataGridViewProjects, 0, 1); - tableLayoutPanelProjects1.Controls.Add(tableLayoutPanelProjects2, 0, 2); tableLayoutPanelProjects1.Dock = DockStyle.Fill; tableLayoutPanelProjects1.Location = new Point(0, 0); tableLayoutPanelProjects1.Name = "tableLayoutPanelProjects1"; tableLayoutPanelProjects1.RowCount = 3; tableLayoutPanelProjects1.RowStyles.Add(new RowStyle(SizeType.Absolute, 1F)); - tableLayoutPanelProjects1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); - tableLayoutPanelProjects1.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); + tableLayoutPanelProjects1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanelProjects1.RowStyles.Add(new RowStyle(SizeType.Percent, 0F)); tableLayoutPanelProjects1.Size = new Size(1862, 966); tableLayoutPanelProjects1.TabIndex = 1; // @@ -186,47 +197,95 @@ dataGridViewProjects.Name = "dataGridViewProjects"; dataGridViewProjects.ReadOnly = true; dataGridViewProjects.RowHeadersWidth = 82; - dataGridViewProjects.Size = new Size(1856, 476); + dataGridViewProjects.Size = new Size(1856, 959); dataGridViewProjects.TabIndex = 0; // - // tableLayoutPanelProjects2 + // MainForm_TabPage4 // - tableLayoutPanelProjects2.ColumnCount = 3; - tableLayoutPanelProjects2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 0F)); - tableLayoutPanelProjects2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); - tableLayoutPanelProjects2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 0F)); - tableLayoutPanelProjects2.Controls.Add(groupBoxProjectTasks, 1, 0); - tableLayoutPanelProjects2.Dock = DockStyle.Fill; - tableLayoutPanelProjects2.Location = new Point(3, 486); - tableLayoutPanelProjects2.Name = "tableLayoutPanelProjects2"; - tableLayoutPanelProjects2.RowCount = 1; - tableLayoutPanelProjects2.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); - tableLayoutPanelProjects2.Size = new Size(1856, 477); - tableLayoutPanelProjects2.TabIndex = 1; + MainForm_TabPage4.Controls.Add(tableLayoutPanelTasks1); + MainForm_TabPage4.Location = new Point(8, 46); + MainForm_TabPage4.Name = "MainForm_TabPage4"; + MainForm_TabPage4.Size = new Size(1862, 966); + MainForm_TabPage4.TabIndex = 3; + MainForm_TabPage4.Text = "Tab 4"; + MainForm_TabPage4.UseVisualStyleBackColor = true; // - // groupBoxProjectTasks + // tableLayoutPanelTasks1 // - groupBoxProjectTasks.Controls.Add(dataGridViewProjectTasks); - groupBoxProjectTasks.Dock = DockStyle.Fill; - groupBoxProjectTasks.Location = new Point(3, 3); - groupBoxProjectTasks.Name = "groupBoxProjectTasks"; - groupBoxProjectTasks.Size = new Size(1850, 471); - groupBoxProjectTasks.TabIndex = 0; - groupBoxProjectTasks.TabStop = false; - groupBoxProjectTasks.Text = "Tasks"; + tableLayoutPanelTasks1.ColumnCount = 1; + tableLayoutPanelTasks1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanelTasks1.Controls.Add(splitContainerTasks1, 0, 1); + tableLayoutPanelTasks1.Dock = DockStyle.Fill; + tableLayoutPanelTasks1.Location = new Point(0, 0); + tableLayoutPanelTasks1.Name = "tableLayoutPanelTasks1"; + tableLayoutPanelTasks1.RowCount = 3; + tableLayoutPanelTasks1.RowStyles.Add(new RowStyle(SizeType.Percent, 0F)); + tableLayoutPanelTasks1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanelTasks1.RowStyles.Add(new RowStyle(SizeType.Percent, 0F)); + tableLayoutPanelTasks1.Size = new Size(1862, 966); + tableLayoutPanelTasks1.TabIndex = 0; // - // dataGridViewProjectTasks + // splitContainerTasks1 // - dataGridViewProjectTasks.AllowUserToAddRows = false; - dataGridViewProjectTasks.AllowUserToDeleteRows = false; - dataGridViewProjectTasks.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; - dataGridViewProjectTasks.Dock = DockStyle.Fill; - dataGridViewProjectTasks.Location = new Point(3, 35); - dataGridViewProjectTasks.Name = "dataGridViewProjectTasks"; - dataGridViewProjectTasks.ReadOnly = true; - dataGridViewProjectTasks.RowHeadersWidth = 82; - dataGridViewProjectTasks.Size = new Size(1844, 433); - dataGridViewProjectTasks.TabIndex = 0; + splitContainerTasks1.Dock = DockStyle.Fill; + splitContainerTasks1.Location = new Point(3, 3); + splitContainerTasks1.Name = "splitContainerTasks1"; + // + // splitContainerTasks1.Panel1 + // + splitContainerTasks1.Panel1.Controls.Add(treeViewTasks1); + splitContainerTasks1.Size = new Size(1856, 960); + splitContainerTasks1.SplitterDistance = 618; + splitContainerTasks1.TabIndex = 0; + // + // treeViewTasks1 + // + treeViewTasks1.ContextMenuStrip = contextMenuStripTreeviewTasks; + treeViewTasks1.Dock = DockStyle.Fill; + treeViewTasks1.Location = new Point(0, 0); + treeViewTasks1.Name = "treeViewTasks1"; + treeViewTasks1.Size = new Size(618, 960); + treeViewTasks1.TabIndex = 0; + // + // contextMenuStripTreeviewTasks + // + 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); + // + // addTaskSubtaskToolStripMenuItem + // + addTaskSubtaskToolStripMenuItem.Name = "addTaskSubtaskToolStripMenuItem"; + addTaskSubtaskToolStripMenuItem.Size = new Size(370, 38); + addTaskSubtaskToolStripMenuItem.Text = "Add Task / Sub-task"; + addTaskSubtaskToolStripMenuItem.Click += addTaskSubtaskToolStripMenuItem_Click; + // + // editThisTaskSubtaskToolStripMenuItem + // + editThisTaskSubtaskToolStripMenuItem.Name = "editThisTaskSubtaskToolStripMenuItem"; + editThisTaskSubtaskToolStripMenuItem.Size = new Size(370, 38); + editThisTaskSubtaskToolStripMenuItem.Text = "Edit this Task / Sub-task"; + editThisTaskSubtaskToolStripMenuItem.Click += editThisTaskSubtaskToolStripMenuItem_Click; + // + // deleteThisTaskSubtaskToolStripMenuItem + // + deleteThisTaskSubtaskToolStripMenuItem.Name = "deleteThisTaskSubtaskToolStripMenuItem"; + deleteThisTaskSubtaskToolStripMenuItem.Size = new Size(370, 38); + deleteThisTaskSubtaskToolStripMenuItem.Text = "Delete this Task / Sub-task"; + deleteThisTaskSubtaskToolStripMenuItem.Click += deleteThisTaskSubtaskToolStripMenuItem_Click; + // + // toolStripSeparator1 + // + toolStripSeparator1.Name = "toolStripSeparator1"; + toolStripSeparator1.Size = new Size(367, 6); + // + // addACommentToolStripMenuItem + // + addACommentToolStripMenuItem.Name = "addACommentToolStripMenuItem"; + addACommentToolStripMenuItem.Size = new Size(370, 38); + addACommentToolStripMenuItem.Text = "Add a comment"; + addACommentToolStripMenuItem.Click += addACommentToolStripMenuItem_Click; // // MainForm // @@ -248,9 +307,12 @@ MainForm_TabPage3.ResumeLayout(false); tableLayoutPanelProjects1.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)dataGridViewProjects).EndInit(); - tableLayoutPanelProjects2.ResumeLayout(false); - groupBoxProjectTasks.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)dataGridViewProjectTasks).EndInit(); + MainForm_TabPage4.ResumeLayout(false); + tableLayoutPanelTasks1.ResumeLayout(false); + splitContainerTasks1.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)splitContainerTasks1).EndInit(); + splitContainerTasks1.ResumeLayout(false); + contextMenuStripTreeviewTasks.ResumeLayout(false); ResumeLayout(false); PerformLayout(); } @@ -269,8 +331,15 @@ private TabPage MainForm_TabPage3; private TableLayoutPanel tableLayoutPanelProjects1; private DataGridView dataGridViewProjects; - private TableLayoutPanel tableLayoutPanelProjects2; - private GroupBox groupBoxProjectTasks; - private DataGridView dataGridViewProjectTasks; + private TabPage MainForm_TabPage4; + private TableLayoutPanel tableLayoutPanelTasks1; + private SplitContainer splitContainerTasks1; + private TreeView treeViewTasks1; + private ContextMenuStrip contextMenuStripTreeviewTasks; + private ToolStripMenuItem addTaskSubtaskToolStripMenuItem; + private ToolStripMenuItem editThisTaskSubtaskToolStripMenuItem; + private ToolStripMenuItem deleteThisTaskSubtaskToolStripMenuItem; + private ToolStripSeparator toolStripSeparator1; + private ToolStripMenuItem addACommentToolStripMenuItem; } } diff --git a/Forms/MainForm.cs b/Forms/MainForm.cs index 602127f..3314afc 100644 --- a/Forms/MainForm.cs +++ b/Forms/MainForm.cs @@ -13,7 +13,7 @@ namespace trakker //private readonly string _dbversion = "[N.N.N]"; private string connectionString = string.Empty; readonly MainCtrl _ctrl; - + /// /// Initializes a new instance of the class. @@ -37,7 +37,8 @@ namespace trakker 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); } @@ -131,7 +132,7 @@ namespace trakker } } } - }; + }; dataGridViewClients.SelectionChanged += (s, e) => { @@ -274,7 +275,7 @@ namespace trakker return; } } - }; + }; dataGridViewClients.SelectionChanged += (s, e) => { @@ -290,6 +291,170 @@ namespace trakker }; } + public void FillTreeViewTasks(List items) + { + if (items == null) return; + + treeViewTasks1.BeginUpdate(); // Improves performance for large trees + treeViewTasks1.Nodes.Clear(); + + // Root node + TaskFS 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; + TaskFS? selectedNode = (TaskFS?)treeViewTasks1.SelectedNode.Tag ?? new TaskFS(); + if (selectedNode?.Parent == "") + { + MessageBox.Show("Cannot add tasks to root node", "Add Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + string taskId = Guid.NewGuid().ToString(); + trakker.Models.Task 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; + //MessageBox.Show(task.ToString(), "Task Details", MessageBoxButtons.OK, MessageBoxIcon.Information); + TaskData taskData = new TaskData(connectionString); + try + { + taskData.Upsert(task); + _ctrl.LoadTasks(); // Reload tasks to update the DataGridView with any changes + } + 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; + TaskFS? selectedTask = treeViewTasks1.SelectedNode.Tag as TaskFS; + //MessageBox.Show(selectedTask != null ? selectedTask.ToString() : "No task selected", "Edit Task", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (selectedTask?.Parent == "/" || selectedTask?.GUID == "/") + { + MessageBox.Show("Cannot edit root node", "Edit Task", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + TaskData taskData = new TaskData(connectionString); + trakker.Models.Task 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); + _ctrl.LoadTasks(); // Reload tasks to update the DataGridView 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; + TaskFS? selectedTask = treeViewTasks1.SelectedNode.Tag as TaskFS; + //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) + { + + } + //private void button1_Click(object sender, EventArgs e) //{ diff --git a/Forms/MainForm.resx b/Forms/MainForm.resx index 9603b8d..604ebd5 100644 --- a/Forms/MainForm.resx +++ b/Forms/MainForm.resx @@ -123,6 +123,9 @@ 16, 55 + + 358, 12 + 162 diff --git a/Forms/TaskForm.Designer.cs b/Forms/TaskForm.Designer.cs new file mode 100644 index 0000000..bdd687f --- /dev/null +++ b/Forms/TaskForm.Designer.cs @@ -0,0 +1,411 @@ +namespace trakker.Forms +{ + partial class TaskForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + groupBoxNewTask = new GroupBox(); + tableLayoutPanel1 = new TableLayoutPanel(); + tableLayoutPanel3 = new TableLayoutPanel(); + buttonOkay = new Button(); + buttonCancel = new Button(); + labelCreatedUpdatedDT = new Label(); + tableLayoutPanel2 = new TableLayoutPanel(); + tableLayoutPanel4 = new TableLayoutPanel(); + dateTimePickerDueDate = new DateTimePicker(); + comboBoxStatus = new ComboBox(); + labelPriority = new Label(); + comboBoxPriority = new ComboBox(); + labelDueDate = new Label(); + labelStatus = new Label(); + richTextBoxDescription = new RichTextBox(); + labelDescription = new Label(); + textBoxTitle = new TextBox(); + labelTitle = new Label(); + labelHoursEst = new Label(); + tableLayoutPanel6 = new TableLayoutPanel(); + labelHoursActual = new Label(); + textBoxHoursEst = new TextBox(); + textBoxHoursActual = new TextBox(); + labelRate = new Label(); + textBoxRate = new TextBox(); + groupBoxNewTask.SuspendLayout(); + tableLayoutPanel1.SuspendLayout(); + tableLayoutPanel3.SuspendLayout(); + tableLayoutPanel2.SuspendLayout(); + tableLayoutPanel4.SuspendLayout(); + tableLayoutPanel6.SuspendLayout(); + SuspendLayout(); + // + // groupBoxNewTask + // + groupBoxNewTask.Controls.Add(tableLayoutPanel1); + groupBoxNewTask.Dock = DockStyle.Fill; + groupBoxNewTask.Location = new Point(0, 0); + groupBoxNewTask.Name = "groupBoxNewTask"; + groupBoxNewTask.Size = new Size(1181, 372); + groupBoxNewTask.TabIndex = 2; + groupBoxNewTask.TabStop = false; + groupBoxNewTask.Text = "Task"; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(tableLayoutPanel3, 0, 1); + tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 0); + tableLayoutPanel1.Dock = DockStyle.Fill; + tableLayoutPanel1.Location = new Point(3, 35); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 2; + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 75F)); + tableLayoutPanel1.Size = new Size(1175, 334); + tableLayoutPanel1.TabIndex = 0; + // + // tableLayoutPanel3 + // + tableLayoutPanel3.ColumnCount = 3; + tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 200F)); + tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 200F)); + tableLayoutPanel3.Controls.Add(buttonOkay, 1, 0); + tableLayoutPanel3.Controls.Add(buttonCancel, 2, 0); + tableLayoutPanel3.Controls.Add(labelCreatedUpdatedDT, 0, 0); + tableLayoutPanel3.Dock = DockStyle.Fill; + tableLayoutPanel3.Location = new Point(3, 262); + tableLayoutPanel3.Name = "tableLayoutPanel3"; + tableLayoutPanel3.RowCount = 1; + tableLayoutPanel3.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); + tableLayoutPanel3.Size = new Size(1169, 69); + tableLayoutPanel3.TabIndex = 99; + // + // buttonOkay + // + buttonOkay.Dock = DockStyle.Fill; + buttonOkay.Location = new Point(772, 3); + buttonOkay.Margin = new Padding(3, 3, 3, 15); + buttonOkay.Name = "buttonOkay"; + buttonOkay.Size = new Size(194, 51); + buttonOkay.TabIndex = 9; + buttonOkay.Text = "Okay"; + buttonOkay.UseVisualStyleBackColor = true; + // + // buttonCancel + // + buttonCancel.Dock = DockStyle.Fill; + buttonCancel.Location = new Point(972, 3); + buttonCancel.Margin = new Padding(3, 3, 3, 15); + buttonCancel.Name = "buttonCancel"; + buttonCancel.Size = new Size(194, 51); + buttonCancel.TabIndex = 99; + buttonCancel.TabStop = false; + buttonCancel.Text = "Cancel"; + buttonCancel.UseVisualStyleBackColor = true; + // + // labelCreatedUpdatedDT + // + labelCreatedUpdatedDT.AutoSize = true; + labelCreatedUpdatedDT.Dock = DockStyle.Fill; + labelCreatedUpdatedDT.Location = new Point(3, 0); + labelCreatedUpdatedDT.Name = "labelCreatedUpdatedDT"; + labelCreatedUpdatedDT.Size = new Size(763, 69); + labelCreatedUpdatedDT.TabIndex = 2; + labelCreatedUpdatedDT.Text = "Created: yyyy-mm-dd, Updated: yyyy-mm-dd"; + // + // tableLayoutPanel2 + // + tableLayoutPanel2.ColumnCount = 2; + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 175F)); + tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel2.Controls.Add(tableLayoutPanel4, 1, 2); + tableLayoutPanel2.Controls.Add(labelStatus, 0, 2); + tableLayoutPanel2.Controls.Add(richTextBoxDescription, 1, 1); + tableLayoutPanel2.Controls.Add(labelDescription, 0, 1); + tableLayoutPanel2.Controls.Add(textBoxTitle, 1, 0); + tableLayoutPanel2.Controls.Add(labelTitle, 0, 0); + tableLayoutPanel2.Controls.Add(labelHoursEst, 0, 3); + tableLayoutPanel2.Controls.Add(tableLayoutPanel6, 1, 3); + tableLayoutPanel2.Dock = DockStyle.Fill; + tableLayoutPanel2.Location = new Point(3, 3); + tableLayoutPanel2.Name = "tableLayoutPanel2"; + tableLayoutPanel2.RowCount = 4; + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 100F)); + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); + tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F)); + tableLayoutPanel2.Size = new Size(1169, 253); + tableLayoutPanel2.TabIndex = 1; + // + // tableLayoutPanel4 + // + tableLayoutPanel4.ColumnCount = 5; + tableLayoutPanel4.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel4.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel4.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel4.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel4.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel4.Controls.Add(dateTimePickerDueDate, 4, 0); + tableLayoutPanel4.Controls.Add(comboBoxStatus, 0, 0); + tableLayoutPanel4.Controls.Add(labelPriority, 1, 0); + tableLayoutPanel4.Controls.Add(comboBoxPriority, 2, 0); + tableLayoutPanel4.Controls.Add(labelDueDate, 3, 0); + tableLayoutPanel4.Dock = DockStyle.Fill; + tableLayoutPanel4.Location = new Point(178, 153); + tableLayoutPanel4.Name = "tableLayoutPanel4"; + tableLayoutPanel4.RowCount = 1; + tableLayoutPanel4.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel4.Size = new Size(988, 44); + tableLayoutPanel4.TabIndex = 99; + // + // dateTimePickerDueDate + // + dateTimePickerDueDate.Format = DateTimePickerFormat.Short; + dateTimePickerDueDate.Location = new Point(791, 3); + dateTimePickerDueDate.Name = "dateTimePickerDueDate"; + dateTimePickerDueDate.Size = new Size(194, 39); + dateTimePickerDueDate.TabIndex = 5; + // + // comboBoxStatus + // + comboBoxStatus.FlatStyle = FlatStyle.Flat; + comboBoxStatus.FormattingEnabled = true; + comboBoxStatus.Location = new Point(3, 3); + comboBoxStatus.Name = "comboBoxStatus"; + comboBoxStatus.Size = new Size(190, 40); + comboBoxStatus.TabIndex = 3; + // + // labelPriority + // + labelPriority.AutoSize = true; + labelPriority.Dock = DockStyle.Fill; + labelPriority.Location = new Point(200, 0); + labelPriority.Name = "labelPriority"; + labelPriority.Size = new Size(191, 44); + labelPriority.TabIndex = 0; + labelPriority.Text = "Priority"; + labelPriority.TextAlign = ContentAlignment.TopCenter; + // + // comboBoxPriority + // + comboBoxPriority.FormattingEnabled = true; + comboBoxPriority.Location = new Point(397, 3); + comboBoxPriority.Name = "comboBoxPriority"; + comboBoxPriority.Size = new Size(191, 40); + comboBoxPriority.TabIndex = 4; + // + // labelDueDate + // + labelDueDate.AutoSize = true; + labelDueDate.Dock = DockStyle.Fill; + labelDueDate.Location = new Point(594, 0); + labelDueDate.Name = "labelDueDate"; + labelDueDate.Size = new Size(191, 44); + labelDueDate.TabIndex = 11; + labelDueDate.Text = "Due Date"; + labelDueDate.TextAlign = ContentAlignment.TopCenter; + // + // labelStatus + // + labelStatus.AutoSize = true; + labelStatus.Dock = DockStyle.Fill; + labelStatus.Location = new Point(3, 150); + labelStatus.Name = "labelStatus"; + labelStatus.Size = new Size(169, 50); + labelStatus.TabIndex = 14; + labelStatus.Text = "Status"; + // + // richTextBoxDescription + // + richTextBoxDescription.Dock = DockStyle.Fill; + richTextBoxDescription.Location = new Point(178, 53); + richTextBoxDescription.Name = "richTextBoxDescription"; + richTextBoxDescription.Size = new Size(988, 94); + richTextBoxDescription.TabIndex = 2; + richTextBoxDescription.Text = ""; + // + // labelDescription + // + labelDescription.AutoSize = true; + labelDescription.Dock = DockStyle.Fill; + labelDescription.Location = new Point(3, 50); + labelDescription.Name = "labelDescription"; + labelDescription.Size = new Size(169, 100); + labelDescription.TabIndex = 12; + labelDescription.Text = "Description"; + // + // textBoxTitle + // + textBoxTitle.Dock = DockStyle.Fill; + textBoxTitle.Location = new Point(178, 3); + textBoxTitle.Margin = new Padding(3, 3, 52, 3); + textBoxTitle.Name = "textBoxTitle"; + textBoxTitle.PlaceholderText = "Task name"; + textBoxTitle.Size = new Size(939, 39); + textBoxTitle.TabIndex = 1; + textBoxTitle.Validating += textBoxTitle_Validating; + // + // labelTitle + // + labelTitle.AutoSize = true; + labelTitle.Dock = DockStyle.Fill; + labelTitle.Location = new Point(3, 0); + labelTitle.Name = "labelTitle"; + labelTitle.Size = new Size(169, 50); + labelTitle.TabIndex = 10; + labelTitle.Text = "Name *"; + // + // labelHoursEst + // + labelHoursEst.AutoSize = true; + labelHoursEst.Dock = DockStyle.Fill; + labelHoursEst.Location = new Point(3, 200); + labelHoursEst.Name = "labelHoursEst"; + labelHoursEst.Size = new Size(169, 53); + labelHoursEst.TabIndex = 0; + labelHoursEst.Text = "Estimate Hrs"; + // + // tableLayoutPanel6 + // + tableLayoutPanel6.ColumnCount = 5; + tableLayoutPanel6.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel6.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel6.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel6.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel6.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); + tableLayoutPanel6.Controls.Add(labelHoursActual, 1, 0); + tableLayoutPanel6.Controls.Add(textBoxHoursEst, 0, 0); + tableLayoutPanel6.Controls.Add(textBoxHoursActual, 2, 0); + tableLayoutPanel6.Controls.Add(labelRate, 3, 0); + tableLayoutPanel6.Controls.Add(textBoxRate, 4, 0); + tableLayoutPanel6.Dock = DockStyle.Fill; + tableLayoutPanel6.Location = new Point(178, 203); + tableLayoutPanel6.Name = "tableLayoutPanel6"; + tableLayoutPanel6.RowCount = 1; + tableLayoutPanel6.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel6.Size = new Size(988, 47); + tableLayoutPanel6.TabIndex = 99; + // + // labelHoursActual + // + labelHoursActual.AutoSize = true; + labelHoursActual.Dock = DockStyle.Fill; + labelHoursActual.Location = new Point(200, 0); + labelHoursActual.Name = "labelHoursActual"; + labelHoursActual.Size = new Size(191, 47); + labelHoursActual.TabIndex = 2; + labelHoursActual.Text = "Actual Hrs"; + labelHoursActual.TextAlign = ContentAlignment.TopCenter; + // + // textBoxHoursEst + // + textBoxHoursEst.Dock = DockStyle.Fill; + textBoxHoursEst.Location = new Point(3, 3); + textBoxHoursEst.Name = "textBoxHoursEst"; + textBoxHoursEst.Size = new Size(191, 39); + textBoxHoursEst.TabIndex = 6; + // + // textBoxHoursActual + // + textBoxHoursActual.Dock = DockStyle.Fill; + textBoxHoursActual.Location = new Point(397, 3); + textBoxHoursActual.Name = "textBoxHoursActual"; + textBoxHoursActual.Size = new Size(191, 39); + textBoxHoursActual.TabIndex = 6; + // + // labelRate + // + labelRate.AutoSize = true; + labelRate.Dock = DockStyle.Fill; + labelRate.Location = new Point(594, 0); + labelRate.Name = "labelRate"; + labelRate.Size = new Size(191, 47); + labelRate.TabIndex = 5; + labelRate.Text = "Rate"; + labelRate.TextAlign = ContentAlignment.TopCenter; + // + // textBoxRate + // + textBoxRate.Dock = DockStyle.Fill; + textBoxRate.Location = new Point(791, 3); + textBoxRate.Name = "textBoxRate"; + textBoxRate.Size = new Size(194, 39); + textBoxRate.TabIndex = 8; + // + // TaskForm + // + AutoScaleDimensions = new SizeF(13F, 32F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1181, 372); + Controls.Add(groupBoxNewTask); + Name = "TaskForm"; + Text = "TaskForm"; + groupBoxNewTask.ResumeLayout(false); + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel3.ResumeLayout(false); + tableLayoutPanel3.PerformLayout(); + tableLayoutPanel2.ResumeLayout(false); + tableLayoutPanel2.PerformLayout(); + tableLayoutPanel4.ResumeLayout(false); + tableLayoutPanel4.PerformLayout(); + tableLayoutPanel6.ResumeLayout(false); + tableLayoutPanel6.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private GroupBox groupBoxNewTask; + private TableLayoutPanel tableLayoutPanel1; + private TableLayoutPanel tableLayoutPanel2; + private Label labelClient; + private Label labelTitle; + private Label labelDescription; + private Label labelHoursEst; + private TextBox textBoxTitle; + private ComboBox comboBoxClient; + private TableLayoutPanel tableLayoutPanel6; + private Label labelHoursActual; + private RichTextBox richTextBoxDescription; + private TableLayoutPanel tableLayoutPanel4; + private ComboBox comboBoxStatus; + private Label labelPriority; + private Label labelStatus; + private DateTimePicker dateTimePickerDueDate; + private ComboBox comboBoxPriority; + private Label labelDueDate; + private TextBox textBoxHoursEst; + private TextBox textBoxHoursActual; + private TableLayoutPanel tableLayoutPanel3; + private Button buttonOkay; + private Button buttonCancel; + private Label labelCreatedUpdatedDT; + private Label labelRate; + private TextBox textBoxRate; + } +} \ No newline at end of file diff --git a/Forms/TaskForm.cs b/Forms/TaskForm.cs new file mode 100644 index 0000000..4eec137 --- /dev/null +++ b/Forms/TaskForm.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using trakker.Models; + +namespace trakker.Forms +{ + + public partial class TaskForm : Form + { + /// + /// The task instance being edited by this form. + /// + private readonly trakker.Models.Task _task; + + /// + /// Binding source that connects the task status to the form controls. + /// + private BindingSource? _status; + + /// + /// Binding source that connects the task priority to the form controls. + /// + private BindingSource? _priority; + + /// + /// Binding source that connects the task model to the form controls. + /// + private BindingSource bindingSource = new BindingSource(); + + /// + /// Error provider used to display validation errors next to input controls. + /// + private ErrorProvider errorProvider = new ErrorProvider(); + + /// + /// Initializes a new instance of the class bound to + /// the provided . Sets up data bindings for all + /// visible input controls and configures dialog button behavior. + /// + /// The instance to edit. Must not be null. + /// The binding source for status values. Must not be null. + /// The binding source for priority values. Must not be null. + public TaskForm(trakker.Models.Task task, BindingSource status, BindingSource priority) + { + InitializeComponent(); + _task = task ?? throw new ArgumentNullException(nameof(task)); + _status = status ?? throw new ArgumentNullException(nameof(status)); + _priority = priority ?? throw new ArgumentNullException(nameof(priority)); + + Text = "Task Details"; + + comboBoxStatus.DataSource = _status; + comboBoxStatus.DisplayMember = "Display"; + comboBoxStatus.ValueMember = "Value"; + + comboBoxPriority.DataSource = _priority; + comboBoxPriority.DisplayMember = "Display"; + comboBoxPriority.ValueMember = "Value"; + + dateTimePickerDueDate.Format = DateTimePickerFormat.Custom; + dateTimePickerDueDate.CustomFormat = "yyyy-MM-dd"; + + // // Bind model properties to controls so the UI reflects and updates the model. + bindingSource.DataSource = _task; + textBoxTitle.DataBindings.Add("Text", bindingSource, "Title", true); + richTextBoxDescription.DataBindings.Add("Text", bindingSource, "Description", true); + dateTimePickerDueDate.DataBindings.Add("Value", bindingSource, "DueDate", true); + textBoxHoursEst.DataBindings.Add("Text", bindingSource, "EstimatedHours", true); + textBoxHoursActual.DataBindings.Add("Text", bindingSource, "ActualHours", true); + textBoxRate.DataBindings.Add("Text", bindingSource, "HourlyRate", true); + comboBoxStatus.DataBindings.Add("SelectedValue", bindingSource, "Status", true); + comboBoxPriority.DataBindings.Add("SelectedValue", bindingSource, "Priority", true); + + // Configure dialog buttons and window behavior. + buttonOkay.DialogResult = DialogResult.OK; + buttonCancel.DialogResult = DialogResult.Cancel; + this.CancelButton = buttonCancel; + this.StartPosition = FormStartPosition.CenterParent; + } + + /// + /// Gets the instance edited by the form. + /// + public trakker.Models.Task Task { get => _task; private set { } } + + /// + /// Validates the Name field. If the name is empty or whitespace, an error is set + /// on the and the event is canceled to prevent the + /// form from closing. + /// + private void textBoxTitle_Validating(object sender, System.ComponentModel.CancelEventArgs e) + { + if (string.IsNullOrWhiteSpace(textBoxTitle.Text)) + { + errorProvider.SetError(textBoxTitle, "Title is required."); + errorProvider.SetIconAlignment(textBoxTitle, ErrorIconAlignment.MiddleRight); + errorProvider.SetIconPadding(textBoxTitle, 2); + e.Cancel = true; + } + else + { + errorProvider.SetError(textBoxTitle, ""); + } + } + + } +} diff --git a/Forms/TaskForm.resx b/Forms/TaskForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/Forms/TaskForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Interfaces/IMainForm.cs b/Interfaces/IMainForm.cs index d537a2d..6aef320 100644 --- a/Interfaces/IMainForm.cs +++ b/Interfaces/IMainForm.cs @@ -13,5 +13,6 @@ namespace trakker.Interfaces void InitDataGridViewClients(BindingList clients); void InitDataGridViewProjects(); void FillDataGridViewProjects(BindingSource projects); + void FillTreeViewTasks(List items); } } diff --git a/Models/LOV.cs b/Models/LOV.cs index e9b4db5..d4f75d8 100644 --- a/Models/LOV.cs +++ b/Models/LOV.cs @@ -1,13 +1,14 @@ namespace trakker.Models { - internal class LOV + public class LOV { public LOV(string value, string display) { Value = value; Display = display; } + public string Value { get; set; } public string Display { get; set; } } -} +} \ No newline at end of file diff --git a/Models/Task.cs b/Models/Task.cs new file mode 100644 index 0000000..d64837a --- /dev/null +++ b/Models/Task.cs @@ -0,0 +1,94 @@ +namespace trakker.Models +{ + public class Task + { + public Task() + { + TaskId = Guid.NewGuid().ToString(); + DueDate = DateTime.Now; + CreatedAt = DateTime.Now; + UpdatedAt = DateTime.Now; + } + public string? TaskId { get; set; } = string.Empty; + + public string? ProjectId { get; set; } = string.Empty; + + public string? Title { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string Status { get; set; } = "todo"; + + public string Priority { get; set; } = "medium"; + + public DateTime? DueDate { get; set; } + + public double? EstimatedHours { get; set; } = 0; + + public double? ActualHours { get; set; } = 0; + + public double? HourlyRate { get; set; } = 0; + + public string? ParentTaskId { get; set; } + + public DateTime? CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + + public override string ToString() + { + return @$" + TaskId: {TaskId} + ProjectId: {ProjectId} + Title: {Title} + Description: {Description} + Status: {Status} + Priority: {Priority} + DueDate: {DueDate} + EstimatedHours: {EstimatedHours} + ActualHours: {ActualHours} + HourlyRate: {HourlyRate} + ParentTaskId: {ParentTaskId} + CreatedAt: {CreatedAt} + UpdatedAt: {UpdatedAt} + "; + } + + } + + public class TaskFS + { + public string GUID { get; set; } = string.Empty; + public string Node { get; set; } = string.Empty; + public string Parent { get; set; } = string.Empty; + public double? HourlyRate { get; set; } = 0; + public string ProjectId { get; set; } = string.Empty; + public override string ToString() + { + return @$" + GUID: {GUID} + Node: {Node} + Parent: {Parent} + HourlyRate: {HourlyRate} + ProjectId: {ProjectId} + "; + } + + } + + public class TaskComment + { + public TaskComment() + { + TaskCommentId = Guid.NewGuid().ToString(); + } + public string? TaskCommentId { get; set; } = string.Empty; + + public string? TaskId { get; set; } = string.Empty; + + public string? Comment { get; set; } = string.Empty; + + public DateTime? CreatedAt { get; set; } + } + +} diff --git a/Services/MainCtrl.cs b/Services/MainCtrl.cs index ffe968e..4595d8f 100644 --- a/Services/MainCtrl.cs +++ b/Services/MainCtrl.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using trakker.Models; using trakker.Interfaces; +using trakker.Data; namespace trakker.Services { @@ -55,6 +56,12 @@ namespace trakker.Services var x = project.Status; } _view.FillDataGridViewProjects(new BindingSource { DataSource = projects }); + LoadTasks(); + } + internal void LoadTasks() + { + var dbo = new TaskData(_connectionString); + _view.FillTreeViewTasks(dbo.GetFS()); } } }