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());
}
}
}