EntityEditDialog.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using System.Data;
  2. using TravelAgencyWinForms.Data;
  3. using TravelAgencyWinForms.Models;
  4. using TravelAgencyWinForms.Utils;
  5. namespace TravelAgencyWinForms.Forms;
  6. public sealed class EntityEditDialog : Form
  7. {
  8. private readonly EntityConfig _config;
  9. private readonly bool _isEdit;
  10. private readonly Dictionary<string, Control> _controls = new();
  11. private readonly Dictionary<string, FieldConfig> _fields = new();
  12. public Dictionary<string, object?> Values { get; } = new();
  13. public bool PasswordWasEntered { get; private set; }
  14. public EntityEditDialog(EntityConfig config, DataRow? row = null)
  15. {
  16. _config = config;
  17. _isEdit = row != null;
  18. Text = (_isEdit ? "Изменение" : "Добавление") + " — " + config.Title;
  19. StartPosition = FormStartPosition.CenterParent;
  20. Width = 560;
  21. Height = Math.Min(760, 180 + config.EditableFields.Count() * 46);
  22. AppTheme.ApplyForm(this, true);
  23. BuildLayout(row);
  24. }
  25. private void BuildLayout(DataRow? row)
  26. {
  27. var main = new TableLayoutPanel
  28. {
  29. Dock = DockStyle.Fill,
  30. Padding = new Padding(18),
  31. ColumnCount = 2,
  32. RowCount = _config.EditableFields.Count() + 2,
  33. AutoScroll = true
  34. };
  35. main.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 180));
  36. main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
  37. var currentRow = 0;
  38. foreach (var field in _config.EditableFields)
  39. {
  40. _fields[field.Name] = field;
  41. main.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
  42. var label = new Label
  43. {
  44. Text = field.Label + (field.Required ? " *" : string.Empty),
  45. AutoSize = true,
  46. Anchor = AnchorStyles.Left,
  47. ForeColor = AppTheme.Text
  48. };
  49. var control = CreateControl(field, row);
  50. control.Anchor = AnchorStyles.Left | AnchorStyles.Right;
  51. if (_isEdit && (field.ReadOnlyOnEdit || field.IsKey))
  52. control.Enabled = false;
  53. main.Controls.Add(label, 0, currentRow);
  54. main.Controls.Add(control, 1, currentRow);
  55. _controls[field.Name] = control;
  56. currentRow++;
  57. }
  58. var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill };
  59. var saveButton = new Button { Text = "Сохранить", Width = 120, Height = 36 };
  60. var cancelButton = new Button { Text = "Отмена", Width = 120, Height = 36 };
  61. AppTheme.StylePrimaryButton(saveButton);
  62. AppTheme.StyleSecondaryButton(cancelButton);
  63. saveButton.Click += (_, _) => Save();
  64. cancelButton.Click += (_, _) => DialogResult = DialogResult.Cancel;
  65. buttons.Controls.Add(saveButton);
  66. buttons.Controls.Add(cancelButton);
  67. main.RowStyles.Add(new RowStyle(SizeType.Absolute, 46));
  68. main.Controls.Add(buttons, 0, currentRow);
  69. main.SetColumnSpan(buttons, 2);
  70. Controls.Add(main);
  71. }
  72. private Control CreateControl(FieldConfig field, DataRow? row)
  73. {
  74. object? value = null;
  75. if (row != null && row.Table.Columns.Contains(field.Name) && row[field.Name] != DBNull.Value)
  76. value = row[field.Name];
  77. switch (field.Kind)
  78. {
  79. case FieldKind.Date:
  80. var datePicker = new DateTimePicker
  81. {
  82. Format = DateTimePickerFormat.Short,
  83. Width = 240,
  84. ShowCheckBox = !field.Required
  85. };
  86. if (value is DateTime dt)
  87. {
  88. datePicker.Value = dt;
  89. datePicker.Checked = true;
  90. }
  91. else
  92. {
  93. datePicker.Value = DateTime.Today;
  94. datePicker.Checked = field.Required;
  95. }
  96. return datePicker;
  97. case FieldKind.Boolean:
  98. var checkBox = new CheckBox { Text = "Да", AutoSize = true };
  99. checkBox.Checked = value == null || Convert.ToBoolean(value);
  100. return checkBox;
  101. case FieldKind.Lookup:
  102. var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 320 };
  103. AppTheme.StyleComboBox(comboBox);
  104. LoadLookup(comboBox, field, value);
  105. return comboBox;
  106. case FieldKind.Enum:
  107. var enumBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 320 };
  108. AppTheme.StyleComboBox(enumBox);
  109. foreach (var option in field.Options)
  110. enumBox.Items.Add(option);
  111. if (value != null && enumBox.Items.Contains(Convert.ToString(value)))
  112. enumBox.SelectedItem = Convert.ToString(value);
  113. else if (enumBox.Items.Count > 0)
  114. enumBox.SelectedIndex = 0;
  115. return enumBox;
  116. case FieldKind.Password:
  117. var passwordBox = new TextBox { Width = 320, UseSystemPasswordChar = true, Text = string.Empty };
  118. AppTheme.StyleTextBox(passwordBox);
  119. return passwordBox;
  120. default:
  121. var textBox = new TextBox { Width = 320, Text = value?.ToString() ?? string.Empty };
  122. AppTheme.StyleTextBox(textBox);
  123. return textBox;
  124. }
  125. }
  126. private static void LoadLookup(ComboBox comboBox, FieldConfig field, object? selectedValue)
  127. {
  128. if (string.IsNullOrWhiteSpace(field.LookupSql))
  129. return;
  130. var source = Db.Query(field.LookupSql);
  131. var table = new DataTable();
  132. table.Columns.Add(field.LookupValueColumn, typeof(object));
  133. table.Columns.Add(field.LookupDisplayColumn, typeof(string));
  134. if (!field.Required)
  135. table.Rows.Add(DBNull.Value, "—");
  136. foreach (DataRow sourceRow in source.Rows)
  137. table.Rows.Add(sourceRow[field.LookupValueColumn] == DBNull.Value ? DBNull.Value : sourceRow[field.LookupValueColumn], Convert.ToString(sourceRow[field.LookupDisplayColumn]) ?? string.Empty);
  138. comboBox.DataSource = table;
  139. comboBox.ValueMember = field.LookupValueColumn;
  140. comboBox.DisplayMember = field.LookupDisplayColumn;
  141. if (selectedValue != null && selectedValue != DBNull.Value)
  142. comboBox.SelectedValue = selectedValue;
  143. else if (comboBox.Items.Count > 0)
  144. comboBox.SelectedIndex = 0;
  145. }
  146. private void Save()
  147. {
  148. Values.Clear();
  149. PasswordWasEntered = false;
  150. foreach (var (name, control) in _controls)
  151. {
  152. var field = _fields[name];
  153. object? value;
  154. try
  155. {
  156. value = ReadControlValue(field, control);
  157. }
  158. catch (Exception ex)
  159. {
  160. MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  161. return;
  162. }
  163. if (field.Required && (value == null || string.IsNullOrWhiteSpace(Convert.ToString(value))))
  164. {
  165. MessageBox.Show($"Поле '{field.Label}' должно быть заполнено.", "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  166. return;
  167. }
  168. if (field.Kind == FieldKind.Password)
  169. {
  170. PasswordWasEntered = !string.IsNullOrWhiteSpace(Convert.ToString(value));
  171. if (!_isEdit && !PasswordWasEntered)
  172. {
  173. MessageBox.Show("Введите пароль.", "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  174. return;
  175. }
  176. }
  177. Values[name] = value;
  178. }
  179. if (!ValidateDomainRules())
  180. return;
  181. DialogResult = DialogResult.OK;
  182. Close();
  183. }
  184. private object? ReadControlValue(FieldConfig field, Control control)
  185. {
  186. return field.Kind switch
  187. {
  188. FieldKind.Date => ((DateTimePicker)control).Checked ? ((DateTimePicker)control).Value.Date : null,
  189. FieldKind.Boolean => ((CheckBox)control).Checked,
  190. FieldKind.Lookup => ((ComboBox)control).SelectedValue == DBNull.Value ? null : ((ComboBox)control).SelectedValue,
  191. FieldKind.Enum => ((ComboBox)control).SelectedItem?.ToString(),
  192. FieldKind.Integer => ParseInteger(field, control.Text),
  193. FieldKind.Decimal => ParseDecimal(field, control.Text),
  194. _ => string.IsNullOrWhiteSpace(control.Text.Trim()) ? null : control.Text.Trim()
  195. };
  196. }
  197. private static int? ParseInteger(FieldConfig field, string text)
  198. {
  199. var value = text.Trim();
  200. if (string.IsNullOrWhiteSpace(value))
  201. return null;
  202. if (!int.TryParse(value, out var number))
  203. throw new InvalidOperationException($"Поле '{field.Label}' должно быть целым числом.");
  204. return number;
  205. }
  206. private static decimal? ParseDecimal(FieldConfig field, string text)
  207. {
  208. var value = text.Trim().Replace(',', '.');
  209. if (string.IsNullOrWhiteSpace(value))
  210. return null;
  211. if (!decimal.TryParse(value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var number))
  212. throw new InvalidOperationException($"Поле '{field.Label}' должно быть числом.");
  213. return number;
  214. }
  215. private bool ValidateDomainRules()
  216. {
  217. try
  218. {
  219. if (_config.TableName == "positions" && Values.TryGetValue("salary", out var salaryObj) && salaryObj != null && Convert.ToDecimal(salaryObj) < 0)
  220. return Warn("Зарплата не может быть отрицательной.");
  221. if (_config.TableName == "tours" && Values.TryGetValue("price", out var priceObj) && priceObj != null && Convert.ToDecimal(priceObj) < 0)
  222. return Warn("Стоимость тура не может быть отрицательной.");
  223. if (_config.TableName == "employees" && Values.TryGetValue("birth_date", out var birthObj) && birthObj is DateTime birthDate)
  224. {
  225. if (birthDate > DateTime.Today)
  226. return Warn("Дата рождения не может быть в будущем.");
  227. if (birthDate > DateTime.Today.AddYears(-18))
  228. return Warn("Сотрудник должен быть старше 18 лет.");
  229. }
  230. 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)
  231. return Warn("Дата окончания не может быть раньше даты начала.");
  232. if (_config.TableName == "bookings" && Values.TryGetValue("booking_date", out var bookingObj) && bookingObj is DateTime bookingDate && bookingDate > DateTime.Today)
  233. return Warn("Дата оформления не может быть в будущем.");
  234. }
  235. catch (Exception ex)
  236. {
  237. return Warn(ErrorHelper.ToUserMessage(ex));
  238. }
  239. return true;
  240. }
  241. private static bool Warn(string message)
  242. {
  243. MessageBox.Show(message, "Проверка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  244. return false;
  245. }
  246. }