Continued development

This commit is contained in:
c0d3.m0nk3y 2026-05-11 20:09:35 -04:00
parent 3491593c07
commit 94e532986c
11 changed files with 1425 additions and 55 deletions

385
Data/TaskData.cs Normal file
View File

@ -0,0 +1,385 @@
using System.ComponentModel;
using trakker.Models;
namespace trakker.Data
{
/// <summary>
/// Provides data access methods for the <see cref="Models.Task"/> entity.
/// This class encapsulates database operations such as upsert, delete and ad-hoc
/// SQL execution for tasks. It inherits from <see cref="DataAccess"/> which
/// provides connection management.
/// </summary>
internal class TaskData(string connectionString) : DataAccess(connectionString)
{
public trakker.Models.Task Get(string? taskId = null)
{
var results = new List<trakker.Models.Task>();
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();
}
/// <summary>
/// Inserts a new task record or updates an existing one (upsert) using
/// the provided <paramref name="task"/> model. This method executes
/// a single SQL statement inside a transaction and will commit on
/// success or roll back on failure.
/// </summary>
/// <param name="task">The <see cref="Task"/> model to insert or update. Must not be null.</param>
/// <remarks>
/// The SQL statement uses an ON CONFLICT clause to perform the update when a
/// matching <c>task_id</c> already exists. Parameter names correspond to the
/// task model property names.
/// </remarks>
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;
}
}
/// <summary>
/// Deletes the task with the specified <paramref name="taskId"/> from the
/// database.
/// </summary>
/// <param name="taskId">The identifier of the task to delete.</param>
/// <returns>An optional integer representing any scalar value returned by the
/// command executed after deletion (if applicable). May be null.</returns>
/// <remarks>
/// 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.
/// </remarks>
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<TaskFS> GetFS()
{
var results = new List<TaskFS>();
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<TaskComment> GetComments(string taskId)
{
var results = new List<TaskComment>();
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;
}
}
}

View File

@ -28,6 +28,7 @@
/// </summary>
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;
}
}

View File

@ -37,6 +37,7 @@ 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);
}
@ -290,6 +291,170 @@ namespace trakker
};
}
public void FillTreeViewTasks(List<TaskFS> 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<string, List<TaskFS>>();
foreach (var it in items)
{
var key = NormalizeKey(it.Parent);
if (!lookup.TryGetValue(key, out var list))
{
list = new List<TaskFS>();
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)
//{

View File

@ -123,6 +123,9 @@
<metadata name="MainForm_StatusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>16, 55</value>
</metadata>
<metadata name="contextMenuStripTreeviewTasks.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>358, 12</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>162</value>
</metadata>

411
Forms/TaskForm.Designer.cs generated Normal file
View File

@ -0,0 +1,411 @@
namespace trakker.Forms
{
partial class TaskForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

114
Forms/TaskForm.cs Normal file
View File

@ -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
{
/// <summary>
/// The task instance being edited by this form.
/// </summary>
private readonly trakker.Models.Task _task;
/// <summary>
/// Binding source that connects the task status to the form controls.
/// </summary>
private BindingSource? _status;
/// <summary>
/// Binding source that connects the task priority to the form controls.
/// </summary>
private BindingSource? _priority;
/// <summary>
/// Binding source that connects the task model to the form controls.
/// </summary>
private BindingSource bindingSource = new BindingSource();
/// <summary>
/// Error provider used to display validation errors next to input controls.
/// </summary>
private ErrorProvider errorProvider = new ErrorProvider();
/// <summary>
/// Initializes a new instance of the <see cref="ProjectForm"/> class bound to
/// the provided <paramref name="project"/>. Sets up data bindings for all
/// visible input controls and configures dialog button behavior.
/// </summary>
/// <param name="task">The <see cref="Task"/> instance to edit. Must not be null.</param>
/// <param name="status">The binding source for status values. Must not be null.</param>
/// <param name="priority">The binding source for priority values. Must not be null.</param>
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;
}
/// <summary>
/// Gets the <see cref="Task"/> instance edited by the form.
/// </summary>
public trakker.Models.Task Task { get => _task; private set { } }
/// <summary>
/// Validates the Name field. If the name is empty or whitespace, an error is set
/// on the <see cref="errorProvider"/> and the event is canceled to prevent the
/// form from closing.
/// </summary>
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, "");
}
}
}
}

120
Forms/TaskForm.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -13,5 +13,6 @@ namespace trakker.Interfaces
void InitDataGridViewClients(BindingList<Client> clients);
void InitDataGridViewProjects();
void FillDataGridViewProjects(BindingSource projects);
void FillTreeViewTasks(List<TaskFS> items);
}
}

View File

@ -1,12 +1,13 @@
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; }
}

94
Models/Task.cs Normal file
View File

@ -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; }
}
}

View File

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