using CsvHelper; using Npgsql; using System; using System.Data; using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace DeanOfficeApp { public partial class MainForm : Form { private readonly string _connString = "Host=245e1-rw.db.pub.dbaas.postgrespro.ru;Port=5432;Database=dbpractice;Username=mardasov_mv;Password=A4%2x$$2Ynr"; private readonly string _userRole; public MainForm(string userRole, string username, string fullName) { InitializeComponent(); _userRole = userRole; this.Text = $"Деканат - {fullName} ({userRole})"; bool canEdit = (userRole == "admin" || userRole == "manager"); gbDisciplines.Visible = canEdit; btnGrades.Visible = canEdit; btnAssignments.Visible = canEdit; numHours.Minimum = 1; numHours.Maximum = 1000; numHours.Value = numHours.Minimum; // Подписываемся на события (если в дизайнере не подписаны) cmbGroups.SelectedIndexChanged += cmbGroups_SelectedIndexChanged; rbAsc.CheckedChanged += rbSort_CheckedChanged; rbDesc.CheckedChanged += rbSort_CheckedChanged; // Асинхронная загрузка начальных данных _ = LoadInitialDataAsync(); } private async Task LoadInitialDataAsync() { try { Cursor = Cursors.WaitCursor; await LoadGroupsAsync(); if (_userRole == "admin" || _userRole == "manager") { await LoadDisciplinesAsync(); await LoadTeachersAsync(); } } finally { Cursor = Cursors.Default; } } private async Task LoadGroupsAsync() { try { using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var cmd = new NpgsqlCommand("SELECT group_id, name FROM \"Group\" ORDER BY name", conn); var dt = new DataTable(); dt.Load(await cmd.ExecuteReaderAsync()); cmbGroups.DisplayMember = "name"; cmbGroups.ValueMember = "group_id"; cmbGroups.DataSource = dt; if (cmbGroups.Items.Count > 0) cmbGroups.SelectedIndex = 0; // автоматически выбрать первую группу } } catch (Exception ex) { MessageBox.Show($"Ошибка загрузки групп: {ex.Message}"); } } private async Task LoadTeachersAsync() { try { using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var cmd = new NpgsqlCommand("SELECT teacher_id, full_name FROM Teacher ORDER BY full_name", conn); var dt = new DataTable(); dt.Load(await cmd.ExecuteReaderAsync()); cmbTeachers.DisplayMember = "full_name"; cmbTeachers.ValueMember = "teacher_id"; cmbTeachers.DataSource = dt; if (cmbTeachers.Items.Count > 0) cmbTeachers.SelectedIndex = 0; } } catch (Exception ex) { MessageBox.Show($"Ошибка загрузки преподавателей: {ex.Message}"); } } private async Task LoadDisciplinesAsync() { try { using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var cmd = new NpgsqlCommand("SELECT discipline_id, name, hours FROM Discipline ORDER BY name", conn); var dt = new DataTable(); dt.Load(await cmd.ExecuteReaderAsync()); dgvDisciplines.DataSource = dt; if (dgvDisciplines.Columns.Contains("discipline_id")) dgvDisciplines.Columns["discipline_id"].Visible = false; } dgvDisciplines.ClearSelection(); txtDiscName.Clear(); numHours.Value = numHours.Minimum; await RefreshScheduleAsync(); } catch (Exception ex) { MessageBox.Show($"Ошибка загрузки дисциплин: {ex.Message}"); } } private async Task RefreshScheduleAsync() { if (cmbGroups.SelectedValue != null) await LoadScheduleAsync((int)cmbGroups.SelectedValue); } private async Task LoadScheduleAsync(int groupId) { // Безопасная сортировка: проверяем, что выбранное значение допустимо string sortDirection = rbAsc.Checked ? "ASC" : "DESC"; // Используем параметризованный запрос, но ORDER BY нельзя параметризовать, // поэтому проверяем значение перед вставкой. if (sortDirection != "ASC" && sortDirection != "DESC") sortDirection = "ASC"; string query = $@" SELECT d.name AS Дисциплина, d.hours AS Часы, t.full_name AS Преподаватель FROM Assignment a JOIN Discipline d ON a.discipline_id = d.discipline_id JOIN Teacher t ON a.teacher_id = t.teacher_id WHERE a.group_id = @gid ORDER BY d.hours {sortDirection}"; try { Cursor = Cursors.WaitCursor; using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); using (var cmd = new NpgsqlCommand(query, conn)) { cmd.Parameters.AddWithValue("@gid", groupId); var dt = new DataTable(); dt.Load(await cmd.ExecuteReaderAsync()); dgvSchedule.DataSource = dt; } } } catch (Exception ex) { MessageBox.Show($"Ошибка загрузки расписания: {ex.Message}"); } finally { Cursor = Cursors.Default; } } // Обработчик выбора группы private async void cmbGroups_SelectedIndexChanged(object sender, EventArgs e) { await RefreshScheduleAsync(); } // Обработчик изменения сортировки private async void rbSort_CheckedChanged(object sender, EventArgs e) { await RefreshScheduleAsync(); } // Кнопка "Загрузить расписание" (можно оставить для ручного обновления) private async void btnLoad_Click(object sender, EventArgs e) { await RefreshScheduleAsync(); } private async void btnAdd_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtDiscName.Text)) { MessageBox.Show("Введите название дисциплины."); return; } if (numHours.Value < numHours.Minimum) { MessageBox.Show($"Количество часов должно быть не менее {numHours.Minimum}."); return; } if (cmbGroups.SelectedValue == null) { MessageBox.Show("Выберите группу, к которой будет привязана дисциплина."); return; } if (cmbTeachers.SelectedValue == null) { MessageBox.Show("Выберите преподавателя."); return; } int groupId = (int)cmbGroups.SelectedValue; int teacherId = (int)cmbTeachers.SelectedValue; string discName = txtDiscName.Text.Trim(); int hours = (int)numHours.Value; try { Cursor = Cursors.WaitCursor; using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var cmdInsert = new NpgsqlCommand( "INSERT INTO Discipline (name, hours) VALUES (@n, @h) RETURNING discipline_id", conn); cmdInsert.Parameters.AddWithValue("@n", discName); cmdInsert.Parameters.AddWithValue("@h", hours); int newDisciplineId = (int)await cmdInsert.ExecuteScalarAsync(); var cmdAssign = new NpgsqlCommand( "INSERT INTO Assignment (group_id, discipline_id, teacher_id, semester) VALUES (@g, @d, @t, 1)", conn); cmdAssign.Parameters.AddWithValue("@g", groupId); cmdAssign.Parameters.AddWithValue("@d", newDisciplineId); cmdAssign.Parameters.AddWithValue("@t", teacherId); await cmdAssign.ExecuteNonQueryAsync(); } await LoadDisciplinesAsync(); txtDiscName.Clear(); numHours.Value = numHours.Minimum; MessageBox.Show("Дисциплина добавлена и привязана к группе и преподавателю."); } catch (Exception ex) { MessageBox.Show($"Ошибка: {ex.Message}"); } finally { Cursor = Cursors.Default; } } private async void btnUpdate_Click(object sender, EventArgs e) { if (dgvDisciplines.CurrentRow == null) { MessageBox.Show("Выберите дисциплину для обновления."); return; } int id = (int)dgvDisciplines.CurrentRow.Cells["discipline_id"].Value; if (string.IsNullOrWhiteSpace(txtDiscName.Text)) { MessageBox.Show("Введите название."); return; } if (numHours.Value < numHours.Minimum) { MessageBox.Show($"Количество часов должно быть не менее {numHours.Minimum}."); return; } try { Cursor = Cursors.WaitCursor; using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var cmd = new NpgsqlCommand("UPDATE Discipline SET name=@n, hours=@h WHERE discipline_id=@id", conn); cmd.Parameters.AddWithValue("@n", txtDiscName.Text.Trim()); cmd.Parameters.AddWithValue("@h", (int)numHours.Value); cmd.Parameters.AddWithValue("@id", id); await cmd.ExecuteNonQueryAsync(); } await LoadDisciplinesAsync(); MessageBox.Show("Дисциплина обновлена."); } catch (Exception ex) { MessageBox.Show($"Ошибка: {ex.Message}"); } finally { Cursor = Cursors.Default; } } private async void btnDelete_Click(object sender, EventArgs e) { if (dgvDisciplines.CurrentRow == null) { MessageBox.Show("Выберите дисциплину для удаления."); return; } int id = (int)dgvDisciplines.CurrentRow.Cells["discipline_id"].Value; if (MessageBox.Show("Удалить дисциплину? Это также удалит все её связи (Assignment).", "Подтверждение", MessageBoxButtons.YesNo) == DialogResult.Yes) { try { Cursor = Cursors.WaitCursor; using (var conn = new NpgsqlConnection(_connString)) { await conn.OpenAsync(); var delAssign = new NpgsqlCommand("DELETE FROM Assignment WHERE discipline_id = @id", conn); delAssign.Parameters.AddWithValue("@id", id); await delAssign.ExecuteNonQueryAsync(); var delDisc = new NpgsqlCommand("DELETE FROM Discipline WHERE discipline_id = @id", conn); delDisc.Parameters.AddWithValue("@id", id); await delDisc.ExecuteNonQueryAsync(); } await LoadDisciplinesAsync(); txtDiscName.Clear(); numHours.Value = numHours.Minimum; MessageBox.Show("Дисциплина и связанные назначения удалены."); } catch (Exception ex) { MessageBox.Show($"Ошибка: {ex.Message}"); } finally { Cursor = Cursors.Default; } } } private void btnExportCsv_Click(object sender, EventArgs e) { if (dgvSchedule.Rows.Count == 0) { MessageBox.Show("Нет данных для экспорта."); return; } using (var sfd = new SaveFileDialog() { Filter = "CSV|*.csv", FileName = "schedule.csv" }) { if (sfd.ShowDialog() == DialogResult.OK) { using (var writer = new StreamWriter(sfd.FileName, false, Encoding.UTF8)) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { foreach (DataGridViewColumn col in dgvSchedule.Columns) csv.WriteField(col.HeaderText); csv.NextRecord(); foreach (DataGridViewRow row in dgvSchedule.Rows) { if (!row.IsNewRow) { foreach (DataGridViewCell cell in row.Cells) csv.WriteField(cell.Value?.ToString() ?? ""); csv.NextRecord(); } } } MessageBox.Show("CSV сохранён."); } } } private void btnExportHtml_Click(object sender, EventArgs e) { if (dgvSchedule.Rows.Count == 0) { MessageBox.Show("Нет данных для формирования отчёта."); return; } using (var sfd = new SaveFileDialog() { Filter = "HTML files (*.html)|*.html", FileName = $"report_{cmbGroups.Text}.html" }) { if (sfd.ShowDialog() == DialogResult.OK) { string groupName = cmbGroups.Text.Trim(); string currentDate = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"); int disciplinesCount = dgvSchedule.Rows.Count; StringBuilder sb = new StringBuilder(); sb.AppendLine("
Дата: {currentDate}
Всего дисциплин: {disciplinesCount}
"); sb.AppendLine("| Дисциплина | Часы | Преподаватель |
|---|---|---|
| {cell.Value} | "); sb.AppendLine("