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