using System.Data; using System.Text; using Npgsql; using TravelAgencyWinForms.Data; using TravelAgencyWinForms.Models; using TravelAgencyWinForms.Utils; namespace TravelAgencyWinForms.Forms; public partial class TableEditorForm : Form { private readonly EntityConfig _config; private readonly bool _canEdit; private DataTable _sourceTable = new(); public TableEditorForm(EntityConfig config, bool canEdit) { _config = config; _canEdit = canEdit && !config.ReadOnly; InitializeComponent(); ApplyStyle(); ConfigureForm(); Load += (_, _) => LoadData(); } private void ApplyStyle() { AppTheme.ApplyForm(this); AppTheme.StyleGrid(dataGrid); AppTheme.StyleTextBox(searchTextBox); AppTheme.StylePrimaryButton(refreshButton); AppTheme.StyleSecondaryButton(addButton); AppTheme.StyleSecondaryButton(editButton); AppTheme.StyleDangerButton(deleteButton); headerPanel.BackColor = AppTheme.Surface; titleLabel.ForeColor = AppTheme.Text; subtitleLabel.ForeColor = AppTheme.Muted; searchLabel.ForeColor = AppTheme.Muted; infoLabel.ForeColor = AppTheme.Muted; emptyLabel.ForeColor = AppTheme.Muted; var image = AppTheme.LoadImage("empty_results.png"); if (image != null) emptyPictureBox.Image = image; } private void ConfigureForm() { Text = _config.Title; titleLabel.Text = _config.Title; subtitleLabel.Text = _config.ReadOnly ? "Просмотр данных" : "Просмотр и редактирование данных"; addButton.Enabled = _canEdit; editButton.Enabled = _canEdit && _config.Fields.Any(f => !f.IsKey); deleteButton.Enabled = _canEdit; } private void RefreshButton_Click(object? sender, EventArgs e) { LoadData(); } private void AddButton_Click(object? sender, EventArgs e) { AddRecord(); } private void EditButton_Click(object? sender, EventArgs e) { EditRecord(); } private void DeleteButton_Click(object? sender, EventArgs e) { DeleteRecord(); } private void SearchTextBox_TextChanged(object? sender, EventArgs e) { ApplySearch(); } private void DataGrid_SelectionChanged(object? sender, EventArgs e) { UpdateCalculatedPanel(); } private void LoadData() { Cursor = Cursors.WaitCursor; try { var (sql, parameters) = BuildSelectSql(); _sourceTable = Db.Query(sql, parameters.ToArray()); dataGrid.DataSource = _sourceTable; ApplyGridPresentation(); UpdateEmptyState(_sourceTable.Rows.Count == 0); UpdateCalculatedPanel(); infoLabel.Text = $"Записей: {_sourceTable.Rows.Count}"; } catch (Exception ex) { MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Загрузка", MessageBoxButtons.OK, MessageBoxIcon.Warning); } finally { Cursor = Cursors.Default; } } private (string Sql, List Parameters) BuildSelectSql() { var parameters = new List(); string selectPart; if (_config.Fields.Count == 0) { selectPart = "*"; } else { selectPart = string.Join(", ", _config.GridFields.Select(f => f.Name == "role" && _config.TableName == "app_users" ? "role::text AS role" : Q(f.Name))); } var sb = new StringBuilder(); sb.Append("SELECT ").Append(selectPart).Append(" FROM ").Append(Q(_config.SourceName)); var where = new List(); if (_config.RestrictByCurrentEmployee && !string.IsNullOrWhiteSpace(_config.RestrictColumn)) { if (CurrentUser.ClientId == null) { where.Add("1 = 0"); } else { where.Add(Q(_config.RestrictColumn) + " = @client_id"); parameters.Add(new NpgsqlParameter("client_id", CurrentUser.ClientId.Value)); } } if (where.Count > 0) sb.Append(" WHERE ").Append(string.Join(" AND ", where)); if (!string.IsNullOrWhiteSpace(_config.OrderBy)) sb.Append(" ORDER BY ").Append(_config.OrderBy); return (sb.ToString(), parameters); } private void ApplyGridPresentation() { PresentationHelper.ApplyHeaders(dataGrid); if (_config.Fields.Count == 0 || _config.ReadOnly) PresentationHelper.HideViewColumns(dataGrid); foreach (DataGridViewColumn column in dataGrid.Columns) if (CalculatedFields.IsCalculated(column.Name)) column.Visible = false; if (dataGrid.Rows.Count > 0 && dataGrid.Columns.Cast().Any(c => c.Visible)) { var firstVisible = dataGrid.Columns.Cast().First(c => c.Visible); dataGrid.CurrentCell = dataGrid.Rows[0].Cells[firstVisible.Index]; } } private void UpdateEmptyState(bool isEmpty) { dataGrid.Visible = !isEmpty; emptyPanel.Visible = isEmpty; } private void UpdateCalculatedPanel() { calculatedPanel.Controls.Clear(); var table = GetCurrentGridTable(); var calculatedColumns = CalculatedFields.GetColumns(table); if (calculatedColumns.Count == 0) { calculatedPanel.Visible = false; return; } calculatedPanel.Visible = true; var selectedRow = GetSelectedRow(); foreach (var columnName in calculatedColumns) { var value = "—"; if (selectedRow != null && selectedRow.Table.Columns.Contains(columnName)) value = CalculatedFields.FormatValue(columnName, selectedRow[columnName]); calculatedPanel.Controls.Add(AppTheme.CreateCard(CalculatedFields.GetLabel(columnName), value)); } } private DataTable GetCurrentGridTable() { return dataGrid.DataSource switch { DataTable table => table, DataView view => view.ToTable(), _ => _sourceTable }; } private void ApplySearch() { if (_sourceTable.Rows.Count == 0) return; var search = searchTextBox.Text.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(search)) { dataGrid.DataSource = _sourceTable; ApplyGridPresentation(); UpdateEmptyState(_sourceTable.Rows.Count == 0); UpdateCalculatedPanel(); infoLabel.Text = $"Записей: {_sourceTable.Rows.Count}"; return; } var filtered = _sourceTable.Clone(); foreach (DataRow row in _sourceTable.Rows) { var haystack = string.Join(" ", row.ItemArray.Select(v => v?.ToString() ?? string.Empty)).ToLowerInvariant(); if (haystack.Contains(search)) filtered.ImportRow(row); } dataGrid.DataSource = filtered; ApplyGridPresentation(); UpdateEmptyState(filtered.Rows.Count == 0); UpdateCalculatedPanel(); infoLabel.Text = $"Найдено: {filtered.Rows.Count}"; } private DataRow? GetSelectedRow() { if (dataGrid.CurrentRow?.DataBoundItem is DataRowView view) return view.Row; return null; } private void AddRecord() { using var dialog = new EntityEditDialog(_config); if (dialog.ShowDialog(this) != DialogResult.OK) return; try { Db.ResetIdentitySequences(); var (sql, parameters) = BuildInsertSql(dialog.Values, dialog.PasswordWasEntered); Db.SetAppContext(CurrentUser.Login); Db.Execute(sql, parameters.ToArray()); LoadData(); } catch (Exception ex) { MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Добавление", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void EditRecord() { var row = GetSelectedRow(); if (row == null) { MessageBox.Show("Сначала выберите запись.", "Изменение", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using var dialog = new EntityEditDialog(_config, row); if (dialog.ShowDialog(this) != DialogResult.OK) return; try { var (sql, parameters) = BuildUpdateSql(dialog.Values, dialog.PasswordWasEntered, row); Db.SetAppContext(CurrentUser.Login); Db.Execute(sql, parameters.ToArray()); LoadData(); } catch (Exception ex) { MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Изменение", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void DeleteRecord() { var row = GetSelectedRow(); if (row == null) { MessageBox.Show("Сначала выберите запись.", "Удаление", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (MessageBox.Show("Удалить выбранную запись?", "Удаление", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; try { var (sql, parameters) = BuildDeleteSql(row); Db.SetAppContext(CurrentUser.Login); Db.Execute(sql, parameters.ToArray()); LoadData(); } catch (Exception ex) { MessageBox.Show(ErrorHelper.ToUserMessage(ex), "Удаление", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private (string Sql, List Parameters) BuildInsertSql(Dictionary values, bool passwordWasEntered) { var insertFields = _config.Fields .Where(f => !(f.IsKey && f.AutoIdentity)) .Where(f => f.Kind != FieldKind.Password || passwordWasEntered) .ToList(); var columns = new List(); var valueSql = new List(); var parameters = new List(); foreach (var field in insertFields) { columns.Add(Q(field.Name)); valueSql.Add(SqlValueExpression(field)); parameters.Add(MakeParameter(field.Name, values.GetValueOrDefault(field.Name))); } var sql = $"INSERT INTO {Q(_config.TableName)} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", valueSql)});"; return (sql, parameters); } private (string Sql, List Parameters) BuildUpdateSql(Dictionary values, bool passwordWasEntered, DataRow originalRow) { var updateFields = _config.Fields .Where(f => !f.IsKey) .Where(f => f.Kind != FieldKind.Password || passwordWasEntered) .ToList(); var setParts = new List(); var parameters = new List(); foreach (var field in updateFields) { setParts.Add($"{Q(field.Name)} = {SqlValueExpression(field)}"); parameters.Add(MakeParameter(field.Name, values.GetValueOrDefault(field.Name))); } var whereParts = new List(); foreach (var key in _config.KeyFields) { var paramName = "key_" + key.Name; whereParts.Add($"{Q(key.Name)} = @{paramName}"); parameters.Add(MakeParameter(paramName, originalRow[key.Name])); } var sql = $"UPDATE {Q(_config.TableName)} SET {string.Join(", ", setParts)} WHERE {string.Join(" AND ", whereParts)};"; return (sql, parameters); } private (string Sql, List Parameters) BuildDeleteSql(DataRow row) { var whereParts = new List(); var parameters = new List(); foreach (var key in _config.KeyFields) { var paramName = "key_" + key.Name; whereParts.Add($"{Q(key.Name)} = @{paramName}"); parameters.Add(MakeParameter(paramName, row[key.Name])); } var sql = $"DELETE FROM {Q(_config.TableName)} WHERE {string.Join(" AND ", whereParts)};"; return (sql, parameters); } private string SqlValueExpression(FieldConfig field) { if (_config.TableName == "app_users" && field.Name == "password_hash") return "fn_hash_password(@password_hash)"; if (_config.TableName == "app_users" && field.Name == "role") return "@role::app_role"; return "@" + field.Name; } private static NpgsqlParameter MakeParameter(string name, object? value) { return new NpgsqlParameter(name, value ?? DBNull.Value); } private static string Q(string identifier) { return "\"" + identifier.Replace("\"", "\"\"") + "\""; } }