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 projects. It inherits from which
/// provides connection management.
///
internal class ProjectData(string connectionString) : DataAccess(connectionString)
{
public BindingList Get(string? projectId = null)
{
var results = new BindingList();
string whereClause = "1 = 1";
if (projectId != null)
{
whereClause = "p.project_id = $project_id";
}
string sql = $@"
SELECT
p.project_id,
p.client_id,
p.project_code,
c.name AS client_name,
p.name AS project_name,
p.description,
p.start_date,
p.end_date,
p.budget,
p.status,
l.display AS status_name,
p.hourly_rate,
IFNULL(a.amount, 0.0) AS actuals,
p.notes,
p.created_at,
p.updated_at,
(SELECT COUNT(*) FROM tasks x WHERE x.project_id = p.project_id ) AS task_count
FROM projects p
LEFT JOIN clients c ON p.client_id = c.client_id
LEFT JOIN (SELECT project_id, SUM(hourly_rate * actual_hours) AS amount FROM tasks GROUP BY project_id) a ON p.project_id = a.project_id
JOIN (SELECT value, display FROM lov WHERE source = 'project.status') l ON p.status = l.value
WHERE
{whereClause}
ORDER BY p.start_date DESC, p.name ASC;
;
";
using var conn = OpenConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
if (projectId != null)
{
cmd.Parameters.AddWithValue("$project_id", projectId);
}
using var reader = cmd.ExecuteReader();
var _var1 = reader.GetOrdinal("project_id");
var _var2 = reader.GetOrdinal("client_id");
var _var3 = reader.GetOrdinal("project_code");
var _var4 = reader.GetOrdinal("client_name");
var _var5 = reader.GetOrdinal("project_name");
var _var6 = reader.GetOrdinal("description");
var _var7 = reader.GetOrdinal("start_date");
var _var8 = reader.GetOrdinal("end_date");
var _var9 = reader.GetOrdinal("budget");
var _var10 = reader.GetOrdinal("status");
var _var11 = reader.GetOrdinal("status_name");
var _var12 = reader.GetOrdinal("hourly_rate");
var _var13 = reader.GetOrdinal("notes");
var _var14 = reader.GetOrdinal("created_at");
var _var15 = reader.GetOrdinal("updated_at");
var _var16 = reader.GetOrdinal("actuals");
var _var17 = reader.GetOrdinal("task_count");
while (reader.Read())
{
results.Add(new Project
{
ProjectId = reader.GetString(_var1),
ClientId = reader.GetString(_var2),
ProjectCode = reader.GetString(_var3),
ClientName = reader.GetString(_var4),
ProjectName = reader.GetString(_var5),
Description = reader.GetString(_var6),
StartDate = reader.IsDBNull(_var7) ? null : reader.GetDateTime(_var7),
EndDate = reader.IsDBNull(_var8) ? null : reader.GetDateTime(_var8),
Budget = reader.GetDecimal(_var9),
Status = reader.GetString(_var10),
StatusName = reader.GetString(_var11),
HourlyRate = reader.GetDecimal(_var12),
Notes = reader.GetString(_var13),
CreatedAt = reader.GetDateTime(_var14),
UpdatedAt = reader.GetDateTime(_var15),
Actuals = reader.IsDBNull(_var16) ? null : reader.GetDecimal(_var16),
TaskCount = reader.GetInt32(_var17)
});
}
return results;
}
///
/// Inserts a new project 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 project_id already exists. Parameter names correspond to the
/// project model property names.
///
public void Upsert(Project project)
{
const string sql = @"
INSERT INTO projects (
project_id,
client_id,
name,
description,
start_date,
end_date,
budget,
status,
hourly_rate,
notes
)
VALUES (
$project_id,
$client_id,
$name,
$description,
$start_date,
$end_date,
$budget,
$status,
$hourly_rate,
$notes
)
ON CONFLICT (project_id) DO UPDATE SET
client_id = excluded.client_id,
name = excluded.name,
description = excluded.description,
start_date = excluded.start_date,
end_date = excluded.end_date,
budget = excluded.budget,
status = excluded.status,
hourly_rate = excluded.hourly_rate,
notes = excluded.notes,
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("$project_id", project.ProjectId);
cmd.Parameters.AddWithValue("$client_id", project.ClientId);
cmd.Parameters.AddWithValue("$name", project.ProjectName);
cmd.Parameters.AddWithValue("$description", project.Description);
cmd.Parameters.AddWithValue("$start_date", project.StartDate);
cmd.Parameters.AddWithValue("$end_date", project.EndDate);
cmd.Parameters.AddWithValue("$budget", project.Budget);
cmd.Parameters.AddWithValue("$status", project.Status);
cmd.Parameters.AddWithValue("$hourly_rate", project.HourlyRate);
cmd.Parameters.AddWithValue("$notes", project.Notes);
cmd.ExecuteNonQuery();
}
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
}
///
/// Deletes the project with the specified from the
/// database.
///
/// The identifier of the project 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 projectId)
{
const string sql = @"
DELETE FROM
projects
WHERE
project_id = $project_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("$project_id", projectId);
cmd.ExecuteNonQuery();
}
using var idCmd = conn.CreateCommand();
idCmd.Transaction = tx;
result = (int?)idCmd.ExecuteScalar() ;
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
return result;
}
}
}