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

View File

@ -1,36 +1,76 @@
using System.Reflection.Metadata.Ecma335;
using trakker.Models;
using trakker.Models;
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
{
/// <summary>
/// The client instance being edited by this form.
/// </summary>
private readonly Client _client;
/// <summary>
/// Binding source that connects the client 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="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)
{
_client = client;
InitializeComponent();
// Bind model properties to controls so the UI reflects and updates the model.
bindingSource.DataSource = _client;
textBoxName.DataBindings.Add("Text", bindingSource, "Name", true);
textBoxCompany.DataBindings.Add("Text", bindingSource, "Company", true);
textBoxEmail.DataBindings.Add("Text", bindingSource, "Email", true);
maskedTextBox_Phone.DataBindings.Add("Text", bindingSource, "Phone", true);
//richTextBoxAddress.DataBindings.Add("Text", bindingSource, "Address", true);
comboBoxIsActive.DataBindings.Add("Text", bindingSource, "IsActive", true);
textBoxAddressStreet.DataBindings.Add("Text", bindingSource, "AddressStreet", 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);
// 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 { } }
/// <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)
{
if (string.IsNullOrWhiteSpace(textBoxName.Text))
{
errorProvider.SetError(textBoxName, "Name is required.");
errorProvider.SetIconAlignment(textBoxName, ErrorIconAlignment.MiddleRight);
errorProvider.SetIconPadding(textBoxName, 2);
e.Cancel = true;
}
else
@ -38,5 +78,25 @@ namespace trakker.Forms
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();
MainForm_Exit_MenuItem = new ToolStripMenuItem();
MainForm_StatusStrip = new StatusStrip();
MainForm_TabControl = new TabControl();
tabControlMainForm = new TabControl();
MainForm_TabPage1 = new TabPage();
MainForm_TabPage2 = new TabPage();
button1 = new Button();
MainForm_TabPage2 = new TabPage();
tableLayoutPanel1Tab2 = new TableLayoutPanel();
dataGridViewClients = new DataGridView();
MainForm_MenuStrip.SuspendLayout();
MainForm_TabControl.SuspendLayout();
tabControlMainForm.SuspendLayout();
MainForm_TabPage1.SuspendLayout();
MainForm_TabPage2.SuspendLayout();
tableLayoutPanel1Tab2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewClients).BeginInit();
SuspendLayout();
//
// MainForm_MenuStrip
@ -47,7 +52,7 @@
MainForm_MenuStrip.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem });
MainForm_MenuStrip.Location = new Point(0, 0);
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.Text = "menuStrip1";
//
@ -68,22 +73,22 @@
// MainForm_StatusStrip
//
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.Size = new Size(1696, 22);
MainForm_StatusStrip.Size = new Size(1343, 22);
MainForm_StatusStrip.TabIndex = 1;
MainForm_StatusStrip.Text = "MainForm_StatusStrip";
//
// MainForm_TabControl
// tabControlMainForm
//
MainForm_TabControl.Controls.Add(MainForm_TabPage1);
MainForm_TabControl.Controls.Add(MainForm_TabPage2);
MainForm_TabControl.Dock = DockStyle.Fill;
MainForm_TabControl.Location = new Point(0, 40);
MainForm_TabControl.Name = "MainForm_TabControl";
MainForm_TabControl.SelectedIndex = 0;
MainForm_TabControl.Size = new Size(1696, 1034);
MainForm_TabControl.TabIndex = 2;
tabControlMainForm.Controls.Add(MainForm_TabPage1);
tabControlMainForm.Controls.Add(MainForm_TabPage2);
tabControlMainForm.Dock = DockStyle.Fill;
tabControlMainForm.Location = new Point(0, 40);
tabControlMainForm.Name = "tabControlMainForm";
tabControlMainForm.SelectedIndex = 0;
tabControlMainForm.Size = new Size(1343, 943);
tabControlMainForm.TabIndex = 2;
//
// MainForm_TabPage1
//
@ -91,21 +96,11 @@
MainForm_TabPage1.Location = new Point(8, 46);
MainForm_TabPage1.Name = "MainForm_TabPage1";
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.Text = "Tab 1";
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.Location = new Point(39, 47);
@ -116,12 +111,51 @@
button1.UseVisualStyleBackColor = true;
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
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1696, 1096);
Controls.Add(MainForm_TabControl);
ClientSize = new Size(1343, 1005);
Controls.Add(tabControlMainForm);
Controls.Add(MainForm_StatusStrip);
Controls.Add(MainForm_MenuStrip);
MainMenuStrip = MainForm_MenuStrip;
@ -129,8 +163,11 @@
Text = "MainForm";
MainForm_MenuStrip.ResumeLayout(false);
MainForm_MenuStrip.PerformLayout();
MainForm_TabControl.ResumeLayout(false);
tabControlMainForm.ResumeLayout(false);
MainForm_TabPage1.ResumeLayout(false);
MainForm_TabPage2.ResumeLayout(false);
tableLayoutPanel1Tab2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dataGridViewClients).EndInit();
ResumeLayout(false);
PerformLayout();
}
@ -139,11 +176,13 @@
private MenuStrip MainForm_MenuStrip;
private StatusStrip MainForm_StatusStrip;
private TabControl MainForm_TabControl;
private TabControl tabControlMainForm;
private TabPage MainForm_TabPage1;
private TabPage MainForm_TabPage2;
private ToolStripMenuItem fileToolStripMenuItem;
private ToolStripMenuItem MainForm_Exit_MenuItem;
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.Interfaces;
using trakker.Models;
using trakker.Services;
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>
/// Initializes a new instance of the <see cref="MainForm"/> class.
@ -13,8 +22,22 @@ namespace trakker
/// </summary>
public MainForm()
{
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>
@ -28,14 +51,118 @@ namespace trakker
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)
{
Client client = new Client();
client.ClientId = Guid.NewGuid().ToString();
var dialog = new ClientForm(client);
dialog.ShowDialog(this);
client = dialog.Client;
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}");
var dialog = new ClientForm(new Client());
if (dialog.ShowDialog(this) == DialogResult.OK)
{
Client client = dialog.Client;
ClientData clientData = new ClientData(connectionString);
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
{
/// <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 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]
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]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Optional company name associated with the client. Maximum length 200 characters.
/// </summary>
[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]
[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]
[MaxLength(50)]
public string? Phone { get; set; }
public string? Phone { get; set; } = string.Empty;
[MaxLength(500)]
public string? Address { get; set; }
/// <summary>
/// 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)]
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;
/// <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;
// 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()
{
UpdatedAt = DateTime.UtcNow;

View File

@ -1,3 +1,5 @@
using SQLitePCL;
namespace trakker
{
internal static class Program
@ -8,6 +10,9 @@ namespace trakker
[STAThread]
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,
// see https://aka.ms/applicationconfiguration.
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>
<ItemGroup>
<Folder Include="Interfaces\" />
<Folder Include="Services\" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.7" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
</ItemGroup>
</Project>