using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Text; using System.Windows.Forms; using Npgsql; using NpgsqlTypes; namespace SimpleDBViewer { public partial class MainForm : Form { public const string DbSchema = "akulova_vs"; private readonly List tableInfos = new List(); private TableInfo currentTable; private DataTable currentTableData; private DataTable currentSearchData; public MainForm() { bool isDesigner = LicenseManager.UsageMode == LicenseUsageMode.Designtime; if (!isDesigner && !UserSession.IsLoggedIn) { MessageBox.Show("Сначала выполните вход в систему.", "Доступ запрещён", MessageBoxButtons.OK, MessageBoxIcon.Warning); Close(); return; } InitializeComponent(); if (isDesigner) return; BuildTableList(); ApplyPermissions(); LoadInitialData(); } private void BuildTableList() { tableInfos.Clear(); if (!UserSession.IsAdmin) { tableInfos.Add(new TableInfo("personal_profile", "Мои данные", "driver_id")); tableInfos.Add(new TableInfo("personal_exams", "Мои экзамены", "exam_id")); tableInfos.Add(new TableInfo("personal_license", "Моё удостоверение", "license_id")); return; } tableInfos.Add(new TableInfo("driving_schools", "Автошколы", "school_id")); tableInfos.Add(new TableInfo("instructors", "Инструкторы", "instructor_id")); tableInfos.Add(new TableInfo("drivers", "Водители", "driver_id")); tableInfos.Add(new TableInfo("exam_routes", "Маршруты экзамена", "route_id")); tableInfos.Add(new TableInfo("traffic_inspectors", "Инспекторы", "inspector_id")); tableInfos.Add(new TableInfo("traffic_officers", "Сотрудники, выдающие права", "officer_id")); tableInfos.Add(new TableInfo("exams", "Экзамены", "exam_id")); tableInfos.Add(new TableInfo("licenses", "Водительские удостоверения", "license_id")); tableInfos.Add(new TableInfo("app_users", "Пользователи приложения", "user_id")); tableInfos.Add(new TableInfo("audit_log", "Журнал операций", "log_id", true)); } private void ApplyPermissions() { lblUser.Text = UserSession.CurrentUser.Login + " · " + UserSession.CurrentUser.Role; bool isAdmin = UserSession.IsAdmin; btnAdd.Enabled = isAdmin; btnEdit.Enabled = isAdmin; btnDelete.Enabled = isAdmin; if (!isAdmin) { lblTitle.Text = "Личный кабинет автошколы"; lblTableCaption.Text = "Раздел:"; tabTables.Text = "Личный кабинет"; btnAdd.Visible = false; btnEdit.Visible = false; btnDelete.Visible = false; btnExportTable.Location = btnAdd.Location; if (tabControl.TabPages.Contains(tabSearch)) tabControl.TabPages.Remove(tabSearch); } UpdateActionButtons(); } private void UpdateActionButtons() { bool canModify = UserSession.IsAdmin && currentTable != null && !currentTable.IsReadOnly; if (btnAdd != null) btnAdd.Enabled = canModify; if (btnEdit != null) btnEdit.Enabled = canModify; if (btnDelete != null) btnDelete.Enabled = canModify; } private void LoadInitialData() { cmbTables.Items.Clear(); foreach (TableInfo table in tableInfos) { cmbTables.Items.Add(table); } if (cmbTables.Items.Count > 0) { cmbTables.SelectedIndex = 0; } if (UserSession.IsAdmin) LoadExamSearch(); } private void CmbTables_SelectedIndexChanged(object sender, EventArgs e) { currentTable = cmbTables.SelectedItem as TableInfo; UpdateActionButtons(); LoadCurrentTable(); } private void LoadCurrentTable() { if (currentTable == null) return; try { using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString)) { connection.Open(); string sql = GetSelectSql(currentTable); using (NpgsqlCommand command = new NpgsqlCommand(sql, connection)) { AddCurrentTableParameters(command); using (NpgsqlDataAdapter adapter = new NpgsqlDataAdapter(command)) { currentTableData = new DataTable(); adapter.Fill(currentTableData); dgvTables.DataSource = currentTableData; ApplyGridPresentation(dgvTables); lblTableStatus.Text = currentTable.DisplayName + ": записей — " + currentTableData.Rows.Count; } } } } catch (Exception ex) { MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось загрузить таблицу."), "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void AddCurrentTableParameters(NpgsqlCommand command) { if (currentTable == null) return; if (currentTable.Name == "personal_profile" || currentTable.Name == "personal_exams" || currentTable.Name == "personal_license") { int driverId = UserSession.CurrentUser != null && UserSession.CurrentUser.DriverId.HasValue ? UserSession.CurrentUser.DriverId.Value : -1; command.Parameters.AddWithValue("@current_driver_id", driverId); } } private string GetSelectSql(TableInfo table) { if (table.Name == "personal_profile") { if (GetCurrentDriverCondition() == null) return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message"; return @"select d.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display, d.birth_date, d.phone, ds.school_name from akulova_vs.drivers d join akulova_vs.driving_schools ds on ds.school_id = d.school_id where d.driver_id = @current_driver_id"; } if (table.Name == "personal_exams") { if (GetCurrentDriverCondition() == null) return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message"; return @"select exam_id, exam_date, result, route_name, inspector_full_name, attempt_number, driver_age from ( select e.exam_id, e.exam_date, e.result, r.route_name, concat_ws(' ', ti.last_name, ti.first_name, ti.middle_name) as inspector_full_name, count(*) over ( partition by e.driver_id order by e.exam_date, e.exam_id rows between unbounded preceding and current row )::integer as attempt_number, extract(year from age(e.exam_date, d.birth_date))::integer as driver_age from akulova_vs.exams e join akulova_vs.drivers d on d.driver_id = e.driver_id join akulova_vs.exam_routes r on r.route_id = e.route_id join akulova_vs.traffic_inspectors ti on ti.inspector_id = e.inspector_id where e.driver_id = @current_driver_id ) personal_exams order by exam_date desc, exam_id desc"; } if (table.Name == "personal_license") { if (GetCurrentDriverCondition() == null) return "select 'Учётная запись не привязана к водителю. Обратитесь к администратору.' as message"; return @"select l.license_id, l.issue_date, l.category, l.license_number, concat_ws(' ', o.last_name, o.first_name, o.middle_name) as officer_display from akulova_vs.licenses l join akulova_vs.traffic_officers o on o.officer_id = l.officer_id where l.driver_id = @current_driver_id order by l.issue_date desc, l.license_id desc"; } if (table.Name == "driving_schools") { return @"select school_id, school_name, address, phone from akulova_vs.driving_schools order by school_id"; } if (table.Name == "instructors") { return @"select i.instructor_id, i.school_id, ds.school_name as school_display, i.last_name, i.first_name, i.middle_name, i.phone, i.license_number from akulova_vs.instructors i join akulova_vs.driving_schools ds on ds.school_id = i.school_id order by i.instructor_id"; } if (table.Name == "drivers") { return @"select d.driver_id, d.school_id, ds.school_name as school_display, d.last_name, d.first_name, d.middle_name, d.birth_date, d.phone from akulova_vs.drivers d join akulova_vs.driving_schools ds on ds.school_id = d.school_id order by d.driver_id"; } if (table.Name == "exam_routes") { return @"select route_id, route_name, start_address, end_address, difficulty from akulova_vs.exam_routes order by route_id"; } if (table.Name == "traffic_inspectors") { return @"select inspector_id, last_name, first_name, middle_name, rank_name from akulova_vs.traffic_inspectors order by inspector_id"; } if (table.Name == "traffic_officers") { return @"select officer_id, last_name, first_name, middle_name, position_name from akulova_vs.traffic_officers order by officer_id"; } if (table.Name == "exams") { return @"select e.exam_id, e.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display, e.route_id, r.route_name as route_display, e.inspector_id, concat_ws(' ', ti.last_name, ti.first_name, ti.middle_name) as inspector_display, e.exam_date, e.result from akulova_vs.exams e join akulova_vs.drivers d on d.driver_id = e.driver_id join akulova_vs.exam_routes r on r.route_id = e.route_id join akulova_vs.traffic_inspectors ti on ti.inspector_id = e.inspector_id order by e.exam_id"; } if (table.Name == "licenses") { return @"select l.license_id, l.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display, l.officer_id, concat_ws(' ', o.last_name, o.first_name, o.middle_name) as officer_display, l.issue_date, l.category, l.license_number from akulova_vs.licenses l join akulova_vs.drivers d on d.driver_id = l.driver_id join akulova_vs.traffic_officers o on o.officer_id = l.officer_id order by l.license_id"; } if (table.Name == "app_users") { return @"select u.user_id, u.login, u.role::text as role, u.is_active, u.instructor_id, concat_ws(' ', i.last_name, i.first_name, i.middle_name) as instructor_display, u.driver_id, concat_ws(' ', d.last_name, d.first_name, d.middle_name) as driver_display from akulova_vs.app_users u left join akulova_vs.instructors i on i.instructor_id = u.instructor_id left join akulova_vs.drivers d on d.driver_id = u.driver_id order by u.user_id"; } if (table.Name == "audit_log") { return @"select log_id, table_name, operation, record_id, changed_at, coalesce(nullif(changed_by, ''), 'system') as changed_by from akulova_vs.audit_log order by changed_at desc, log_id desc limit 500"; } return "select * from " + FullName(table.Name) + " order by " + Quote(table.PrimaryKey); } private string GetCurrentDriverCondition() { if (UserSession.CurrentUser == null || !UserSession.CurrentUser.DriverId.HasValue) return null; return UserSession.CurrentUser.DriverId.Value.ToString(); } private void ApplyGridPresentation(DataGridView grid) { if (grid == null) return; foreach (DataGridViewColumn column in grid.Columns) { string name = column.Name; column.Visible = !IsTechnicalColumn(name); column.HeaderText = GetColumnCaption(name); if (name.EndsWith("_date", StringComparison.OrdinalIgnoreCase) || name == "birth_date" || name == "exam_date" || name == "issue_date") column.DefaultCellStyle.Format = "dd.MM.yyyy"; } grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; grid.AutoResizeColumns(); } private bool IsTechnicalColumn(string columnName) { if (string.IsNullOrEmpty(columnName)) return false; if (columnName == "password_hash") return true; if (columnName == "record_id") return false; if (columnName.EndsWith("_id", StringComparison.OrdinalIgnoreCase)) return true; return false; } private string GetColumnCaption(string columnName) { Dictionary captions = new Dictionary { { "school_name", "Название автошколы" }, { "address", "Адрес" }, { "phone", "Телефон" }, { "school_display", "Автошкола" }, { "last_name", "Фамилия" }, { "first_name", "Имя" }, { "middle_name", "Отчество" }, { "license_number", "Номер удостоверения" }, { "birth_date", "Дата рождения" }, { "route_name", "Название маршрута" }, { "route_display", "Маршрут" }, { "start_address", "Начальный адрес" }, { "end_address", "Конечный адрес" }, { "difficulty", "Сложность" }, { "rank_name", "Звание" }, { "position_name", "Должность" }, { "driver_display", "Водитель" }, { "message", "Сообщение" }, { "inspector_display", "Инспектор" }, { "officer_display", "Сотрудник" }, { "exam_date", "Дата экзамена" }, { "result", "Результат" }, { "issue_date", "Дата выдачи" }, { "category", "Категория" }, { "login", "Логин" }, { "role", "Роль" }, { "is_active", "Активен" }, { "instructor_display", "Инструктор" }, { "driver_full_name", "Водитель" }, { "inspector_full_name", "Инспектор" }, { "attempt_number", "Попытка №" }, { "driver_age", "Возраст водителя" }, { "table_name", "Таблица" }, { "operation", "Операция" }, { "record_id", "ID записи" }, { "changed_at", "Дата и время" }, { "changed_by", "Пользователь" } }; return captions.ContainsKey(columnName) ? captions[columnName] : columnName; } private void BtnRefresh_Click(object sender, EventArgs e) { LoadCurrentTable(); } private void BtnAdd_Click(object sender, EventArgs e) { if (!UserSession.IsAdmin) { ShowReadonlyMessage(); return; } if (currentTable == null) return; if (currentTable.IsReadOnly) { MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (RecordEditForm form = new RecordEditForm(DbSettings.ConnectionString, currentTable, null)) { if (form.ShowDialog(this) == DialogResult.OK) { LoadCurrentTable(); LoadExamSearch(); } } } private void BtnEdit_Click(object sender, EventArgs e) { if (!UserSession.IsAdmin) { ShowReadonlyMessage(); return; } if (currentTable != null && currentTable.IsReadOnly) { MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (currentTable == null || dgvTables.SelectedRows.Count == 0) { MessageBox.Show("Выберите запись для изменения.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } DataRowView rowView = dgvTables.SelectedRows[0].DataBoundItem as DataRowView; if (rowView == null) return; using (RecordEditForm form = new RecordEditForm(DbSettings.ConnectionString, currentTable, rowView.Row)) { if (form.ShowDialog(this) == DialogResult.OK) { LoadCurrentTable(); LoadExamSearch(); } } } private void BtnDelete_Click(object sender, EventArgs e) { if (!UserSession.IsAdmin) { ShowReadonlyMessage(); return; } if (currentTable != null && currentTable.IsReadOnly) { MessageBox.Show("Этот раздел предназначен только для просмотра.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (currentTable == null || dgvTables.SelectedRows.Count == 0) { MessageBox.Show("Выберите запись для удаления.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } DataRowView rowView = dgvTables.SelectedRows[0].DataBoundItem as DataRowView; if (rowView == null) return; object pkValue = rowView.Row[currentTable.PrimaryKey]; if (currentTable.Name == "app_users" && Convert.ToInt32(pkValue) == UserSession.CurrentUser.UserId) { MessageBox.Show("Нельзя удалить текущую учётную запись, под которой выполнен вход.", "Удаление запрещено", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } DialogResult result = MessageBox.Show("Удалить выбранную запись?", "Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return; try { using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString)) { connection.Open(); DbAppContext.Apply(connection); string sql = "delete from " + FullName(currentTable.Name) + " where " + Quote(currentTable.PrimaryKey) + " = @id"; using (NpgsqlCommand command = new NpgsqlCommand(sql, connection)) { command.Parameters.AddWithValue("@id", pkValue); command.ExecuteNonQuery(); DbAppContext.MarkLatestAuditLogin(connection, currentTable.Name, pkValue); } } LoadCurrentTable(); LoadExamSearch(); } catch (Exception ex) { MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось удалить запись."), "Ошибка удаления", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void ShowReadonlyMessage() { MessageBox.Show("Обычный пользователь работает только с личным кабинетом. Изменение данных доступно только роли admin.", "Недостаточно прав", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void BtnExportTable_Click(object sender, EventArgs e) { ExportTable(dgvTables, currentTable == null ? "table" : currentTable.Name); } private void BtnSearch_Click(object sender, EventArgs e) { LoadExamSearch(); } private void BtnReset_Click(object sender, EventArgs e) { txtDriverFilter.Clear(); txtSchoolFilter.Clear(); cmbResultFilter.SelectedIndex = 0; dtFrom.Checked = false; dtTo.Checked = false; LoadExamSearch(); } private void BtnExportSearch_Click(object sender, EventArgs e) { ExportTable(dgvSearch, "exam_search"); } private void LoadExamSearch() { try { using (NpgsqlConnection connection = new NpgsqlConnection(DbSettings.ConnectionString)) { connection.Open(); const string sql = @" select exam_id, exam_date, result, driver_full_name, school_name, route_name, inspector_full_name, attempt_number, driver_age from akulova_vs.v_exam_search where (cast(@driver as text) = '' or driver_full_name ilike '%' || cast(@driver as text) || '%') and (cast(@school as text) = '' or school_name ilike '%' || cast(@school as text) || '%') and (cast(@result as text) = '' or result = cast(@result as text)) and (cast(@date_from as date) is null or exam_date >= cast(@date_from as date)) and (cast(@date_to as date) is null or exam_date <= cast(@date_to as date)) order by exam_date desc, exam_id desc;"; using (NpgsqlCommand command = new NpgsqlCommand(sql, connection)) { command.Parameters.AddWithValue("@driver", txtDriverFilter == null ? "" : txtDriverFilter.Text.Trim()); command.Parameters.AddWithValue("@school", txtSchoolFilter == null ? "" : txtSchoolFilter.Text.Trim()); string selectedResult = cmbResultFilter == null || cmbResultFilter.SelectedIndex <= 0 ? "" : cmbResultFilter.SelectedItem.ToString(); command.Parameters.AddWithValue("@result", selectedResult); Npgsql.NpgsqlParameter dateFromParameter = command.Parameters.Add("@date_from", NpgsqlTypes.NpgsqlDbType.Date); dateFromParameter.Value = dtFrom != null && dtFrom.Checked ? (object)dtFrom.Value.Date : DBNull.Value; Npgsql.NpgsqlParameter dateToParameter = command.Parameters.Add("@date_to", NpgsqlTypes.NpgsqlDbType.Date); dateToParameter.Value = dtTo != null && dtTo.Checked ? (object)dtTo.Value.Date : DBNull.Value; using (NpgsqlDataAdapter adapter = new NpgsqlDataAdapter(command)) { currentSearchData = new DataTable(); adapter.Fill(currentSearchData); if (dgvSearch != null) { dgvSearch.DataSource = currentSearchData; ApplyGridPresentation(dgvSearch); } } } } } catch (Exception ex) { if (dgvSearch != null) { MessageBox.Show(AppError.ToUserMessage(ex, "Не удалось выполнить поиск по экзаменам."), "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void ExportTable(DataGridView grid, string filePrefix) { if (grid == null || grid.DataSource == null || grid.Rows.Count == 0) { MessageBox.Show("Нет данных для экспорта.", "Информация", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (SaveFileDialog dialog = new SaveFileDialog()) { dialog.Filter = "CSV файл (*.csv)|*.csv"; dialog.FileName = filePrefix + "_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".csv"; if (dialog.ShowDialog(this) != DialogResult.OK) return; try { WriteCsv(grid, dialog.FileName); MessageBox.Show("CSV-файл сохранён:\n" + dialog.FileName, "Экспорт выполнен", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show("Не удалось сохранить CSV.\n\n" + ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void WriteCsv(DataGridView grid, string path) { List visibleColumns = new List(); foreach (DataGridViewColumn column in grid.Columns) { if (column.Visible) visibleColumns.Add(column); } using (StreamWriter writer = new StreamWriter(path, false, new UTF8Encoding(true))) { for (int i = 0; i < visibleColumns.Count; i++) { if (i > 0) writer.Write(';'); writer.Write(EscapeCsv(visibleColumns[i].HeaderText)); } writer.WriteLine(); foreach (DataGridViewRow row in grid.Rows) { if (row.IsNewRow) continue; for (int i = 0; i < visibleColumns.Count; i++) { if (i > 0) writer.Write(';'); object value = row.Cells[visibleColumns[i].Name].Value; writer.Write(EscapeCsv(value == null || value == DBNull.Value ? "" : value.ToString())); } writer.WriteLine(); } } } private string EscapeCsv(string value) { if (value == null) return ""; bool needQuotes = value.Contains(";") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"); value = value.Replace("\"", "\"\""); return needQuotes ? "\"" + value + "\"" : value; } private void BtnLogout_Click(object sender, EventArgs e) { UserSession.Logout(); Close(); } public static string FullName(string identifier) { return Quote(DbSchema) + "." + Quote(identifier); } public static string Quote(string identifier) { return "\"" + identifier.Replace("\"", "\"\"") + "\""; } } public class TableInfo { public string Name { get; private set; } public string DisplayName { get; private set; } public string PrimaryKey { get; private set; } public bool IsReadOnly { get; private set; } public TableInfo(string name, string displayName, string primaryKey) : this(name, displayName, primaryKey, false) { } public TableInfo(string name, string displayName, string primaryKey, bool isReadOnly) { Name = name; DisplayName = displayName; PrimaryKey = primaryKey; IsReadOnly = isReadOnly; } public override string ToString() { return DisplayName; } } public class ColumnInfo { public string Name { get; set; } public string DataType { get; set; } public string UdtName { get; set; } public bool IsNullable { get; set; } public bool IsIdentity { get; set; } public bool IsPrimaryKey { get; set; } } public class ComboItem { public int? Id { get; private set; } public string Text { get; private set; } public ComboItem(int? id, string text) { Id = id; Text = text; } public override string ToString() { return Text; } } public class FieldBinding { public ColumnInfo Column { get; set; } public Control Control { get; set; } public bool IsPasswordField { get; set; } } }