Continued development

This commit is contained in:
c0d3.m0nk3y 2026-05-05 21:05:23 -04:00
parent 222a37c0de
commit 289657a7d5
11 changed files with 737 additions and 120 deletions

268
Data/ClientData.cs Normal file
View File

@ -0,0 +1,268 @@
using System.ComponentModel;
using trakker.Models;
namespace trakker.Data
{
/// <summary>
/// Provides data access methods for the <see cref="Models.Client"/> entity.
/// This class encapsulates database operations such as upsert, delete and ad-hoc
/// SQL execution for clients. It inherits from <see cref="DataAccess"/> which
/// provides connection management.
/// </summary>
internal class ClientData(string connectionString) : DataAccess(connectionString)
{
public BindingList<Client> Get(string? clientId = null)
{
var results = new BindingList<Client>();
string whereClause = "1 = 1";
if (clientId != null)
{
whereClause = "client_id = $client_id";
}
string sql = $@"
SELECT
client_id,
name,
company,
email,
phone,
address_street,
address_city,
address_state,
address_postal,
notes,
is_active
FROM
clients
WHERE
{whereClause}
ORDER BY
name ASC
;
";
using var conn = OpenConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
if (clientId != null)
{
cmd.Parameters.AddWithValue("$client_id", clientId);
}
using var reader = cmd.ExecuteReader();
var _var1 = reader.GetOrdinal("client_id");
var _var2 = reader.GetOrdinal("name");
var _var3 = reader.GetOrdinal("company");
var _var4 = reader.GetOrdinal("email");
var _var5 = reader.GetOrdinal("phone");
var _var6 = reader.GetOrdinal("address_street");
var _var7 = reader.GetOrdinal("address_city");
var _var8 = reader.GetOrdinal("address_state");
var _var9 = reader.GetOrdinal("address_postal");
var _var10 = reader.GetOrdinal("notes");
var _var11 = reader.GetOrdinal("is_active");
while (reader.Read())
{
results.Add(new Client
{
ClientId = reader.GetString(_var1),
Name = reader.GetString(_var2),
Company = reader.GetString(_var3),
Email = reader.GetString(_var4),
Phone = reader.GetString(_var5),
AddressStreet = reader.GetString(_var6),
AddressCity = reader.GetString(_var7),
AddressState = reader.GetString(_var8),
AddressPostal = reader.GetString(_var9),
Notes = reader.GetString(_var10),
IsActive = reader.GetString(_var11),
});
}
return results;
}
/// <summary>
/// Inserts a new client record or updates an existing one (upsert) using
/// the provided <paramref name="client"/> model. This method executes
/// a single SQL statement inside a transaction and will commit on
/// success or roll back on failure.
/// </summary>
/// <param name="client">The <see cref="Client"/> 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>client_id</c> already exists. Parameter names correspond to the
/// client model property names.
/// </remarks>
public void Upsert(Client client)
{
const string sql = @"
INSERT INTO clients (
client_id,
name,
company,
email,
phone,
address_street,
address_city,
address_state,
address_postal,
notes,
is_active
)
VALUES (
$client_id,
$name,
$company,
$email,
$phone,
$address_street,
$address_city,
$address_state,
$address_postal,
$notes,
$is_active
)
ON CONFLICT(client_id) DO UPDATE SET
name = excluded.name,
company = excluded.company,
email = excluded.email,
phone = excluded.phone,
address_street = excluded.address_street,
address_city = excluded.address_city,
address_state = excluded.address_state,
address_postal = excluded.address_postal,
notes = excluded.notes,
is_active = excluded.is_active,
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("$client_id", client.ClientId);
cmd.Parameters.AddWithValue("$name", client.Name);
cmd.Parameters.AddWithValue("$company", client.Company);
cmd.Parameters.AddWithValue("$email", client.Email);
cmd.Parameters.AddWithValue("$phone", client.Phone);
cmd.Parameters.AddWithValue("$address_street", client.AddressStreet);
cmd.Parameters.AddWithValue("$address_city", client.AddressCity);
cmd.Parameters.AddWithValue("$address_state", client.AddressState);
cmd.Parameters.AddWithValue("$address_postal", client.AddressPostal);
cmd.Parameters.AddWithValue("$notes", client.Notes);
cmd.Parameters.AddWithValue("$is_active", client.IsActive);
cmd.ExecuteNonQuery();
}
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
}
/// <summary>
/// Deletes the client with the specified <paramref name="clientId"/> from the
/// database.
/// </summary>
/// <param name="clientId">The identifier of the client 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 clientId)
{
const string sql = @"
DELETE FROM
clients
WHERE
client_id = $client_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("$client_id", clientId);
cmd.ExecuteNonQuery();
}
using var idCmd = conn.CreateCommand();
idCmd.Transaction = tx;
result = (int?)idCmd.ExecuteScalar() ;
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
return result;
}
/// <summary>
/// Executes arbitrary, ad-hoc SQL against the database inside a transaction.
/// </summary>
/// <param name="sql">A SQL statement to execute. The caller is responsible for
/// ensuring the SQL is safe and properly parameterized to avoid SQL injection.</param>
/// <remarks>
/// This method is intended for one-off maintenance or administrative commands.
/// It does not return any result; if a scalar value is produced by the SQL,
/// the current implementation captures it but does not expose it to the caller.
/// </remarks>
public void Adhoc(string sql)
{
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.ExecuteNonQuery();
}
using var idCmd = conn.CreateCommand();
idCmd.Transaction = tx;
result = (int?)idCmd.ExecuteScalar() ;
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
}
}
}

29
Data/DataAccess.cs Normal file
View File

@ -0,0 +1,29 @@
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace trakker.Data
{
public class DataAccess(string connectionString)
{
private readonly string _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
/// <summary>
/// Creates a new, unopened SqliteConnection.
/// </summary>
protected SqliteConnection CreateConnection() => new(_connectionString);
/// <summary>
/// Opens and returns a SqliteConnection (synchronous).
/// </summary>
protected SqliteConnection OpenConnection()
{
var conn = CreateConnection();
conn.Open();
return conn;
}
}
}

View File

@ -36,17 +36,15 @@
labelEmail = new Label(); labelEmail = new Label();
labelPhone = new Label(); labelPhone = new Label();
labelAddress = new Label(); labelAddress = new Label();
labelIsActive = new Label();
textBoxName = new TextBox(); textBoxName = new TextBox();
textBoxCompany = new TextBox(); textBoxCompany = new TextBox();
textBoxEmail = new TextBox(); textBoxEmail = new TextBox();
comboBoxIsActive = new ComboBox();
maskedTextBox_Phone = new MaskedTextBox(); maskedTextBox_Phone = new MaskedTextBox();
tableLayoutPanel4 = new TableLayoutPanel(); tableLayoutPanel4 = new TableLayoutPanel();
tableLayoutPanel5 = new TableLayoutPanel(); tableLayoutPanel5 = new TableLayoutPanel();
textBoxAddressCity = new TextBox(); textBoxAddressCity = new TextBox();
comboBoxAddressState = new ComboBox(); comboBoxAddressState = new ComboBox();
maskedTextBoxAddressZipcode = new MaskedTextBox(); maskedTextBoxAddressPostal = new MaskedTextBox();
textBoxAddressStreet = new TextBox(); textBoxAddressStreet = new TextBox();
groupBoxNotes = new GroupBox(); groupBoxNotes = new GroupBox();
richTextBoxNotes = new RichTextBox(); richTextBoxNotes = new RichTextBox();
@ -86,7 +84,7 @@
tableLayoutPanel1.Name = "tableLayoutPanel1"; tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 3; tableLayoutPanel1.RowCount = 3;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 200F)); tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 242F));
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 75F)); tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 75F));
tableLayoutPanel1.Size = new Size(1122, 627); tableLayoutPanel1.Size = new Size(1122, 627);
tableLayoutPanel1.TabIndex = 0; tableLayoutPanel1.TabIndex = 0;
@ -101,25 +99,22 @@
tableLayoutPanel2.Controls.Add(labelEmail, 0, 2); tableLayoutPanel2.Controls.Add(labelEmail, 0, 2);
tableLayoutPanel2.Controls.Add(labelPhone, 0, 3); tableLayoutPanel2.Controls.Add(labelPhone, 0, 3);
tableLayoutPanel2.Controls.Add(labelAddress, 0, 4); tableLayoutPanel2.Controls.Add(labelAddress, 0, 4);
tableLayoutPanel2.Controls.Add(labelIsActive, 0, 5);
tableLayoutPanel2.Controls.Add(textBoxName, 1, 0); tableLayoutPanel2.Controls.Add(textBoxName, 1, 0);
tableLayoutPanel2.Controls.Add(textBoxCompany, 1, 1); tableLayoutPanel2.Controls.Add(textBoxCompany, 1, 1);
tableLayoutPanel2.Controls.Add(textBoxEmail, 1, 2); tableLayoutPanel2.Controls.Add(textBoxEmail, 1, 2);
tableLayoutPanel2.Controls.Add(comboBoxIsActive, 1, 5);
tableLayoutPanel2.Controls.Add(maskedTextBox_Phone, 1, 3); tableLayoutPanel2.Controls.Add(maskedTextBox_Phone, 1, 3);
tableLayoutPanel2.Controls.Add(tableLayoutPanel4, 1, 4); tableLayoutPanel2.Controls.Add(tableLayoutPanel4, 1, 4);
tableLayoutPanel2.Dock = DockStyle.Fill; tableLayoutPanel2.Dock = DockStyle.Fill;
tableLayoutPanel2.Location = new Point(3, 3); tableLayoutPanel2.Location = new Point(3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2"; tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 6; tableLayoutPanel2.RowCount = 5;
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F));
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F));
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F));
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F));
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 100F)); tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 100F));
tableLayoutPanel2.RowStyles.Add(new RowStyle(SizeType.Absolute, 50F)); tableLayoutPanel2.Size = new Size(1116, 304);
tableLayoutPanel2.Size = new Size(1116, 346); tableLayoutPanel2.TabIndex = 1;
tableLayoutPanel2.TabIndex = 0;
// //
// labelName // labelName
// //
@ -138,7 +133,7 @@
labelCompany.Location = new Point(3, 50); labelCompany.Location = new Point(3, 50);
labelCompany.Name = "labelCompany"; labelCompany.Name = "labelCompany";
labelCompany.Size = new Size(144, 50); labelCompany.Size = new Size(144, 50);
labelCompany.TabIndex = 1; labelCompany.TabIndex = 0;
labelCompany.Text = "Company"; labelCompany.Text = "Company";
// //
// labelEmail // labelEmail
@ -148,7 +143,7 @@
labelEmail.Location = new Point(3, 100); labelEmail.Location = new Point(3, 100);
labelEmail.Name = "labelEmail"; labelEmail.Name = "labelEmail";
labelEmail.Size = new Size(144, 50); labelEmail.Size = new Size(144, 50);
labelEmail.TabIndex = 2; labelEmail.TabIndex = 0;
labelEmail.Text = "Email *"; labelEmail.Text = "Email *";
// //
// labelPhone // labelPhone
@ -158,7 +153,7 @@
labelPhone.Location = new Point(3, 150); labelPhone.Location = new Point(3, 150);
labelPhone.Name = "labelPhone"; labelPhone.Name = "labelPhone";
labelPhone.Size = new Size(144, 50); labelPhone.Size = new Size(144, 50);
labelPhone.TabIndex = 3; labelPhone.TabIndex = 0;
labelPhone.Text = "Phone"; labelPhone.Text = "Phone";
// //
// labelAddress // labelAddress
@ -167,28 +162,18 @@
labelAddress.Dock = DockStyle.Fill; labelAddress.Dock = DockStyle.Fill;
labelAddress.Location = new Point(3, 200); labelAddress.Location = new Point(3, 200);
labelAddress.Name = "labelAddress"; labelAddress.Name = "labelAddress";
labelAddress.Size = new Size(144, 100); labelAddress.Size = new Size(144, 104);
labelAddress.TabIndex = 4; labelAddress.TabIndex = 0;
labelAddress.Text = "Address"; labelAddress.Text = "Address";
// //
// labelIsActive
//
labelIsActive.AutoSize = true;
labelIsActive.Dock = DockStyle.Fill;
labelIsActive.Location = new Point(3, 300);
labelIsActive.Name = "labelIsActive";
labelIsActive.Size = new Size(144, 50);
labelIsActive.TabIndex = 5;
labelIsActive.Text = "Is Active?";
//
// textBoxName // textBoxName
// //
textBoxName.Dock = DockStyle.Fill; textBoxName.Dock = DockStyle.Left;
textBoxName.Location = new Point(153, 3); textBoxName.Location = new Point(153, 3);
textBoxName.Name = "textBoxName"; textBoxName.Name = "textBoxName";
textBoxName.PlaceholderText = "Nancy Thompson"; textBoxName.PlaceholderText = "Nancy Thompson";
textBoxName.Size = new Size(960, 39); textBoxName.Size = new Size(916, 39);
textBoxName.TabIndex = 6; textBoxName.TabIndex = 1;
textBoxName.Validating += textBoxName_Validating; textBoxName.Validating += textBoxName_Validating;
// //
// textBoxCompany // textBoxCompany
@ -196,25 +181,19 @@
textBoxCompany.Dock = DockStyle.Fill; textBoxCompany.Dock = DockStyle.Fill;
textBoxCompany.Location = new Point(153, 53); textBoxCompany.Location = new Point(153, 53);
textBoxCompany.Name = "textBoxCompany"; textBoxCompany.Name = "textBoxCompany";
textBoxCompany.PlaceholderText = "Acme Corporation";
textBoxCompany.Size = new Size(960, 39); textBoxCompany.Size = new Size(960, 39);
textBoxCompany.TabIndex = 7; textBoxCompany.TabIndex = 2;
// //
// textBoxEmail // textBoxEmail
// //
textBoxEmail.Dock = DockStyle.Fill; textBoxEmail.Dock = DockStyle.Left;
textBoxEmail.Location = new Point(153, 103); textBoxEmail.Location = new Point(153, 103);
textBoxEmail.Name = "textBoxEmail"; textBoxEmail.Name = "textBoxEmail";
textBoxEmail.Size = new Size(960, 39); textBoxEmail.PlaceholderText = "username@domain.com";
textBoxEmail.TabIndex = 8; textBoxEmail.Size = new Size(916, 39);
// textBoxEmail.TabIndex = 3;
// comboBoxIsActive textBoxEmail.Validating += textBoxEmail_Validating;
//
comboBoxIsActive.FormattingEnabled = true;
comboBoxIsActive.Items.AddRange(new object[] { "True", "False" });
comboBoxIsActive.Location = new Point(153, 303);
comboBoxIsActive.Name = "comboBoxIsActive";
comboBoxIsActive.Size = new Size(147, 40);
comboBoxIsActive.TabIndex = 11;
// //
// maskedTextBox_Phone // maskedTextBox_Phone
// //
@ -223,7 +202,7 @@
maskedTextBox_Phone.Mask = "(999) 000-0000"; maskedTextBox_Phone.Mask = "(999) 000-0000";
maskedTextBox_Phone.Name = "maskedTextBox_Phone"; maskedTextBox_Phone.Name = "maskedTextBox_Phone";
maskedTextBox_Phone.Size = new Size(960, 39); maskedTextBox_Phone.Size = new Size(960, 39);
maskedTextBox_Phone.TabIndex = 12; maskedTextBox_Phone.TabIndex = 4;
// //
// tableLayoutPanel4 // tableLayoutPanel4
// //
@ -237,8 +216,8 @@
tableLayoutPanel4.RowCount = 2; tableLayoutPanel4.RowCount = 2;
tableLayoutPanel4.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); tableLayoutPanel4.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
tableLayoutPanel4.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); tableLayoutPanel4.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
tableLayoutPanel4.Size = new Size(960, 94); tableLayoutPanel4.Size = new Size(960, 98);
tableLayoutPanel4.TabIndex = 13; tableLayoutPanel4.TabIndex = 5;
// //
// tableLayoutPanel5 // tableLayoutPanel5
// //
@ -248,14 +227,14 @@
tableLayoutPanel5.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 15F)); tableLayoutPanel5.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 15F));
tableLayoutPanel5.Controls.Add(textBoxAddressCity, 0, 0); tableLayoutPanel5.Controls.Add(textBoxAddressCity, 0, 0);
tableLayoutPanel5.Controls.Add(comboBoxAddressState, 1, 0); tableLayoutPanel5.Controls.Add(comboBoxAddressState, 1, 0);
tableLayoutPanel5.Controls.Add(maskedTextBoxAddressZipcode, 2, 0); tableLayoutPanel5.Controls.Add(maskedTextBoxAddressPostal, 2, 0);
tableLayoutPanel5.Dock = DockStyle.Fill; tableLayoutPanel5.Dock = DockStyle.Fill;
tableLayoutPanel5.Location = new Point(3, 50); tableLayoutPanel5.Location = new Point(3, 52);
tableLayoutPanel5.Name = "tableLayoutPanel5"; tableLayoutPanel5.Name = "tableLayoutPanel5";
tableLayoutPanel5.RowCount = 1; tableLayoutPanel5.RowCount = 1;
tableLayoutPanel5.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); tableLayoutPanel5.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel5.Size = new Size(954, 41); tableLayoutPanel5.Size = new Size(954, 43);
tableLayoutPanel5.TabIndex = 0; tableLayoutPanel5.TabIndex = 6;
// //
// textBoxAddressCity // textBoxAddressCity
// //
@ -264,7 +243,7 @@
textBoxAddressCity.Name = "textBoxAddressCity"; textBoxAddressCity.Name = "textBoxAddressCity";
textBoxAddressCity.PlaceholderText = "Springwood"; textBoxAddressCity.PlaceholderText = "Springwood";
textBoxAddressCity.Size = new Size(661, 39); textBoxAddressCity.Size = new Size(661, 39);
textBoxAddressCity.TabIndex = 0; textBoxAddressCity.TabIndex = 1;
// //
// comboBoxAddressState // comboBoxAddressState
// //
@ -273,16 +252,16 @@
comboBoxAddressState.Location = new Point(670, 3); comboBoxAddressState.Location = new Point(670, 3);
comboBoxAddressState.Name = "comboBoxAddressState"; comboBoxAddressState.Name = "comboBoxAddressState";
comboBoxAddressState.Size = new Size(137, 40); comboBoxAddressState.Size = new Size(137, 40);
comboBoxAddressState.TabIndex = 1; comboBoxAddressState.TabIndex = 2;
// //
// maskedTextBoxAddressZipcode // maskedTextBoxAddressPostal
// //
maskedTextBoxAddressZipcode.Dock = DockStyle.Fill; maskedTextBoxAddressPostal.Dock = DockStyle.Fill;
maskedTextBoxAddressZipcode.Location = new Point(813, 3); maskedTextBoxAddressPostal.Location = new Point(813, 3);
maskedTextBoxAddressZipcode.Mask = "00000"; maskedTextBoxAddressPostal.Mask = "00000";
maskedTextBoxAddressZipcode.Name = "maskedTextBoxAddressZipcode"; maskedTextBoxAddressPostal.Name = "maskedTextBoxAddressPostal";
maskedTextBoxAddressZipcode.Size = new Size(138, 39); maskedTextBoxAddressPostal.Size = new Size(138, 39);
maskedTextBoxAddressZipcode.TabIndex = 2; maskedTextBoxAddressPostal.TabIndex = 3;
// //
// textBoxAddressStreet // textBoxAddressStreet
// //
@ -297,10 +276,10 @@
// //
groupBoxNotes.Controls.Add(richTextBoxNotes); groupBoxNotes.Controls.Add(richTextBoxNotes);
groupBoxNotes.Dock = DockStyle.Fill; groupBoxNotes.Dock = DockStyle.Fill;
groupBoxNotes.Location = new Point(3, 355); groupBoxNotes.Location = new Point(3, 313);
groupBoxNotes.Name = "groupBoxNotes"; groupBoxNotes.Name = "groupBoxNotes";
groupBoxNotes.Size = new Size(1116, 194); groupBoxNotes.Size = new Size(1116, 236);
groupBoxNotes.TabIndex = 1; groupBoxNotes.TabIndex = 0;
groupBoxNotes.TabStop = false; groupBoxNotes.TabStop = false;
groupBoxNotes.Text = "Notes"; groupBoxNotes.Text = "Notes";
// //
@ -309,8 +288,8 @@
richTextBoxNotes.Dock = DockStyle.Fill; richTextBoxNotes.Dock = DockStyle.Fill;
richTextBoxNotes.Location = new Point(3, 35); richTextBoxNotes.Location = new Point(3, 35);
richTextBoxNotes.Name = "richTextBoxNotes"; richTextBoxNotes.Name = "richTextBoxNotes";
richTextBoxNotes.Size = new Size(1110, 156); richTextBoxNotes.Size = new Size(1110, 198);
richTextBoxNotes.TabIndex = 0; richTextBoxNotes.TabIndex = 9;
richTextBoxNotes.Text = ""; richTextBoxNotes.Text = "";
// //
// tableLayoutPanel3 // tableLayoutPanel3
@ -337,7 +316,7 @@
buttonOkay.Margin = new Padding(3, 3, 3, 15); buttonOkay.Margin = new Padding(3, 3, 3, 15);
buttonOkay.Name = "buttonOkay"; buttonOkay.Name = "buttonOkay";
buttonOkay.Size = new Size(194, 57); buttonOkay.Size = new Size(194, 57);
buttonOkay.TabIndex = 0; buttonOkay.TabIndex = 10;
buttonOkay.Text = "Okay"; buttonOkay.Text = "Okay";
buttonOkay.UseVisualStyleBackColor = true; buttonOkay.UseVisualStyleBackColor = true;
// //
@ -348,7 +327,8 @@
buttonCancel.Margin = new Padding(3, 3, 3, 15); buttonCancel.Margin = new Padding(3, 3, 3, 15);
buttonCancel.Name = "buttonCancel"; buttonCancel.Name = "buttonCancel";
buttonCancel.Size = new Size(194, 57); buttonCancel.Size = new Size(194, 57);
buttonCancel.TabIndex = 1; buttonCancel.TabIndex = 99;
buttonCancel.TabStop = false;
buttonCancel.Text = "Cancel"; buttonCancel.Text = "Cancel";
buttonCancel.UseVisualStyleBackColor = true; buttonCancel.UseVisualStyleBackColor = true;
// //
@ -369,7 +349,6 @@
ClientSize = new Size(1128, 665); ClientSize = new Size(1128, 665);
Controls.Add(groupBoxNewClient); Controls.Add(groupBoxNewClient);
Name = "ClientForm"; Name = "ClientForm";
Text = "Client";
groupBoxNewClient.ResumeLayout(false); groupBoxNewClient.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false); tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel2.ResumeLayout(false); tableLayoutPanel2.ResumeLayout(false);
@ -394,12 +373,10 @@
private Label labelEmail; private Label labelEmail;
private Label labelPhone; private Label labelPhone;
private Label labelAddress; private Label labelAddress;
private Label labelIsActive;
private GroupBox groupBoxNotes; private GroupBox groupBoxNotes;
private TextBox textBoxName; private TextBox textBoxName;
private TextBox textBoxCompany; private TextBox textBoxCompany;
private TextBox textBoxEmail; private TextBox textBoxEmail;
private ComboBox comboBoxIsActive;
private RichTextBox richTextBoxNotes; private RichTextBox richTextBoxNotes;
private TableLayoutPanel tableLayoutPanel3; private TableLayoutPanel tableLayoutPanel3;
private Button buttonOkay; private Button buttonOkay;
@ -410,7 +387,7 @@
private TableLayoutPanel tableLayoutPanel5; private TableLayoutPanel tableLayoutPanel5;
private TextBox textBoxAddressCity; private TextBox textBoxAddressCity;
private ComboBox comboBoxAddressState; private ComboBox comboBoxAddressState;
private MaskedTextBox maskedTextBoxAddressZipcode; private MaskedTextBox maskedTextBoxAddressPostal;
private TextBox textBoxAddressStreet; private TextBox textBoxAddressStreet;
} }
} }

View File

@ -1,36 +1,76 @@
using System.Reflection.Metadata.Ecma335; using trakker.Models;
using trakker.Models;
namespace trakker.Forms namespace trakker.Forms
{ {
/// <summary>
/// Form used to view and edit a <see cref="Client"/> model. Fields on the form
/// are data-bound to the provided client instance and basic validation is
/// performed on required fields.
/// </summary>
public partial class ClientForm : Form public partial class ClientForm : Form
{ {
/// <summary>
/// The client instance being edited by this form.
/// </summary>
private readonly Client _client; private readonly Client _client;
/// <summary>
/// Binding source that connects the client model to the form controls.
/// </summary>
private BindingSource bindingSource = new BindingSource(); private BindingSource bindingSource = new BindingSource();
/// <summary>
/// Error provider used to display validation errors next to input controls.
/// </summary>
private ErrorProvider errorProvider = new ErrorProvider(); private ErrorProvider errorProvider = new ErrorProvider();
/// <summary>
/// Initializes a new instance of the <see cref="ClientForm"/> class bound to
/// the provided <paramref name="client"/>. Sets up data bindings for all
/// visible input controls and configures dialog button behavior.
/// </summary>
/// <param name="client">The <see cref="Client"/> instance to edit. Must not be null.</param>
public ClientForm(Client client) public ClientForm(Client client)
{ {
_client = client; _client = client;
InitializeComponent(); InitializeComponent();
// Bind model properties to controls so the UI reflects and updates the model.
bindingSource.DataSource = _client; bindingSource.DataSource = _client;
textBoxName.DataBindings.Add("Text", bindingSource, "Name", true); textBoxName.DataBindings.Add("Text", bindingSource, "Name", true);
textBoxCompany.DataBindings.Add("Text", bindingSource, "Company", true); textBoxCompany.DataBindings.Add("Text", bindingSource, "Company", true);
textBoxEmail.DataBindings.Add("Text", bindingSource, "Email", true); textBoxEmail.DataBindings.Add("Text", bindingSource, "Email", true);
maskedTextBox_Phone.DataBindings.Add("Text", bindingSource, "Phone", true); maskedTextBox_Phone.DataBindings.Add("Text", bindingSource, "Phone", true);
//richTextBoxAddress.DataBindings.Add("Text", bindingSource, "Address", true); textBoxAddressStreet.DataBindings.Add("Text", bindingSource, "AddressStreet", true);
comboBoxIsActive.DataBindings.Add("Text", bindingSource, "IsActive", true); textBoxAddressCity.DataBindings.Add("Text", bindingSource, "AddressCity", true);
comboBoxAddressState.DataBindings.Add("Text", bindingSource, "AddressState", true);
maskedTextBoxAddressPostal.DataBindings.Add("Text", bindingSource, "AddressPostal", true);
richTextBoxNotes.DataBindings.Add("Text", bindingSource, "Notes", true); richTextBoxNotes.DataBindings.Add("Text", bindingSource, "Notes", true);
// Configure dialog buttons and window behavior.
buttonOkay.DialogResult = DialogResult.OK;
buttonCancel.DialogResult = DialogResult.Cancel;
this.CancelButton = CancelButton;
this.StartPosition = FormStartPosition.CenterParent;
} }
/// <summary>
/// Gets the <see cref="Client"/> instance edited by the form.
/// </summary>
public Client Client { get => _client; private set { } } public Client Client { get => _client; 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 textBoxName_Validating(object sender, System.ComponentModel.CancelEventArgs e) private void textBoxName_Validating(object sender, System.ComponentModel.CancelEventArgs e)
{ {
if (string.IsNullOrWhiteSpace(textBoxName.Text)) if (string.IsNullOrWhiteSpace(textBoxName.Text))
{ {
errorProvider.SetError(textBoxName, "Name is required."); errorProvider.SetError(textBoxName, "Name is required.");
errorProvider.SetIconAlignment(textBoxName, ErrorIconAlignment.MiddleRight);
errorProvider.SetIconPadding(textBoxName, 2);
e.Cancel = true; e.Cancel = true;
} }
else else
@ -38,5 +78,25 @@ namespace trakker.Forms
errorProvider.SetError(textBoxName, ""); errorProvider.SetError(textBoxName, "");
} }
} }
/// <summary>
/// Validates the Email field. If the email is empty or whitespace, an error is set
/// on the <see cref="errorProvider"/> and the event is canceled to prevent the
/// form from closing. Note: this validation only checks presence, not format.
/// </summary>
private void textBoxEmail_Validating(object sender, System.ComponentModel.CancelEventArgs e)
{
if (string.IsNullOrWhiteSpace(textBoxEmail.Text))
{
errorProvider.SetError(textBoxEmail, "Email is required.");
errorProvider.SetIconAlignment(textBoxEmail, ErrorIconAlignment.MiddleRight);
errorProvider.SetIconPadding(textBoxEmail, 2);
e.Cancel = true;
}
else
{
errorProvider.SetError(textBoxEmail, "");
}
}
} }
} }

View File

@ -32,13 +32,18 @@
fileToolStripMenuItem = new ToolStripMenuItem(); fileToolStripMenuItem = new ToolStripMenuItem();
MainForm_Exit_MenuItem = new ToolStripMenuItem(); MainForm_Exit_MenuItem = new ToolStripMenuItem();
MainForm_StatusStrip = new StatusStrip(); MainForm_StatusStrip = new StatusStrip();
MainForm_TabControl = new TabControl(); tabControlMainForm = new TabControl();
MainForm_TabPage1 = new TabPage(); MainForm_TabPage1 = new TabPage();
MainForm_TabPage2 = new TabPage();
button1 = new Button(); button1 = new Button();
MainForm_TabPage2 = new TabPage();
tableLayoutPanel1Tab2 = new TableLayoutPanel();
dataGridViewClients = new DataGridView();
MainForm_MenuStrip.SuspendLayout(); MainForm_MenuStrip.SuspendLayout();
MainForm_TabControl.SuspendLayout(); tabControlMainForm.SuspendLayout();
MainForm_TabPage1.SuspendLayout(); MainForm_TabPage1.SuspendLayout();
MainForm_TabPage2.SuspendLayout();
tableLayoutPanel1Tab2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewClients).BeginInit();
SuspendLayout(); SuspendLayout();
// //
// MainForm_MenuStrip // MainForm_MenuStrip
@ -47,7 +52,7 @@
MainForm_MenuStrip.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem }); MainForm_MenuStrip.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem });
MainForm_MenuStrip.Location = new Point(0, 0); MainForm_MenuStrip.Location = new Point(0, 0);
MainForm_MenuStrip.Name = "MainForm_MenuStrip"; MainForm_MenuStrip.Name = "MainForm_MenuStrip";
MainForm_MenuStrip.Size = new Size(1696, 40); MainForm_MenuStrip.Size = new Size(1343, 40);
MainForm_MenuStrip.TabIndex = 0; MainForm_MenuStrip.TabIndex = 0;
MainForm_MenuStrip.Text = "menuStrip1"; MainForm_MenuStrip.Text = "menuStrip1";
// //
@ -68,22 +73,22 @@
// MainForm_StatusStrip // MainForm_StatusStrip
// //
MainForm_StatusStrip.ImageScalingSize = new Size(32, 32); MainForm_StatusStrip.ImageScalingSize = new Size(32, 32);
MainForm_StatusStrip.Location = new Point(0, 1074); MainForm_StatusStrip.Location = new Point(0, 983);
MainForm_StatusStrip.Name = "MainForm_StatusStrip"; MainForm_StatusStrip.Name = "MainForm_StatusStrip";
MainForm_StatusStrip.Size = new Size(1696, 22); MainForm_StatusStrip.Size = new Size(1343, 22);
MainForm_StatusStrip.TabIndex = 1; MainForm_StatusStrip.TabIndex = 1;
MainForm_StatusStrip.Text = "MainForm_StatusStrip"; MainForm_StatusStrip.Text = "MainForm_StatusStrip";
// //
// MainForm_TabControl // tabControlMainForm
// //
MainForm_TabControl.Controls.Add(MainForm_TabPage1); tabControlMainForm.Controls.Add(MainForm_TabPage1);
MainForm_TabControl.Controls.Add(MainForm_TabPage2); tabControlMainForm.Controls.Add(MainForm_TabPage2);
MainForm_TabControl.Dock = DockStyle.Fill; tabControlMainForm.Dock = DockStyle.Fill;
MainForm_TabControl.Location = new Point(0, 40); tabControlMainForm.Location = new Point(0, 40);
MainForm_TabControl.Name = "MainForm_TabControl"; tabControlMainForm.Name = "tabControlMainForm";
MainForm_TabControl.SelectedIndex = 0; tabControlMainForm.SelectedIndex = 0;
MainForm_TabControl.Size = new Size(1696, 1034); tabControlMainForm.Size = new Size(1343, 943);
MainForm_TabControl.TabIndex = 2; tabControlMainForm.TabIndex = 2;
// //
// MainForm_TabPage1 // MainForm_TabPage1
// //
@ -91,21 +96,11 @@
MainForm_TabPage1.Location = new Point(8, 46); MainForm_TabPage1.Location = new Point(8, 46);
MainForm_TabPage1.Name = "MainForm_TabPage1"; MainForm_TabPage1.Name = "MainForm_TabPage1";
MainForm_TabPage1.Padding = new Padding(3); MainForm_TabPage1.Padding = new Padding(3);
MainForm_TabPage1.Size = new Size(1680, 980); MainForm_TabPage1.Size = new Size(1327, 889);
MainForm_TabPage1.TabIndex = 0; MainForm_TabPage1.TabIndex = 0;
MainForm_TabPage1.Text = "Tab 1"; MainForm_TabPage1.Text = "Tab 1";
MainForm_TabPage1.UseVisualStyleBackColor = true; MainForm_TabPage1.UseVisualStyleBackColor = true;
// //
// MainForm_TabPage2
//
MainForm_TabPage2.Location = new Point(8, 46);
MainForm_TabPage2.Name = "MainForm_TabPage2";
MainForm_TabPage2.Padding = new Padding(3);
MainForm_TabPage2.Size = new Size(1680, 1015);
MainForm_TabPage2.TabIndex = 1;
MainForm_TabPage2.Text = "Tab 2";
MainForm_TabPage2.UseVisualStyleBackColor = true;
//
// button1 // button1
// //
button1.Location = new Point(39, 47); button1.Location = new Point(39, 47);
@ -116,12 +111,51 @@
button1.UseVisualStyleBackColor = true; button1.UseVisualStyleBackColor = true;
button1.Click += button1_Click; button1.Click += button1_Click;
// //
// MainForm_TabPage2
//
MainForm_TabPage2.Controls.Add(tableLayoutPanel1Tab2);
MainForm_TabPage2.Location = new Point(8, 46);
MainForm_TabPage2.Name = "MainForm_TabPage2";
MainForm_TabPage2.Padding = new Padding(3);
MainForm_TabPage2.Size = new Size(1327, 889);
MainForm_TabPage2.TabIndex = 1;
MainForm_TabPage2.Text = "Tab 2";
MainForm_TabPage2.UseVisualStyleBackColor = true;
//
// tableLayoutPanel1Tab2
//
tableLayoutPanel1Tab2.ColumnCount = 1;
tableLayoutPanel1Tab2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1Tab2.Controls.Add(dataGridViewClients, 0, 1);
tableLayoutPanel1Tab2.Dock = DockStyle.Fill;
tableLayoutPanel1Tab2.Location = new Point(3, 3);
tableLayoutPanel1Tab2.Name = "tableLayoutPanel1Tab2";
tableLayoutPanel1Tab2.RowCount = 3;
tableLayoutPanel1Tab2.RowStyles.Add(new RowStyle(SizeType.Absolute, 1F));
tableLayoutPanel1Tab2.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1Tab2.RowStyles.Add(new RowStyle(SizeType.Absolute, 200F));
tableLayoutPanel1Tab2.Size = new Size(1321, 883);
tableLayoutPanel1Tab2.TabIndex = 0;
//
// dataGridViewClients
//
dataGridViewClients.AllowUserToAddRows = false;
dataGridViewClients.AllowUserToDeleteRows = false;
dataGridViewClients.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridViewClients.Dock = DockStyle.Fill;
dataGridViewClients.Location = new Point(3, 4);
dataGridViewClients.Name = "dataGridViewClients";
dataGridViewClients.ReadOnly = true;
dataGridViewClients.RowHeadersWidth = 82;
dataGridViewClients.Size = new Size(1315, 676);
dataGridViewClients.TabIndex = 0;
//
// MainForm // MainForm
// //
AutoScaleDimensions = new SizeF(13F, 32F); AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1696, 1096); ClientSize = new Size(1343, 1005);
Controls.Add(MainForm_TabControl); Controls.Add(tabControlMainForm);
Controls.Add(MainForm_StatusStrip); Controls.Add(MainForm_StatusStrip);
Controls.Add(MainForm_MenuStrip); Controls.Add(MainForm_MenuStrip);
MainMenuStrip = MainForm_MenuStrip; MainMenuStrip = MainForm_MenuStrip;
@ -129,8 +163,11 @@
Text = "MainForm"; Text = "MainForm";
MainForm_MenuStrip.ResumeLayout(false); MainForm_MenuStrip.ResumeLayout(false);
MainForm_MenuStrip.PerformLayout(); MainForm_MenuStrip.PerformLayout();
MainForm_TabControl.ResumeLayout(false); tabControlMainForm.ResumeLayout(false);
MainForm_TabPage1.ResumeLayout(false); MainForm_TabPage1.ResumeLayout(false);
MainForm_TabPage2.ResumeLayout(false);
tableLayoutPanel1Tab2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dataGridViewClients).EndInit();
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
@ -139,11 +176,13 @@
private MenuStrip MainForm_MenuStrip; private MenuStrip MainForm_MenuStrip;
private StatusStrip MainForm_StatusStrip; private StatusStrip MainForm_StatusStrip;
private TabControl MainForm_TabControl; private TabControl tabControlMainForm;
private TabPage MainForm_TabPage1; private TabPage MainForm_TabPage1;
private TabPage MainForm_TabPage2; private TabPage MainForm_TabPage2;
private ToolStripMenuItem fileToolStripMenuItem; private ToolStripMenuItem fileToolStripMenuItem;
private ToolStripMenuItem MainForm_Exit_MenuItem; private ToolStripMenuItem MainForm_Exit_MenuItem;
private Button button1; private Button button1;
private TableLayoutPanel tableLayoutPanel1Tab2;
private DataGridView dataGridViewClients;
} }
} }

View File

@ -1,10 +1,19 @@
using Microsoft.Data.Sqlite;
using System.ComponentModel;
using trakker.Data;
using trakker.Forms; using trakker.Forms;
using trakker.Interfaces;
using trakker.Models; using trakker.Models;
using trakker.Services;
namespace trakker namespace trakker
{ {
public partial class MainForm : Form public partial class MainForm : Form, IMainForm
{ {
//private readonly string _dbversion = "[N.N.N]";
private string connectionString = string.Empty;
readonly MainCtrl _ctrl;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MainForm"/> class. /// Initializes a new instance of the <see cref="MainForm"/> class.
@ -13,8 +22,22 @@ namespace trakker
/// </summary> /// </summary>
public MainForm() public MainForm()
{ {
InitializeComponent(); InitializeComponent();
// build connection string that will be used for database connections
// ------------------------------------------------------------------------
var dbPath = Path.Combine(AppContext.BaseDirectory, "trakker.db");
connectionString = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared
}.ToString();
tabControlMainForm.TabPages[0].Text = " Home ";
tabControlMainForm.TabPages[1].Text = " Clients ";
_ctrl = new Services.MainCtrl(this, connectionString);
} }
/// <summary> /// <summary>
@ -28,14 +51,118 @@ namespace trakker
Application.Exit(); Application.Exit();
} }
public void InitDataGridViewClients(BindingList<Client> clients)
{
dataGridViewClients.AllowUserToAddRows = true;
dataGridViewClients.AllowUserToDeleteRows = true;
dataGridViewClients.AutoGenerateColumns = false;
dataGridViewClients.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridViewClients.BackgroundColor = Color.White;
dataGridViewClients.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridViewClients.RowHeadersVisible = false;
dataGridViewClients.ColumnHeadersVisible = true;
dataGridViewClients.MultiSelect = false;
dataGridViewClients.DataSource = clients;
dataGridViewClients.Columns.Clear();
{
var textColumn = new DataGridViewTextBoxColumn
{
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
DataPropertyName = "Name",
Name = "Name",
Visible = true,
};
dataGridViewClients.Columns.Add(textColumn);
}
{
var textColumn = new DataGridViewTextBoxColumn
{
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
DataPropertyName = "Company",
Name = "Company",
Visible = true,
};
dataGridViewClients.Columns.Add(textColumn);
}
{
var textColumn = new DataGridViewTextBoxColumn
{
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
DataPropertyName = "Email",
Name = "Email",
Visible = true,
};
dataGridViewClients.Columns.Add(textColumn);
}
{
var textColumn = new DataGridViewTextBoxColumn
{
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
DataPropertyName = "Phone",
Name = "Phone",
Visible = true,
};
dataGridViewClients.Columns.Add(textColumn);
}
dataGridViewClients.DoubleClick += (s, e) =>
{
if (dataGridViewClients.SelectedRows.Count > 0)
{
var selectedClient = dataGridViewClients.SelectedRows[0].DataBoundItem as Client;
if (selectedClient != null)
{
var dialog = new ClientForm(selectedClient);
if (dialog.ShowDialog(this) == DialogResult.OK)
{
Client client = dialog.Client;
ClientData clientData = new ClientData(connectionString);
try
{
clientData.Upsert(client);
dataGridViewClients.Refresh(); // Refresh the DataGridView to reflect changes
}
catch (Exception ex)
{
MessageBox.Show($"Error saving client: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
};
dataGridViewClients.SelectionChanged += (s, e) =>
{
if (dataGridViewClients.SelectedRows.Count > 0)
{
var selectedClient = dataGridViewClients.SelectedRows[0].DataBoundItem as Client;
if (selectedClient != null)
{
// Handle the selected client as needed
// MessageBox.Show($"Selected Client: {selectedClient.AddressStreet}", "Client Selected", MessageBoxButtons.OK, MessageBoxIcon.Information );
}
}
};
}
private void button1_Click(object sender, EventArgs e) private void button1_Click(object sender, EventArgs e)
{ {
Client client = new Client(); var dialog = new ClientForm(new Client());
client.ClientId = Guid.NewGuid().ToString(); if (dialog.ShowDialog(this) == DialogResult.OK)
var dialog = new ClientForm(client); {
dialog.ShowDialog(this); Client client = dialog.Client;
client = dialog.Client; ClientData clientData = new ClientData(connectionString);
MessageBox.Show($"Client Name: {client.Name}\nCompany: {client.Company}\nEmail: {client.Email}\nPhone: {client.Phone}\nAddress: {client.Address}\nIs Active: {client.IsActive}\nNotes: {client.Notes}"); try
{
clientData.Upsert(client);
}
catch (Exception ex)
{
MessageBox.Show($"Error saving client: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
} }
} }
} }

15
Interfaces/IMainForm.cs Normal file
View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using trakker.Models;
namespace trakker.Interfaces
{
internal interface IMainForm
{
void InitDataGridViewClients(BindingList<Client> clients);
}
}

View File

@ -3,39 +3,104 @@ using System.ComponentModel.DataAnnotations;
namespace trakker.Models namespace trakker.Models
{ {
/// <summary>
/// Represents a client record in the application.
/// Contains contact information, address fields, and audit timestamps.
/// Validation attributes decorate properties to enforce basic constraints.
/// </summary>
public class Client public class Client
{ {
public Client()
{
this.ClientId = Guid.NewGuid().ToString();
this.IsActive = "y";
}
/// <summary>
/// Primary identifier for the client. This maps to the database <c>client_id</c>.
/// Marked with <see cref="Key"/> to indicate the primary key.
/// </summary>
[Key] [Key]
public string ClientId { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty;
/// <summary>
/// The client's full name. This field is required and has a maximum length of 200 characters.
/// </summary>
[Required] [Required]
[MaxLength(200)] [MaxLength(200)]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary>
/// Optional company name associated with the client. Maximum length 200 characters.
/// </summary>
[MaxLength(200)] [MaxLength(200)]
public string? Company { get; set; } public string? Company { get; set; } = string.Empty;
/// <summary>
/// Optional email address for the client. Validated with <see cref="EmailAddressAttribute"/> and
/// constrained to 255 characters.
/// </summary>
[EmailAddress] [EmailAddress]
[MaxLength(255)] [MaxLength(255)]
public string? Email { get; set; } public string? Email { get; set; } = string.Empty;
/// <summary>
/// Optional phone number for the client. Validated with <see cref="PhoneAttribute"/> and
/// constrained to 50 characters.
/// </summary>
[Phone] [Phone]
[MaxLength(50)] [MaxLength(50)]
public string? Phone { get; set; } public string? Phone { get; set; } = string.Empty;
[MaxLength(500)] /// <summary>
public string? Address { get; set; } /// Street address for the client. Optional and limited to 100 characters.
/// </summary>
[MaxLength(100)]
public string? AddressStreet { get; set; } = string.Empty;
/// <summary>
/// City portion of the client's address. Optional and limited to 100 characters.
/// </summary>
[MaxLength(100)]
public string? AddressCity { get; set; } = string.Empty;
/// <summary>
/// State portion of the client's address. Stored as a 2-character code (e.g., US state abbreviation).
/// </summary>
[MaxLength(2)]
public string? AddressState { get; set; } = string.Empty;
/// <summary>
/// Postal code portion of the client's address. Limited to 5 characters in this model.
/// </summary>
[MaxLength(5)]
public string? AddressPostal { get; set; } = string.Empty;
/// <summary>
/// Free-form notes about the client. Optional and limited to 2000 characters.
/// </summary>
[MaxLength(2000)] [MaxLength(2000)]
public string? Notes { get; set; } public string? Notes { get; set; } = string.Empty;
public bool IsActive { get; set; } = true; /// <summary>
/// Indicates whether the client is active. Defaults to <c>true</c>.
/// </summary>
public string IsActive { get; set; } = "y";
/// <summary>
/// UTC timestamp for when the record was created. Defaults to <see cref="DateTime.UtcNow"/>.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// UTC timestamp for when the record was last updated. Updated by application code
/// when changes are made.
/// </summary>
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Optional: Helper method for updating timestamp /// <summary>
/// Updates the <see cref="UpdatedAt"/> timestamp to the current UTC time. Call this
/// before persisting changes to ensure the audit timestamp is accurate.
/// </summary>
public void UpdateTimestamp() public void UpdateTimestamp()
{ {
UpdatedAt = DateTime.UtcNow; UpdatedAt = DateTime.UtcNow;

View File

@ -1,3 +1,5 @@
using SQLitePCL;
namespace trakker namespace trakker
{ {
internal static class Program internal static class Program
@ -8,6 +10,9 @@ namespace trakker
[STAThread] [STAThread]
static void Main() static void Main()
{ {
// Initialize SQLite early
Batteries_V2.Init(); // ← Modern version (recommended)
// To customize application configuration such as set high DPI settings or default font, // To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration. // see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();

32
Services/MainCtrl.cs Normal file
View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using trakker.Models;
using trakker.Interfaces;
namespace trakker.Services
{
internal class MainCtrl
{
private readonly string _connectionString;
private readonly IMainForm _view;
public MainCtrl(IMainForm view, string? connectionString)
{
_view = view ?? throw new ArgumentNullException(nameof(view));
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
LoadClients();
}
internal void LoadClients()
{
// Implement logic to load clients from the database using _connectionString
var dbo = new Data.ClientData(_connectionString);
var clients = dbo.Get();
_view.InitDataGridViewClients(clients);
}
}
}

View File

@ -9,8 +9,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Interfaces\" /> <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.7" />
<Folder Include="Services\" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>