trakker/Data/TaskData.cs

503 lines
18 KiB
C#

using System.ComponentModel;
using trakker.Models;
namespace trakker.Data
{
/// <summary>
/// Provides data access methods for the <see cref="Models.PTask"/> 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 BindingList<PTask> GetRecursive(string? id = null, PTask.RecursiveRoot root = PTask.RecursiveRoot.TASK_ID)
{
var results = new BindingList<PTask>();
string whereClause = "1 = 1";
switch(root)
{
case PTask.RecursiveRoot.TASK_ID:
if (id != null)
{
whereClause = "task_id = $id";
}
break;
case PTask.RecursiveRoot.PARENT_TASK_ID:
if (id != null)
{
whereClause = "parent_task_id = $id";
}
break;
}
string sql = $@"
WITH RECURSIVE TaskHierarchy AS (
-- Anchor: starting task(s)
SELECT
task_id,
project_id,
title,
description,
status,
priority,
due_date,
estimated_hours,
actual_hours,
hourly_rate,
parent_task_id,
created_at,
updated_at,
0 AS level,
title AS path
FROM tasks
WHERE
{whereClause}
UNION ALL
-- Recursive part: get children
SELECT
t.task_id,
t.project_id,
t.title,
t.description,
t.status,
t.priority,
t.due_date,
t.estimated_hours,
t.actual_hours,
t.hourly_rate,
t.parent_task_id,
t.created_at,
t.updated_at,
th.level + 1,
th.path || ' > ' || t.title
FROM tasks t
JOIN TaskHierarchy th ON t.parent_task_id = th.task_id
)
SELECT *
FROM TaskHierarchy
ORDER BY path
;
";
using var conn = OpenConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
if (id != null)
{
cmd.Parameters.AddWithValue("$id", id);
}
using var reader = cmd.ExecuteReader();
var _var1 = reader.GetOrdinal("task_id");
var _var2 = reader.GetOrdinal("project_id");
var _var3 = reader.GetOrdinal("title");
var _var4 = reader.GetOrdinal("description");
var _var5 = reader.GetOrdinal("status");
var _var6 = reader.GetOrdinal("priority");
var _var7 = reader.GetOrdinal("due_date");
var _var8 = reader.GetOrdinal("estimated_hours");
var _var9 = reader.GetOrdinal("actual_hours");
var _var10 = reader.GetOrdinal("hourly_rate");
var _var11 = reader.GetOrdinal("parent_task_id");
var _var12 = reader.GetOrdinal("created_at");
var _var13 = reader.GetOrdinal("updated_at");
while (reader.Read())
{
results.Add(new PTask
{
TaskId = reader.GetString(_var1),
ProjectId = reader.GetString(_var2),
Title = reader.GetString(_var3),
Description = reader.GetString(_var4),
Status = reader.GetString(_var5),
Priority = reader.GetString(_var6),
DueDate = reader.IsDBNull(_var7) ? null : reader.GetDateTime(_var7),
EstimatedHours = reader.IsDBNull(_var8) ? null : reader.GetDouble(_var8),
ActualHours = reader.IsDBNull(_var9) ? null : reader.GetDouble(_var9),
HourlyRate = reader.IsDBNull(_var10) ? null : reader.GetDouble(_var10),
ParentTaskId = reader.IsDBNull(_var11) ? null : reader.GetString(_var11),
CreatedAt = reader.IsDBNull(_var12) ? null : reader.GetDateTime(_var12),
UpdatedAt = reader.IsDBNull(_var13) ? null : reader.GetDateTime(_var13),
});
}
return results;
}
public PTask Get(string? taskId = null)
{
var results = new List<PTask>();
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 PTask
{
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 PTask();
}
/// <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="PTask"/> 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(PTask 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<PTaskFS> GetFS()
{
var results = new List<PTaskFS>();
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 PTaskFS
{
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(PTaskComment 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<PTaskComment> GetComments(string taskId)
{
var results = new List<PTaskComment>();
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 PTaskComment
{
TaskCommentId = reader.GetString(_var1),
TaskId = reader.GetString(_var2),
Comment = reader.GetString(_var3),
CreatedAt = reader.IsDBNull(_var4) ? (DateTime?)null : reader.GetDateTime(_var4)
});
}
return results;
}
}
}