using System.Data; using TravelAgencyWinForms.Data; using TravelAgencyWinForms.Models; using TravelAgencyWinForms.Utils; namespace TravelAgencyWinForms.Forms; public sealed class EntityEditDialog : Form { private readonly EntityConfig _config; private readonly bool _isEdit; private readonly Dictionary _controls = new(); private readonly Dictionary _fields = new(); public Dictionary Values { get; } = new(); public bool PasswordWasEntered { get; private set; } public EntityEditDialog(EntityConfig config, DataRow? row = null) { _config = config; _isEdit = row != null; Text = (_isEdit ? "Изменение" : "Добавление") + " — " + config.Title; StartPosition = FormStartPosition.CenterParent; Width = 560; Height = Math.Min(760, 180 + config.EditableFields.Count() * 46); AppTheme.ApplyForm(this, true); BuildLayout(row); } private void BuildLayout(DataRow? row) { var main = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(18), ColumnCount = 2, RowCount = _config.EditableFields.Count() + 2, AutoScroll = true }; main.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 180)); main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var currentRow = 0; foreach (var field in _config.EditableFields) { _fields[field.Name] = field; main.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); var label = new Label { Text = field.Label + (field.Required ? " *" : string.Empty), AutoSize = true, Anchor = AnchorStyles.Left, ForeColor = AppTheme.Text }; var control = CreateControl(field, row); control.Anchor = AnchorStyles.Left | AnchorStyles.Right; if (_isEdit && (field.ReadOnlyOnEdit || field.IsKey)) control.Enabled = false; main.Controls.Add(label, 0, currentRow); main.Controls.Add(control, 1, currentRow); _controls[field.Name] = control; currentRow++; } var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill }; var saveButton = new Button { Text = "Сохранить", Width = 120, Height = 36 }; var cancelButton = new Button { Text = "Отмена", Width = 120, Height = 36 }; AppTheme.StylePrimaryButton(saveButton); AppTheme.StyleSecondaryButton(cancelButton); saveButton.Click += (_, _) => Save(); cancelButton.Click += (_, _) => DialogResult = DialogResult.Cancel; buttons.Controls.Add(saveButton); buttons.Controls.Add(cancelButton); main.RowStyles.Add(new RowStyle(SizeType.Absolute, 46)); main.Controls.Add(buttons, 0, currentRow); main.SetColumnSpan(buttons, 2); Controls.Add(main); } private Control CreateControl(FieldConfig field, DataRow? row) { object? value = null; if (row != null && row.Table.Columns.Contains(field.Name) && row[field.Name] != DBNull.Value) value = row[field.Name]; switch (field.Kind) { case FieldKind.Date: var datePicker = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 240, ShowCheckBox = !field.Required }; if (value is DateTime dt) { datePicker.Value = dt; datePicker.Checked = true; } else { datePicker.Value = DateTime.Today; datePicker.Checked = field.Required; } return datePicker; case FieldKind.Boolean: var checkBox = new CheckBox { Text = "Да", AutoSize = true }; checkBox.Checked = value == null || Convert.ToBoolean(value); return checkBox; case FieldKind.Lookup: var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 320 }; AppTheme.StyleComboBox(comboBox); LoadLookup(comboBox, field, value); return comboBox; case FieldKind.Enum: var enumBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 320 }; AppTheme.StyleComboBox(enumBox); foreach (var option in field.Options) enumBox.Items.Add(option); if (value != null && enumBox.Items.Contains(Convert.ToString(value))) enumBox.SelectedItem = Convert.ToString(value); else if (enumBox.Items.Count > 0) enumBox.SelectedIndex = 0; return enumBox; case FieldKind.Password: var passwordBox = new TextBox { Width = 320, UseSystemPasswordChar = true, Text = string.Empty }; AppTheme.StyleTextBox(passwordBox); return passwordBox; default: var textBox = new TextBox { Width = 320, Text = value?.ToString() ?? string.Empty }; AppTheme.StyleTextBox(textBox); return textBox; } } private static void LoadLookup(ComboBox comboBox, FieldConfig field, object? selectedValue) { if (string.IsNullOrWhiteSpace(field.LookupSql)) return; var source = Db.Query(field.LookupSql); var table = new DataTable(); table.Columns.Add(field.LookupValueColumn, typeof(object)); table.Columns.Add(field.LookupDisplayColumn, typeof(string)); if (!field.Required) table.Rows.Add(DBNull.Value, "—"); foreach (DataRow sourceRow in source.Rows) table.Rows.Add(sourceRow[field.LookupValueColumn] == DBNull.Value ? DBNull.Value : sourceRow[field.LookupValueColumn], Convert.ToString(sourceRow[field.LookupDisplayColumn]) ?? string.Empty); comboBox.DataSource = table; comboBox.ValueMember = field.LookupValueColumn; comboBox.DisplayMember = field.LookupDisplayColumn; if (selectedValue != null && selectedValue != DBNull.Value) comboBox.SelectedValue = selectedValue; else if (comboBox.Items.Count > 0) comboBox.SelectedIndex = 0; } private void Save() { Values.Clear(); PasswordWasEntered = false; foreach (var (name, control) in _controls) { var field = _fields[name]; object? value; try { value = ReadControlValue(field, control); } catch (Exception ex) { MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (field.Required && (value == null || string.IsNullOrWhiteSpace(Convert.ToString(value)))) { MessageBox.Show($"Поле '{field.Label}' должно быть заполнено.", "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (field.Kind == FieldKind.Password) { PasswordWasEntered = !string.IsNullOrWhiteSpace(Convert.ToString(value)); if (!_isEdit && !PasswordWasEntered) { MessageBox.Show("Введите пароль.", "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } } Values[name] = value; } if (!ValidateDomainRules()) return; DialogResult = DialogResult.OK; Close(); } private object? ReadControlValue(FieldConfig field, Control control) { return field.Kind switch { FieldKind.Date => ((DateTimePicker)control).Checked ? ((DateTimePicker)control).Value.Date : null, FieldKind.Boolean => ((CheckBox)control).Checked, FieldKind.Lookup => ((ComboBox)control).SelectedValue == DBNull.Value ? null : ((ComboBox)control).SelectedValue, FieldKind.Enum => ((ComboBox)control).SelectedItem?.ToString(), FieldKind.Integer => ParseInteger(field, control.Text), FieldKind.Decimal => ParseDecimal(field, control.Text), _ => string.IsNullOrWhiteSpace(control.Text.Trim()) ? null : control.Text.Trim() }; } private static int? ParseInteger(FieldConfig field, string text) { var value = text.Trim(); if (string.IsNullOrWhiteSpace(value)) return null; if (!int.TryParse(value, out var number)) throw new InvalidOperationException($"Поле '{field.Label}' должно быть целым числом."); return number; } private static decimal? ParseDecimal(FieldConfig field, string text) { var value = text.Trim().Replace(',', '.'); if (string.IsNullOrWhiteSpace(value)) return null; if (!decimal.TryParse(value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var number)) throw new InvalidOperationException($"Поле '{field.Label}' должно быть числом."); return number; } private bool ValidateDomainRules() { try { if (_config.TableName == "positions" && Values.TryGetValue("salary", out var salaryObj) && salaryObj != null && Convert.ToDecimal(salaryObj) < 0) return Warn("Зарплата не может быть отрицательной."); if (_config.TableName == "tours" && Values.TryGetValue("price", out var priceObj) && priceObj != null && Convert.ToDecimal(priceObj) < 0) return Warn("Стоимость тура не может быть отрицательной."); if (_config.TableName == "employees" && Values.TryGetValue("birth_date", out var birthObj) && birthObj is DateTime birthDate) { if (birthDate > DateTime.Today) return Warn("Дата рождения не может быть в будущем."); if (birthDate > DateTime.Today.AddYears(-18)) return Warn("Сотрудник должен быть старше 18 лет."); } if (_config.TableName == "tour_list" && Values.TryGetValue("start_date", out var startObj) && Values.TryGetValue("end_date", out var endObj) && startObj is DateTime start && endObj is DateTime end && end < start) return Warn("Дата окончания не может быть раньше даты начала."); if (_config.TableName == "bookings" && Values.TryGetValue("booking_date", out var bookingObj) && bookingObj is DateTime bookingDate && bookingDate > DateTime.Today) return Warn("Дата оформления не может быть в будущем."); } catch (Exception ex) { return Warn(ErrorHelper.ToUserMessage(ex)); } return true; } private static bool Warn(string message) { MessageBox.Show(message, "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } }