diff --git a/Data/ClientData.cs b/Data/ClientData.cs
new file mode 100644
index 0000000..ffbc6a5
--- /dev/null
+++ b/Data/ClientData.cs
@@ -0,0 +1,268 @@
+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 clients. It inherits from which
+ /// provides connection management.
+ ///
+ internal class ClientData(string connectionString) : DataAccess(connectionString)
+ {
+
+ public BindingList Get(string? clientId = null)
+ {
+ var results = new BindingList();
+
+ 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;
+ }
+
+ ///
+ /// Inserts a new client 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 client_id already exists. Parameter names correspond to the
+ /// client model property names.
+ ///
+ 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;
+ }
+
+ }
+ ///
+ /// Deletes the client with the specified from the
+ /// database.
+ ///
+ /// The identifier of the client 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 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;
+ }
+ ///
+ /// Executes arbitrary, ad-hoc SQL against the database inside a transaction.
+ ///
+ /// A SQL statement to execute. The caller is responsible for
+ /// ensuring the SQL is safe and properly parameterized to avoid SQL injection.
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ }
+
+ }
+}
diff --git a/Data/DataAccess.cs b/Data/DataAccess.cs
new file mode 100644
index 0000000..2c02f52
--- /dev/null
+++ b/Data/DataAccess.cs
@@ -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));
+
+ ///
+ /// Creates a new, unopened SqliteConnection.
+ ///
+ protected SqliteConnection CreateConnection() => new(_connectionString);
+
+ ///
+ /// Opens and returns a SqliteConnection (synchronous).
+ ///
+ protected SqliteConnection OpenConnection()
+ {
+ var conn = CreateConnection();
+ conn.Open();
+ return conn;
+ }
+ }
+}
diff --git a/Forms/ClientForm.Designer.cs b/Forms/ClientForm.Designer.cs
index d5821cb..f95df90 100644
--- a/Forms/ClientForm.Designer.cs
+++ b/Forms/ClientForm.Designer.cs
@@ -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;
}
}
\ No newline at end of file
diff --git a/Forms/ClientForm.cs b/Forms/ClientForm.cs
index 812f16c..c242bef 100644
--- a/Forms/ClientForm.cs
+++ b/Forms/ClientForm.cs
@@ -1,36 +1,76 @@
-using System.Reflection.Metadata.Ecma335;
-using trakker.Models;
+using trakker.Models;
namespace trakker.Forms
{
+ ///
+ /// Form used to view and edit a model. Fields on the form
+ /// are data-bound to the provided client instance and basic validation is
+ /// performed on required fields.
+ ///
public partial class ClientForm : Form
{
+ ///
+ /// The client instance being edited by this form.
+ ///
private readonly Client _client;
+
+ ///
+ /// Binding source that connects the client model to the form controls.
+ ///
private BindingSource bindingSource = new BindingSource();
+
+ ///
+ /// Error provider used to display validation errors next to input controls.
+ ///
private ErrorProvider errorProvider = new ErrorProvider();
+ ///
+ /// Initializes a new instance of the class bound to
+ /// the provided . Sets up data bindings for all
+ /// visible input controls and configures dialog button behavior.
+ ///
+ /// The instance to edit. Must not be null.
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;
}
+
+ ///
+ /// Gets the instance edited by the form.
+ ///
public Client Client { get => _client; private set { } }
+ ///
+ /// Validates the Name field. If the name is empty or whitespace, an error is set
+ /// on the and the event is canceled to prevent the
+ /// form from closing.
+ ///
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, "");
}
}
+
+ ///
+ /// Validates the Email field. If the email is empty or whitespace, an error is set
+ /// on the and the event is canceled to prevent the
+ /// form from closing. Note: this validation only checks presence, not format.
+ ///
+ 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, "");
+ }
+ }
}
}
diff --git a/Forms/MainForm.Designer.cs b/Forms/MainForm.Designer.cs
index 1c59763..5e6deed 100644
--- a/Forms/MainForm.Designer.cs
+++ b/Forms/MainForm.Designer.cs
@@ -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;
}
}
diff --git a/Forms/MainForm.cs b/Forms/MainForm.cs
index b538478..b2f2ba0 100644
--- a/Forms/MainForm.cs
+++ b/Forms/MainForm.cs
@@ -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;
+
///
/// Initializes a new instance of the class.
@@ -13,8 +22,22 @@ namespace trakker
///
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);
}
///
@@ -28,14 +51,118 @@ namespace trakker
Application.Exit();
}
+ public void InitDataGridViewClients(BindingList 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);
+ }
+ }
}
}
}
diff --git a/Interfaces/IMainForm.cs b/Interfaces/IMainForm.cs
new file mode 100644
index 0000000..4f5c57c
--- /dev/null
+++ b/Interfaces/IMainForm.cs
@@ -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 clients);
+ }
+}
diff --git a/Models/Client.cs b/Models/Client.cs
index 747275d..a768e16 100644
--- a/Models/Client.cs
+++ b/Models/Client.cs
@@ -3,39 +3,104 @@ using System.ComponentModel.DataAnnotations;
namespace trakker.Models
{
+ ///
+ /// Represents a client record in the application.
+ /// Contains contact information, address fields, and audit timestamps.
+ /// Validation attributes decorate properties to enforce basic constraints.
+ ///
public class Client
{
+ public Client()
+ {
+ this.ClientId = Guid.NewGuid().ToString();
+ this.IsActive = "y";
+ }
+ ///
+ /// Primary identifier for the client. This maps to the database client_id.
+ /// Marked with to indicate the primary key.
+ ///
[Key]
public string ClientId { get; set; } = string.Empty;
+ ///
+ /// The client's full name. This field is required and has a maximum length of 200 characters.
+ ///
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
+ ///
+ /// Optional company name associated with the client. Maximum length 200 characters.
+ ///
[MaxLength(200)]
- public string? Company { get; set; }
+ public string? Company { get; set; } = string.Empty;
+ ///
+ /// Optional email address for the client. Validated with and
+ /// constrained to 255 characters.
+ ///
[EmailAddress]
[MaxLength(255)]
- public string? Email { get; set; }
+ public string? Email { get; set; } = string.Empty;
+ ///
+ /// Optional phone number for the client. Validated with and
+ /// constrained to 50 characters.
+ ///
[Phone]
[MaxLength(50)]
- public string? Phone { get; set; }
+ public string? Phone { get; set; } = string.Empty;
- [MaxLength(500)]
- public string? Address { get; set; }
+ ///
+ /// Street address for the client. Optional and limited to 100 characters.
+ ///
+ [MaxLength(100)]
+ public string? AddressStreet { get; set; } = string.Empty;
+ ///
+ /// City portion of the client's address. Optional and limited to 100 characters.
+ ///
+ [MaxLength(100)]
+ public string? AddressCity { get; set; } = string.Empty;
+
+ ///
+ /// State portion of the client's address. Stored as a 2-character code (e.g., US state abbreviation).
+ ///
+ [MaxLength(2)]
+ public string? AddressState { get; set; } = string.Empty;
+
+ ///
+ /// Postal code portion of the client's address. Limited to 5 characters in this model.
+ ///
+ [MaxLength(5)]
+ public string? AddressPostal { get; set; } = string.Empty;
+
+ ///
+ /// Free-form notes about the client. Optional and limited to 2000 characters.
+ ///
[MaxLength(2000)]
- public string? Notes { get; set; }
+ public string? Notes { get; set; } = string.Empty;
- public bool IsActive { get; set; } = true;
+ ///
+ /// Indicates whether the client is active. Defaults to true.
+ ///
+ public string IsActive { get; set; } = "y";
+ ///
+ /// UTC timestamp for when the record was created. Defaults to .
+ ///
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+ ///
+ /// UTC timestamp for when the record was last updated. Updated by application code
+ /// when changes are made.
+ ///
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
- // Optional: Helper method for updating timestamp
+ ///
+ /// Updates the timestamp to the current UTC time. Call this
+ /// before persisting changes to ensure the audit timestamp is accurate.
+ ///
public void UpdateTimestamp()
{
UpdatedAt = DateTime.UtcNow;
diff --git a/Program.cs b/Program.cs
index 8e4fbbb..44a90ab 100644
--- a/Program.cs
+++ b/Program.cs
@@ -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();
diff --git a/Services/MainCtrl.cs b/Services/MainCtrl.cs
new file mode 100644
index 0000000..01a672a
--- /dev/null
+++ b/Services/MainCtrl.cs
@@ -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);
+ }
+ }
+}
diff --git a/trakker.csproj b/trakker.csproj
index f718f2e..939822f 100644
--- a/trakker.csproj
+++ b/trakker.csproj
@@ -9,8 +9,8 @@
-
-
+
+
\ No newline at end of file