TheLim148 1 周之前
當前提交
44aa8bc8a7

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+# Visual Studio
+.vs/
+*.user
+*.suo
+
+# Build results
+bin/
+obj/
+publish-win-x64/
+packages/
+
+# Debug and temporary files
+*.log
+*.tmp
+*.pdb
+
+# Documents from PM11
+*.docx
+*.pptx
+

+ 3 - 0
MuseumWinForms/MuseumWinForms.slnx

@@ -0,0 +1,3 @@
+<Solution>
+  <Project Path="MuseumWinForms/MuseumWinForms.csproj" Id="ee9816d1-0298-4a0d-ae1f-c1392faa8ec7" />
+</Solution>

+ 34 - 0
MuseumWinForms/MuseumWinForms/App.config

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
+    </startup>
+  <runtime>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+</configuration>

+ 51 - 0
MuseumWinForms/MuseumWinForms/DatabaseHelper.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Data;
+using Npgsql;  
+
+namespace MuseumWinForms
+{
+    public static class DatabaseHelper
+    {
+        // Строка подключения для PostgreSQL
+        private static string connectionString = "Server=edu-pg.itiscaf.ru;Port=5432;Database=dbwork;UserId=dyachkov_ya;Password=i5G&l405V27;";
+
+        public static NpgsqlConnection GetConnection()
+        {
+            return new NpgsqlConnection(connectionString);
+        }
+
+        // Вызов PostgreSQL функции
+        public static int GetExhibitCountByAuthor(int authorId)
+        {
+            using (var conn = GetConnection())
+            {
+                conn.Open();
+                // PostgreSQL вызывает функцию через SELECT
+                string sql = "SELECT get_exhibit_count_by_author(@author_id)";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                {
+                    cmd.Parameters.AddWithValue("@author_id", authorId);
+                    object result = cmd.ExecuteScalar();
+                    return Convert.ToInt32(result);
+                }
+            }
+        }
+
+        // Получить всех авторов
+        public static DataTable GetAllCreators()
+        {
+            using (var conn = GetConnection())
+            {
+                conn.Open();
+                string sql = "SELECT id_creator, full_name FROM Creator ORDER BY id_creator";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                using (var adapter = new NpgsqlDataAdapter(cmd))
+                {
+                    DataTable dt = new DataTable();
+                    adapter.Fill(dt);
+                    return dt;
+                }
+            }
+        }
+    }
+}

+ 86 - 0
MuseumWinForms/MuseumWinForms/ExhibitRepository.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Data;
+using Npgsql;  // Вместо MySql.Data.MySqlClient
+
+namespace MuseumWinForms
+{
+    public static class ExhibitRepository
+    {
+        private static string connectionString = "Server=edu-pg.itiscaf.ru;Port=5432;Database=dbwork;UserId=dyachkov_ya;Password=i5G&l405V27;";
+
+        public static DataTable GetAllExhibits()
+        {
+            using (var conn = new NpgsqlConnection(connectionString))
+            {
+                conn.Open();
+                string sql = @"SELECT e.id_exhibit AS ""ID"", 
+                                      e.type AS ""Тип"", 
+                                      e.creation_date AS ""Дата создания"",
+                                      e.copy_price AS ""Цена"",
+                                      e.creation_cost AS ""Себестоимость"",
+                                      c.full_name AS ""Автор"",
+                                      e.id_museum AS ""ID музея""
+                               FROM Exhibit e
+                               LEFT JOIN Creator c ON e.id_creator = c.id_creator
+                               ORDER BY e.id_exhibit";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                using (var adapter = new NpgsqlDataAdapter(cmd))
+                {
+                    DataTable dt = new DataTable();
+                    adapter.Fill(dt);
+                    return dt;
+                }
+            }
+        }
+
+        public static void AddExhibit(int id, string type, DateTime date, decimal price, decimal cost, int creatorId, int museumId)
+        {
+            using (var conn = new NpgsqlConnection(connectionString))
+            {
+                conn.Open();
+                string sql = @"INSERT INTO Exhibit (id_exhibit, type, creation_date, copy_price, creation_cost, id_creator, id_museum) 
+                               VALUES (@id, @type, @date, @price, @cost, @creatorId, @museumId)";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                {
+                    cmd.Parameters.AddWithValue("@id", id);
+                    cmd.Parameters.AddWithValue("@type", type);
+                    cmd.Parameters.AddWithValue("@date", date);
+                    cmd.Parameters.AddWithValue("@price", price);
+                    cmd.Parameters.AddWithValue("@cost", cost);
+                    cmd.Parameters.AddWithValue("@creatorId", creatorId);
+                    cmd.Parameters.AddWithValue("@museumId", museumId);
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        public static void UpdateExhibitPrice(int exhibitId, decimal newPrice)
+        {
+            using (var conn = new NpgsqlConnection(connectionString))
+            {
+                conn.Open();
+                string sql = "UPDATE Exhibit SET copy_price = @price WHERE id_exhibit = @id";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                {
+                    cmd.Parameters.AddWithValue("@price", newPrice);
+                    cmd.Parameters.AddWithValue("@id", exhibitId);
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        public static void DeleteExhibit(int exhibitId)
+        {
+            using (var conn = new NpgsqlConnection(connectionString))
+            {
+                conn.Open();
+                string sql = "DELETE FROM Exhibit WHERE id_exhibit = @id";
+                using (var cmd = new NpgsqlCommand(sql, conn))
+                {
+                    cmd.Parameters.AddWithValue("@id", exhibitId);
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+    }
+}

+ 95 - 0
MuseumWinForms/MuseumWinForms/InputDialog.cs

@@ -0,0 +1,95 @@
+using System;
+using System.Windows.Forms;
+
+namespace MuseumWinForms
+{
+    public partial class InputDialog : Form
+    {
+        public string InputText { get; private set; }
+
+        public InputDialog(string prompt, string title, string defaultValue = "")
+        {
+            InitializeComponent();
+            this.Text = title;
+            lblPrompt.Text = prompt;
+            txtInput.Text = defaultValue;
+        }
+
+        private void btnOK_Click(object sender, EventArgs e)
+        {
+            InputText = txtInput.Text;
+            this.DialogResult = DialogResult.OK;
+            this.Close();
+        }
+
+        private void btnCancel_Click(object sender, EventArgs e)
+        {
+            this.DialogResult = DialogResult.Cancel;
+            this.Close();
+        }
+
+        private void InitializeComponent()
+        {
+            this.lblPrompt = new Label();
+            this.txtInput = new TextBox();
+            this.btnOK = new Button();
+            this.btnCancel = new Button();
+            this.SuspendLayout();
+
+            // lblPrompt
+            this.lblPrompt.AutoSize = true;
+            this.lblPrompt.Location = new System.Drawing.Point(12, 15);
+            this.lblPrompt.Size = new System.Drawing.Size(100, 20);
+            this.lblPrompt.Text = "Prompt";
+
+            // txtInput
+            this.txtInput.Location = new System.Drawing.Point(12, 40);
+            this.txtInput.Size = new System.Drawing.Size(260, 22);
+
+            // btnOK
+            this.btnOK.Location = new System.Drawing.Point(110, 75);
+            this.btnOK.Size = new System.Drawing.Size(75, 30);
+            this.btnOK.Text = "OK";
+            this.btnOK.UseVisualStyleBackColor = true;
+            this.btnOK.Click += new EventHandler(btnOK_Click);
+
+            // btnCancel
+            this.btnCancel.Location = new System.Drawing.Point(195, 75);
+            this.btnCancel.Size = new System.Drawing.Size(75, 30);
+            this.btnCancel.Text = "Отмена";
+            this.btnCancel.UseVisualStyleBackColor = true;
+            this.btnCancel.Click += new EventHandler(btnCancel_Click);
+
+            // Form
+            this.ClientSize = new System.Drawing.Size(284, 121);
+            this.Controls.Add(this.lblPrompt);
+            this.Controls.Add(this.txtInput);
+            this.Controls.Add(this.btnOK);
+            this.Controls.Add(this.btnCancel);
+            this.FormBorderStyle = FormBorderStyle.FixedDialog;
+            this.StartPosition = FormStartPosition.CenterParent;
+            this.MaximizeBox = false;
+            this.MinimizeBox = false;
+            this.ResumeLayout(false);
+            this.PerformLayout();
+        }
+
+        private Label lblPrompt;
+        private TextBox txtInput;
+        private Button btnOK;
+        private Button btnCancel;
+    }
+
+    public static class InputBox
+    {
+        public static string Show(string prompt, string title, string defaultValue = "")
+        {
+            using (var dialog = new InputDialog(prompt, title, defaultValue))
+            {
+                if (dialog.ShowDialog() == DialogResult.OK)
+                    return dialog.InputText;
+                return "";
+            }
+        }
+    }
+}

+ 194 - 0
MuseumWinForms/MuseumWinForms/MainForm.Designer.cs

@@ -0,0 +1,194 @@
+using System.Windows.Forms;
+
+namespace MuseumWinForms
+{
+    partial class MainForm
+    {
+        private System.ComponentModel.IContainer components = null;
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+                components.Dispose();
+            base.Dispose(disposing);
+        }
+
+        private void InitializeComponent()
+        {
+            this.dgvExhibits = new DataGridView();
+            this.btnRefresh = new Button();
+            this.btnAdd = new Button();
+            this.btnUpdate = new Button();
+            this.btnDelete = new Button();
+            this.btnTestProcedure = new Button();
+            this.gbInput = new GroupBox();
+            this.txtId = new TextBox();
+            this.txtType = new TextBox();
+            this.dtpDate = new DateTimePicker();
+            this.txtPrice = new TextBox();
+            this.txtCost = new TextBox();
+            this.cmbCreator = new ComboBox();
+            this.txtMuseumId = new TextBox();
+            this.lblId = new Label();
+            this.lblType = new Label();
+            this.lblDate = new Label();
+            this.lblPrice = new Label();
+            this.lblCost = new Label();
+            this.lblCreator = new Label();
+            this.lblMuseumId = new Label();
+            this.lblResult = new Label();
+            ((System.ComponentModel.ISupportInitialize)(this.dgvExhibits)).BeginInit();
+            this.gbInput.SuspendLayout();
+            this.SuspendLayout();
+
+            // dgvExhibits
+            this.dgvExhibits.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+            this.dgvExhibits.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+            this.dgvExhibits.Location = new System.Drawing.Point(12, 12);
+            this.dgvExhibits.Size = new System.Drawing.Size(860, 300);
+            this.dgvExhibits.TabIndex = 0;
+            this.dgvExhibits.SelectionChanged += new System.EventHandler(this.dgvExhibits_SelectionChanged);
+
+            // btnRefresh
+            this.btnRefresh.Location = new System.Drawing.Point(12, 320);
+            this.btnRefresh.Size = new System.Drawing.Size(100, 35);
+            this.btnRefresh.Text = "Обновить";
+            this.btnRefresh.UseVisualStyleBackColor = true;
+            this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
+
+            // btnAdd
+            this.btnAdd.Location = new System.Drawing.Point(118, 320);
+            this.btnAdd.Size = new System.Drawing.Size(100, 35);
+            this.btnAdd.Text = "Добавить";
+            this.btnAdd.UseVisualStyleBackColor = true;
+            this.btnAdd.Click += new System.EventHandler(this.btnAdd_Click);
+
+            // btnUpdate
+            this.btnUpdate.Location = new System.Drawing.Point(224, 320);
+            this.btnUpdate.Size = new System.Drawing.Size(100, 35);
+            this.btnUpdate.Text = "Изменить цену";
+            this.btnUpdate.UseVisualStyleBackColor = true;
+            this.btnUpdate.Click += new System.EventHandler(this.btnUpdate_Click);
+
+            // btnDelete
+            this.btnDelete.Location = new System.Drawing.Point(330, 320);
+            this.btnDelete.Size = new System.Drawing.Size(100, 35);
+            this.btnDelete.Text = "Удалить";
+            this.btnDelete.UseVisualStyleBackColor = true;
+            this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);
+
+            // btnTestProcedure
+            this.btnTestProcedure.Location = new System.Drawing.Point(436, 320);
+            this.btnTestProcedure.Size = new System.Drawing.Size(150, 35);
+            this.btnTestProcedure.Text = "Тест процедуры";
+            this.btnTestProcedure.UseVisualStyleBackColor = true;
+            this.btnTestProcedure.Click += new System.EventHandler(this.btnTestProcedure_Click);
+
+            // gbInput
+            this.gbInput.Controls.Add(this.txtId);
+            this.gbInput.Controls.Add(this.txtType);
+            this.gbInput.Controls.Add(this.dtpDate);
+            this.gbInput.Controls.Add(this.txtPrice);
+            this.gbInput.Controls.Add(this.txtCost);
+            this.gbInput.Controls.Add(this.cmbCreator);
+            this.gbInput.Controls.Add(this.txtMuseumId);
+            this.gbInput.Controls.Add(this.lblId);
+            this.gbInput.Controls.Add(this.lblType);
+            this.gbInput.Controls.Add(this.lblDate);
+            this.gbInput.Controls.Add(this.lblPrice);
+            this.gbInput.Controls.Add(this.lblCost);
+            this.gbInput.Controls.Add(this.lblCreator);
+            this.gbInput.Controls.Add(this.lblMuseumId);
+            this.gbInput.Location = new System.Drawing.Point(12, 370);
+            this.gbInput.Size = new System.Drawing.Size(860, 150);
+            this.gbInput.TabIndex = 1;
+            this.gbInput.TabStop = false;
+            this.gbInput.Text = "Данные для добавления";
+
+            // txtId
+            this.txtId.Location = new System.Drawing.Point(100, 25);
+            this.txtId.Size = new System.Drawing.Size(120, 22);
+            // txtType
+            this.txtType.Location = new System.Drawing.Point(100, 55);
+            this.txtType.Size = new System.Drawing.Size(120, 22);
+            // dtpDate
+            this.dtpDate.Location = new System.Drawing.Point(100, 85);
+            this.dtpDate.Size = new System.Drawing.Size(120, 22);
+            // txtPrice
+            this.txtPrice.Location = new System.Drawing.Point(320, 25);
+            this.txtPrice.Size = new System.Drawing.Size(120, 22);
+            // txtCost
+            this.txtCost.Location = new System.Drawing.Point(320, 55);
+            this.txtCost.Size = new System.Drawing.Size(120, 22);
+            // cmbCreator
+            this.cmbCreator.Location = new System.Drawing.Point(320, 85);
+            this.cmbCreator.Size = new System.Drawing.Size(120, 24);
+            // txtMuseumId
+            this.txtMuseumId.Location = new System.Drawing.Point(560, 25);
+            this.txtMuseumId.Size = new System.Drawing.Size(120, 22);
+
+            // Labels
+            this.lblId.Location = new System.Drawing.Point(20, 28);
+            this.lblId.Text = "ID:";
+            this.lblType.Location = new System.Drawing.Point(20, 58);
+            this.lblType.Text = "Тип:";
+            this.lblDate.Location = new System.Drawing.Point(20, 88);
+            this.lblDate.Text = "Дата:";
+            this.lblPrice.Location = new System.Drawing.Point(240, 28);
+            this.lblPrice.Text = "Цена:";
+            this.lblCost.Location = new System.Drawing.Point(240, 58);
+            this.lblCost.Text = "Себестоимость:";
+            this.lblCreator.Location = new System.Drawing.Point(240, 88);
+            this.lblCreator.Text = "Автор:";
+            this.lblMuseumId.Location = new System.Drawing.Point(480, 28);
+            this.lblMuseumId.Text = "ID музея:";
+
+            // lblResult
+            this.lblResult.Location = new System.Drawing.Point(600, 320);
+            this.lblResult.Size = new System.Drawing.Size(250, 35);
+            this.lblResult.Text = "";
+
+            // MainForm
+            this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
+            this.AutoScaleMode = AutoScaleMode.Font;
+            this.ClientSize = new System.Drawing.Size(884, 541);
+            this.Controls.Add(this.lblResult);
+            this.Controls.Add(this.gbInput);
+            this.Controls.Add(this.btnTestProcedure);
+            this.Controls.Add(this.btnDelete);
+            this.Controls.Add(this.btnUpdate);
+            this.Controls.Add(this.btnAdd);
+            this.Controls.Add(this.btnRefresh);
+            this.Controls.Add(this.dgvExhibits);
+            this.Text = "Музей изобразительных искусств - Учет экспонатов";
+
+            ((System.ComponentModel.ISupportInitialize)(this.dgvExhibits)).EndInit();
+            this.gbInput.ResumeLayout(false);
+            this.gbInput.PerformLayout();
+            this.ResumeLayout(false);
+        }
+
+        private DataGridView dgvExhibits;
+        private Button btnRefresh;
+        private Button btnAdd;
+        private Button btnUpdate;
+        private Button btnDelete;
+        private Button btnTestProcedure;
+        private GroupBox gbInput;
+        private TextBox txtId;
+        private TextBox txtType;
+        private DateTimePicker dtpDate;
+        private TextBox txtPrice;
+        private TextBox txtCost;
+        private ComboBox cmbCreator;
+        private TextBox txtMuseumId;
+        private Label lblId;
+        private Label lblType;
+        private Label lblDate;
+        private Label lblPrice;
+        private Label lblCost;
+        private Label lblCreator;
+        private Label lblMuseumId;
+        private Label lblResult;
+    }
+}

+ 168 - 0
MuseumWinForms/MuseumWinForms/MainForm.cs

@@ -0,0 +1,168 @@
+using System;
+using System.Data;
+using System.Windows.Forms;
+using MuseumWinForms;
+using Npgsql;
+
+namespace MuseumWinForms
+{
+    public partial class MainForm : Form
+    {
+        public MainForm()
+        {
+            InitializeComponent();
+            LoadData();
+            LoadCreators();
+        }
+
+        private void LoadData()
+        {
+            try
+            {
+                DataTable dt = ExhibitRepository.GetAllExhibits();
+                dgvExhibits.DataSource = dt;
+                dgvExhibits.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"Ошибка загрузки: {ex.Message}");
+            }
+        }
+
+        private void LoadCreators()
+        {
+            try
+            {
+                DataTable dt = DatabaseHelper.GetAllCreators();
+                cmbCreator.DisplayMember = "full_name";
+                cmbCreator.ValueMember = "id_creator";
+                cmbCreator.DataSource = dt;
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"Ошибка загрузки авторов: {ex.Message}");
+            }
+        }
+
+        private void btnRefresh_Click(object sender, EventArgs e)
+        {
+            LoadData();
+            lblResult.Text = "Данные обновлены";
+        }
+
+        private void btnAdd_Click(object sender, EventArgs e)
+        {
+            try
+            {
+                int id = int.Parse(txtId.Text);
+                string type = txtType.Text;
+                DateTime date = dtpDate.Value;
+                decimal price = decimal.Parse(txtPrice.Text);
+                decimal cost = decimal.Parse(txtCost.Text);
+                int creatorId = (int)cmbCreator.SelectedValue;
+                int museumId = int.Parse(txtMuseumId.Text);
+
+                ExhibitRepository.AddExhibit(id, type, date, price, cost, creatorId, museumId);
+                LoadData();
+                ClearInputs();
+                lblResult.Text = "Экспонат добавлен!";
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"Ошибка добавления: {ex.Message}");
+            }
+        }
+
+        // ИСПРАВЛЕНО: без Microsoft.VisualBasic
+        private void btnUpdate_Click(object sender, EventArgs e)
+        {
+            if (dgvExhibits.CurrentRow == null)
+            {
+                MessageBox.Show("Выберите экспонат для изменения");
+                return;
+            }
+
+            try
+            {
+                int exhibitId = Convert.ToInt32(dgvExhibits.CurrentRow.Cells["ID"].Value);
+
+                // Используем свою форму InputBox вместо Microsoft.VisualBasic.Interaction.InputBox
+                string newPriceStr = InputBox.Show("Введите новую цену:", "Изменение цены", "");
+
+                if (!string.IsNullOrEmpty(newPriceStr) && decimal.TryParse(newPriceStr, out decimal newPrice))
+                {
+                    ExhibitRepository.UpdateExhibitPrice(exhibitId, newPrice);
+                    LoadData();
+                    lblResult.Text = "Цена обновлена!";
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"Ошибка изменения: {ex.Message}");
+            }
+        }
+
+        private void btnDelete_Click(object sender, EventArgs e)
+        {
+            if (dgvExhibits.CurrentRow == null)
+            {
+                MessageBox.Show("Выберите экспонат для удаления");
+                return;
+            }
+
+            DialogResult result = MessageBox.Show("Удалить выбранный экспонат?", "Подтверждение",
+                MessageBoxButtons.YesNo, MessageBoxIcon.Question);
+
+            if (result == DialogResult.Yes)
+            {
+                try
+                {
+                    int exhibitId = Convert.ToInt32(dgvExhibits.CurrentRow.Cells["ID"].Value);
+                    ExhibitRepository.DeleteExhibit(exhibitId);
+                    LoadData();
+                    lblResult.Text = "Экспонат удален!";
+                }
+                catch (Exception ex)
+                {
+                    MessageBox.Show($"Ошибка удаления: {ex.Message}");
+                }
+            }
+        }
+
+        // ИСПРАВЛЕНО: без Microsoft.VisualBasic
+        private void btnTestProcedure_Click(object sender, EventArgs e)
+        {
+            try
+            {
+                string authorIdStr = InputBox.Show("Введите ID автора:", "Тест хранимой процедуры", "1");
+
+                if (!string.IsNullOrEmpty(authorIdStr) && int.TryParse(authorIdStr, out int authorId))
+                {
+                    int count = DatabaseHelper.GetExhibitCountByAuthor(authorId);
+                    lblResult.Text = $"Автор с ID {authorId} создал {count} экспонатов";
+                }
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show($"Ошибка: {ex.Message}");
+            }
+        }
+
+        private void ClearInputs()
+        {
+            txtId.Text = "";
+            txtType.Text = "";
+            txtPrice.Text = "";
+            txtCost.Text = "";
+            txtMuseumId.Text = "";
+            dtpDate.Value = DateTime.Now;
+            if (cmbCreator.Items.Count > 0)
+                cmbCreator.SelectedIndex = 0;
+        }
+
+        private void dgvExhibits_SelectionChanged(object sender, EventArgs e)
+        {
+            // Опционально
+        }
+    }
+}

+ 147 - 0
MuseumWinForms/MuseumWinForms/MuseumWinForms.csproj

@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{EE9816D1-0298-4A0D-AE1F-C1392FAA8EC7}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <RootNamespace>MuseumWinForms</RootNamespace>
+    <AssemblyName>MuseumWinForms</AssemblyName>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <Deterministic>true</Deterministic>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="BouncyCastle.Cryptography, Version=2.0.0.0, Culture=neutral, PublicKeyToken=072edcf4a5328938, processorArchitecture=MSIL">
+      <HintPath>..\packages\BouncyCastle.Cryptography.2.6.2\lib\net461\BouncyCastle.Cryptography.dll</HintPath>
+    </Reference>
+    <Reference Include="Google.Protobuf, Version=3.32.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604, processorArchitecture=MSIL">
+      <HintPath>..\packages\Google.Protobuf.3.32.0\lib\net45\Google.Protobuf.dll</HintPath>
+    </Reference>
+    <Reference Include="K4os.Compression.LZ4, Version=1.3.8.0, Culture=neutral, PublicKeyToken=2186fa9121ef231d, processorArchitecture=MSIL">
+      <HintPath>..\packages\K4os.Compression.LZ4.1.3.8\lib\net462\K4os.Compression.LZ4.dll</HintPath>
+    </Reference>
+    <Reference Include="K4os.Compression.LZ4.Streams, Version=1.3.8.0, Culture=neutral, PublicKeyToken=2186fa9121ef231d, processorArchitecture=MSIL">
+      <HintPath>..\packages\K4os.Compression.LZ4.Streams.1.3.8\lib\net462\K4os.Compression.LZ4.Streams.dll</HintPath>
+    </Reference>
+    <Reference Include="K4os.Hash.xxHash, Version=1.0.8.0, Culture=neutral, PublicKeyToken=32cd54395057cec3, processorArchitecture=MSIL">
+      <HintPath>..\packages\K4os.Hash.xxHash.1.0.8\lib\net462\K4os.Hash.xxHash.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="MySql.Data, Version=9.7.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d, processorArchitecture=MSIL">
+      <HintPath>..\packages\MySql.Data.9.7.0\lib\net462\MySql.Data.dll</HintPath>
+    </Reference>
+    <Reference Include="Npgsql, Version=4.1.14.0, Culture=neutral, PublicKeyToken=5d8b90d52f46fda7, processorArchitecture=MSIL">
+      <HintPath>..\packages\Npgsql.4.1.14\lib\net461\Npgsql.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Configuration" />
+    <Reference Include="System.Configuration.ConfigurationManager, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Configuration.ConfigurationManager.8.0.0\lib\net462\System.Configuration.ConfigurationManager.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Core" />
+    <Reference Include="System.IO.Pipelines, Version=5.0.0.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.IO.Pipelines.5.0.2\lib\net461\System.IO.Pipelines.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Management" />
+    <Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Numerics" />
+    <Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Text.Encodings.Web, Version=4.0.4.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Text.Encodings.Web.4.6.0\lib\netstandard2.0\System.Text.Encodings.Web.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Text.Json, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Text.Json.4.6.0\lib\net461\System.Text.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Transactions" />
+    <Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Deployment" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Windows.Forms" />
+    <Reference Include="System.Xml" />
+    <Reference Include="ZstdSharp, Version=0.8.6.0, Culture=neutral, PublicKeyToken=8d151af33a4ad5cf, processorArchitecture=MSIL">
+      <HintPath>..\packages\ZstdSharp.Port.0.8.6\lib\net462\ZstdSharp.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ExhibitRepository.cs" />
+    <Compile Include="DatabaseHelper.cs" />
+    <Compile Include="InputDialog.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="MainForm.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="MainForm.Designer.cs">
+      <DependentUpon>MainForm.cs</DependentUpon>
+    </Compile>
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <None Include="packages.config" />
+    <None Include="Properties\Settings.settings">
+      <Generator>SettingsSingleFileGenerator</Generator>
+      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+    </None>
+    <Compile Include="Properties\Settings.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Settings.settings</DependentUpon>
+      <DesignTimeSharedInput>True</DesignTimeSharedInput>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>

+ 16 - 0
MuseumWinForms/MuseumWinForms/Program.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Windows.Forms;
+
+namespace MuseumWinForms
+{
+    internal static class Program
+    {
+        [STAThread]
+        static void Main()
+        {
+            Application.EnableVisualStyles();
+            Application.SetCompatibleTextRenderingDefault(false);
+            Application.Run(new MainForm());
+        }
+    }
+}

+ 33 - 0
MuseumWinForms/MuseumWinForms/Properties/AssemblyInfo.cs

@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// Общие сведения об этой сборке предоставляются следующим набором
+// набора атрибутов. Измените значения этих атрибутов для изменения сведений,
+// связанных со сборкой.
+[assembly: AssemblyTitle("MuseumWinForms")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MuseumWinForms")]
+[assembly: AssemblyCopyright("Copyright ©  2026")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
+// для компонентов COM. Если необходимо обратиться к типу в этой сборке через
+// COM, следует установить атрибут ComVisible в TRUE для этого типа.
+[assembly: ComVisible(false)]
+
+// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM
+[assembly: Guid("ee9816d1-0298-4a0d-ae1f-c1392faa8ec7")]
+
+// Сведения о версии сборки состоят из указанных ниже четырех значений:
+//
+//      Основной номер версии
+//      Дополнительный номер версии
+//      Номер сборки
+//      Редакция
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 71 - 0
MuseumWinForms/MuseumWinForms/Properties/Resources.Designer.cs

@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     Этот код создан программным средством.
+//     Версия среды выполнения: 4.0.30319.42000
+//
+//     Изменения в этом файле могут привести к неправильному поведению и будут утрачены, если
+//     код создан повторно.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MuseumWinForms.Properties
+{
+
+
+    /// <summary>
+    ///   Класс ресурсов со строгим типом для поиска локализованных строк и пр.
+    /// </summary>
+    // Этот класс был автоматически создан при помощи StronglyTypedResourceBuilder
+    // класс с помощью таких средств, как ResGen или Visual Studio.
+    // Для добавления или удаления члена измените файл .ResX, а затем перезапустите ResGen
+    // с параметром /str или заново постройте свой VS-проект.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   Возврат кэшированного экземпляра ResourceManager, используемого этим классом.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if ((resourceMan == null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MuseumWinForms.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   Переопределяет свойство CurrentUICulture текущего потока для всех
+        ///   подстановки ресурсов с помощью этого класса ресурсов со строгим типом.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+    }
+}

+ 117 - 0
MuseumWinForms/MuseumWinForms/Properties/Resources.resx

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>

+ 30 - 0
MuseumWinForms/MuseumWinForms/Properties/Settings.Designer.cs

@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MuseumWinForms.Properties
+{
+
+
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+    {
+
+        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+        public static Settings Default
+        {
+            get
+            {
+                return defaultInstance;
+            }
+        }
+    }
+}

+ 7 - 0
MuseumWinForms/MuseumWinForms/Properties/Settings.settings

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
+  <Profiles>
+    <Profile Name="(Default)" />
+  </Profiles>
+  <Settings />
+</SettingsFile>

+ 22 - 0
MuseumWinForms/MuseumWinForms/packages.config

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="BouncyCastle.Cryptography" version="2.6.2" targetFramework="net472" />
+  <package id="Google.Protobuf" version="3.32.0" targetFramework="net472" />
+  <package id="K4os.Compression.LZ4" version="1.3.8" targetFramework="net472" />
+  <package id="K4os.Compression.LZ4.Streams" version="1.3.8" targetFramework="net472" />
+  <package id="K4os.Hash.xxHash" version="1.0.8" targetFramework="net472" />
+  <package id="Microsoft.Bcl.AsyncInterfaces" version="5.0.0" targetFramework="net472" />
+  <package id="MySql.Data" version="9.7.0" targetFramework="net472" />
+  <package id="Npgsql" version="4.1.14" targetFramework="net472" />
+  <package id="System.Buffers" version="4.5.1" targetFramework="net472" />
+  <package id="System.Configuration.ConfigurationManager" version="8.0.0" targetFramework="net472" />
+  <package id="System.IO.Pipelines" version="5.0.2" targetFramework="net472" />
+  <package id="System.Memory" version="4.5.5" targetFramework="net472" />
+  <package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
+  <package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
+  <package id="System.Text.Encodings.Web" version="4.6.0" targetFramework="net472" />
+  <package id="System.Text.Json" version="4.6.0" targetFramework="net472" />
+  <package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net472" />
+  <package id="System.ValueTuple" version="4.5.0" targetFramework="net472" />
+  <package id="ZstdSharp.Port" version="0.8.6" targetFramework="net472" />
+</packages>

+ 179 - 0
README.md

@@ -0,0 +1,179 @@
+# Информационная система «Учёт экспонатов музея»
+
+# Аннотация
+
+«Учёт экспонатов музея» — клиентское Windows-приложение для просмотра и изменения сведений о музейных экспонатах. Программа разработана на C# с использованием Windows Forms, работает с отдельно развёрнутой базой данных PostgreSQL и подключается к ней с помощью библиотеки Npgsql.
+
+В репозитории размещается исходный код клиентской части информационной системы. Сервер PostgreSQL, дамп базы данных и средства администрирования базы не входят в установочный пакет приложения.
+
+# Введение
+
+Основные возможности программы:
+
+- просмотр списка экспонатов в табличном виде;
+- отображение типа экспоната, даты создания, цены, себестоимости, автора и идентификатора музея;
+- добавление новых экспонатов;
+- изменение цены выбранного экспоната;
+- удаление выбранного экспоната с подтверждением;
+- обновление сведений, загруженных из базы данных;
+- загрузка списка авторов;
+- подсчёт количества экспонатов, созданных указанным автором;
+- отображение результатов выполненных операций и сообщений об ошибках.
+
+В текущей версии программы отсутствуют отдельная форма авторизации и разграничение доступа по ролям. После запуска оператору доступны все реализованные функции.
+
+# Назначение и условия применения
+
+Программа предназначена для ведения сведений об экспонатах музея. Она может применяться сотрудником музея или ответственным оператором для просмотра каталога, добавления новых объектов, изменения их цены и удаления записей.
+
+Для работы необходимы:
+
+- компьютер под управлением Windows;
+- установленная платформа .NET Framework 4.7.2;
+- доступ к заранее развёрнутому серверу PostgreSQL;
+- корректно настроенные параметры подключения к базе данных;
+- наличие в базе таблиц `Exhibit`, `Creator` и `Museum`;
+- наличие функции `get_exhibit_count_by_author`;
+- права учётной записи базы данных на чтение и изменение используемых таблиц;
+- установленные вместе с приложением библиотеки, включая Npgsql.
+
+Клиентское приложение не устанавливает и не настраивает сервер PostgreSQL автоматически. Перед первым запуском база данных должна быть развёрнута и доступна по сети.
+
+# Установка
+
+1. [ ] Получить установочный файл `MuseumExhibitsSetup.exe`.
+2. [ ] Убедиться, что сервер PostgreSQL запущен и доступен с рабочего компьютера.
+3. [ ] При необходимости установить .NET Framework 4.7.2.
+4. [ ] Запустить `MuseumExhibitsSetup.exe`.
+5. [ ] Разрешить запуск установщика, если Windows выведет предупреждение.
+6. [ ] Выбрать папку установки или оставить предложенный путь.
+7. [ ] Подтвердить создание ярлыков в меню «Пуск» и на рабочем столе.
+8. [ ] Дождаться завершения копирования файлов приложения и зависимостей.
+9. [ ] Запустить программу через созданный ярлык.
+10. [ ] Убедиться, что в главном окне загрузились список экспонатов и список авторов.
+
+Удаление приложения выполняется через стандартный раздел Windows для удаления программ либо с помощью файла деинсталляции, созданного Inno Setup.
+
+Удаление клиентского приложения не удаляет базу данных с сервера PostgreSQL.
+
+# Описание операций
+
+## Просмотр экспонатов
+
+После запуска программа подключается к серверу PostgreSQL и загружает список экспонатов.
+
+В таблице отображаются:
+
+- идентификатор экспоната;
+- тип экспоната;
+- дата создания;
+- цена экземпляра;
+- себестоимость создания;
+- автор;
+- идентификатор музея.
+
+Для просмотра большого количества записей используются полосы прокрутки таблицы.
+
+## Обновление данных
+
+Кнопка **«Обновить»** повторно загружает сведения из базы данных.
+
+Эту операцию следует выполнять после изменения данных другим пользователем или непосредственно на сервере. После успешного обновления программа выводит сообщение «Данные обновлены».
+
+## Добавление экспоната
+
+Для добавления записи необходимо заполнить поля в группе «Данные для добавления»:
+
+- `ID` — уникальный числовой идентификатор экспоната;
+- `Тип` — наименование типа экспоната;
+- `Дата` — дата создания;
+- `Цена` — цена экземпляра;
+- `Себестоимость` — затраты на создание или приобретение;
+- `Автор` — значение, выбранное из списка авторов;
+- `ID музея` — идентификатор существующего музея.
+
+После заполнения полей следует нажать кнопку **«Добавить»**.
+
+Идентификатор экспоната должен быть уникальным. Выбранный автор и указанный музей должны существовать в базе данных. Цена и себестоимость вводятся в числовом формате, принятом в настройках Windows.
+
+После успешного добавления таблица обновляется, поля ввода очищаются, а программа выводит сообщение «Экспонат добавлен!».
+
+## Изменение цены
+
+Для изменения цены необходимо:
+
+1. Выбрать экспонат в таблице.
+2. Нажать кнопку **«Изменить цену»**.
+3. Ввести новое числовое значение.
+4. Подтвердить операцию.
+
+После успешного изменения таблица обновляется и появляется сообщение «Цена обновлена!».
+
+Другие сведения об экспонате в текущей версии приложения не редактируются.
+
+## Удаление экспоната
+
+Для удаления необходимо:
+
+1. Выбрать экспонат в таблице.
+2. Нажать кнопку **«Удалить»**.
+3. Проверить выбранную запись.
+4. Подтвердить удаление в диалоговом окне.
+
+Если выбрать ответ «Нет», операция будет отменена.
+
+Удаление может быть отклонено базой данных, если на экспонат ссылаются связанные записи.
+
+## Подсчёт количества экспонатов автора
+
+Кнопка **«Тест процедуры»** вызывает функцию подсчёта количества экспонатов автора.
+
+Для выполнения операции необходимо:
+
+1. Нажать кнопку **«Тест процедуры»**.
+2. Ввести числовой идентификатор автора.
+3. Подтвердить ввод.
+4. Просмотреть результат в нижней части главного окна.
+
+Программа вызывает функцию PostgreSQL `get_exhibit_count_by_author` и отображает количество экспонатов, связанных с указанным автором.
+
+# Сборка проекта
+
+Для сборки исходного кода необходимы:
+
+- Visual Studio с поддержкой проектов .NET Framework;
+- .NET Framework 4.7.2 Developer Pack;
+- восстановленные NuGet-пакеты проекта;
+- доступ к серверу PostgreSQL для проверки работы приложения.
+
+Рекомендуемый порядок сборки:
+
+1. Открыть решение `MuseumWinForms.sln` в Visual Studio.
+2. Восстановить NuGet-пакеты.
+3. Проверить параметры подключения к PostgreSQL.
+4. Выбрать конфигурацию `Release`.
+5. Выполнить сборку решения.
+6. Получить исполняемые файлы в каталоге `MuseumWinForms/bin/Release`.
+7. Добавить содержимое каталога Release в установочный пакет Inno Setup.
+
+Конфиденциальные параметры подключения, включая пароль к базе данных, не следует публиковать в открытом репозитории.
+
+# Термины и сокращения
+
+| Термин | Определение |
+|---|---|
+| БД | База данных, в которой хранятся сведения информационной системы |
+| СУБД | Система управления базами данных |
+| PostgreSQL | СУБД, используемая для централизованного хранения данных |
+| Клиентское приложение | Программа, установленная на компьютере оператора и подключающаяся к БД |
+| Экспонат | Объект, сведения о котором хранятся в музейном каталоге |
+| Автор | Создатель произведения или другого музейного объекта |
+| Музей | Организация, в которой хранится или учитывается экспонат |
+| Цена экземпляра | Указанная в базе данных стоимость экспоната |
+| Себестоимость | Затраты, связанные с созданием или приобретением экспоната |
+| Идентификатор | Уникальное числовое значение записи |
+| Windows Forms | Технология создания графических приложений для Windows |
+| .NET Framework | Программная платформа, необходимая для запуска приложения |
+| Npgsql | Библиотека для подключения приложений .NET к PostgreSQL |
+| Inno Setup | Средство создания установочного файла Windows |
+| Git | Система контроля версий исходного кода |

+ 2103 - 0
dump.sql

@@ -0,0 +1,2103 @@
+--
+-- PostgreSQL database dump
+--
+
+\restrict fMX8pe7fyPeennYB6eeK17lVC7rkBp0LxzYWCFMQgMflMYdB3BYjGmqxfiz2Aj2
+
+-- Dumped from database version 18.0
+-- Dumped by pg_dump version 18.4
+
+-- Started on 2026-06-30 18:28:52 MSK
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET transaction_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+--
+-- TOC entry 16 (class 2615 OID 442818)
+-- Name: dyachkov_ya; Type: SCHEMA; Schema: -; Owner: dyachkov_ya
+--
+
+CREATE SCHEMA dyachkov_ya;
+
+
+ALTER SCHEMA dyachkov_ya OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1403 (class 1255 OID 443423)
+-- Name: add_user_with_phones(character varying, character varying, integer, integer, integer, character varying, real, integer, character varying, character varying); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.add_user_with_phones(IN p_lastname character varying, IN p_name character varying, IN p_year_b integer, IN p_groups integer, IN p_age integer, IN p_category character varying DEFAULT NULL::character varying, IN p_debt real DEFAULT 0, IN p_level integer DEFAULT 1, IN p_home_phone character varying DEFAULT NULL::character varying, IN p_mobile_phone character varying DEFAULT NULL::character varying)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_has_phone BOOLEAN;
+BEGIN
+    BEGIN
+        -- Проверка наличия хотя бы одного телефона
+        v_has_phone := (p_home_phone IS NOT NULL AND p_home_phone != '') OR
+                       (p_mobile_phone IS NOT NULL AND p_mobile_phone != '');
+        
+        IF NOT v_has_phone THEN
+            RAISE EXCEPTION 'Необходимо указать хотя бы один телефон (домашний или мобильный) для связи с читателем';
+        END IF;
+        
+        -- Вставка пользователя
+        INSERT INTO users (lastname, name, year_b, groups, age, category, debt, level, home_phone, mobile_phone)
+        VALUES (p_lastname, p_name, p_year_b, p_groups, p_age, p_category, p_debt, p_level, p_home_phone, p_mobile_phone);
+        
+        COMMIT;
+        RAISE NOTICE 'Пользователь "% %" успешно добавлен', p_lastname, p_name;
+        RAISE NOTICE 'Телефоны: дом.%, моб.%', 
+                     COALESCE(p_home_phone, 'не указан'), 
+                     COALESCE(p_mobile_phone, 'не указан');
+        
+    EXCEPTION
+        WHEN OTHERS THEN
+            ROLLBACK;
+            RAISE NOTICE 'ОШИБКА: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.add_user_with_phones(IN p_lastname character varying, IN p_name character varying, IN p_year_b integer, IN p_groups integer, IN p_age integer, IN p_category character varying, IN p_debt real, IN p_level integer, IN p_home_phone character varying, IN p_mobile_phone character varying) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1404 (class 1255 OID 443424)
+-- Name: authenticate_user(character varying, text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.authenticate_user(p_username character varying, p_password text) RETURNS TABLE(user_id integer, username character varying, decrypted_name text, role_name character varying, is_authenticated boolean)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_user_record RECORD;
+    v_decrypted_name TEXT;
+BEGIN
+    -- Поиск пользователя
+    SELECT u.id, u.username, u.encrypted_name, u.password_hash, r.role_name
+    INTO v_user_record
+    FROM system_users u
+    JOIN roles r ON u.role_id = r.id
+    WHERE u.username = p_username AND u.is_active = TRUE;
+    
+    IF v_user_record.id IS NULL THEN
+        RETURN QUERY SELECT NULL::INTEGER, NULL::VARCHAR, NULL::TEXT, NULL::VARCHAR, FALSE::BOOLEAN;
+        RETURN;
+    END IF;
+    
+    -- Проверка пароля
+    IF v_user_record.password_hash = crypt(p_password, v_user_record.password_hash) THEN
+        -- Расшифровываем имя
+        BEGIN
+            v_decrypted_name := convert_from(decrypt(decode(v_user_record.encrypted_name, 'base64'), 'master_key', 'aes'), 'UTF8');
+        EXCEPTION WHEN OTHERS THEN
+            v_decrypted_name := '[Ошибка расшифровки]';
+        END;
+        
+        RETURN QUERY SELECT 
+            v_user_record.id, 
+            v_user_record.username, 
+            v_decrypted_name, 
+            v_user_record.role_name, 
+            TRUE::BOOLEAN;
+    ELSE
+        RETURN QUERY SELECT NULL::INTEGER, NULL::VARCHAR, NULL::TEXT, NULL::VARCHAR, FALSE::BOOLEAN;
+    END IF;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.authenticate_user(p_username character varying, p_password text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1405 (class 1255 OID 443425)
+-- Name: autonomous_log(integer, real, real); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.autonomous_log(p_book_bshifr integer, p_old_price real, p_new_price real) RETURNS void
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    -- Имитация автономной транзакции
+    BEGIN
+        INSERT INTO logs (bk, cena_old, cena_new)
+        VALUES (p_book_bshifr, p_old_price, p_new_price);
+        
+        -- Фиксируем лог независимо от основной транзакции
+        COMMIT;
+        RAISE NOTICE 'Лог сохранен автономно: книга bshifr=%, цена %.2f -> %.2f', 
+                     p_book_bshifr, p_old_price, p_new_price;
+    EXCEPTION
+        WHEN OTHERS THEN
+            RAISE NOTICE 'Не удалось сохранить лог: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.autonomous_log(p_book_bshifr integer, p_old_price real, p_new_price real) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1406 (class 1255 OID 443426)
+-- Name: check_ticket_constraints(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.check_ticket_constraints() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+user_exists BOOLEAN;
+book_exists BOOLEAN;
+BEGIN
+SELECT EXISTS (SELECT 1 FROM users WHERE id = NEW.user_id)
+INTO user_exists;
+IF NOT user_exists THEN
+RAISE EXCEPTION 'Ошибка: Пользователь с ID % не существует в таблице users. Запись в ticket не может быть добавлена.', NEW.user_id;
+END IF;
+SELECT EXISTS (SELECT 1 FROM book WHERE bshifr = NEW.book_id)
+INTO book_exists;
+IF NOT book_exists THEN
+RAISE EXCEPTION 'Ошибка: Книга с шифром % не существует в таблице book. Запись в ticket не может быть добавлена.', NEW.book_id;
+END IF;
+IF NEW.date_return IS NOT NULL AND NEW.date_of_issue IS NOT NULL THEN
+IF NEW.date_return < NEW.date_of_issue THEN
+RAISE EXCEPTION 'Ошибка: Дата возврата (%) не может быть раньше даты выдачи (%).', NEW.date_return, NEW.date_of_issue;
+END IF;
+END IF;
+RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.check_ticket_constraints() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1407 (class 1255 OID 443427)
+-- Name: check_user_access(character varying, character varying); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.check_user_access(p_username character varying, p_required_role character varying) RETURNS boolean
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_user_role VARCHAR(50);
+    v_has_access BOOLEAN := FALSE;
+BEGIN
+    -- Начинаем транзакцию
+    BEGIN
+        SELECT r.role_name INTO v_user_role
+        FROM system_users u
+        JOIN roles r ON u.role_id = r.id
+        WHERE u.username = p_username AND u.is_active = TRUE;
+        
+        -- Проверка роли (admin имеет полный доступ)
+        IF v_user_role = 'admin' OR v_user_role = p_required_role THEN
+            v_has_access := TRUE;
+        END IF;
+        
+        -- Логируем попытку доступа (в реальной системе)
+        INSERT INTO access_log (username, access_time, has_access, required_role)
+        VALUES (p_username, CURRENT_TIMESTAMP, v_has_access, p_required_role);
+        
+        COMMIT;
+    EXCEPTION WHEN OTHERS THEN
+        ROLLBACK;
+        RAISE NOTICE 'Ошибка при проверке доступа: %', SQLERRM;
+        v_has_access := FALSE;
+    END;
+    
+    RETURN v_has_access;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.check_user_access(p_username character varying, p_required_role character varying) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1408 (class 1255 OID 443428)
+-- Name: check_user_by_email(text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.check_user_by_email(p_email text) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    user_exists INTEGER;
+    decrypted_email TEXT;
+    user_record RECORD;
+BEGIN
+    user_exists := 0;
+    
+    -- Проходим по всем пользователям и расшифровываем email для сравнения
+    FOR user_record IN SELECT id, email FROM users LOOP
+        IF user_record.email IS NOT NULL THEN
+            BEGIN
+                decrypted_email := decrypt_email(user_record.email);
+                IF decrypted_email = p_email THEN
+                    user_exists := 1;
+                    EXIT;
+                END IF;
+            EXCEPTION WHEN OTHERS THEN
+                -- Пропускаем ошибки дешифрования
+                CONTINUE;
+            END;
+        END IF;
+    END LOOP;
+    
+    RETURN user_exists;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.check_user_by_email(p_email text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1409 (class 1255 OID 443429)
+-- Name: clean_string(text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.clean_string(input text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    IF input IS NULL THEN
+        RETURN NULL;
+    END IF;
+    -- Удаление лишних пробелов, замена нескольких пробелов на один
+    RETURN TRIM(REGEXP_REPLACE(input, '\s+', ' ', 'g'));
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.clean_string(input text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1410 (class 1255 OID 443430)
+-- Name: count_books_by_user(character varying, character varying); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.count_books_by_user(p_lastname character varying, p_name character varying) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+books_count INTEGER := 0;
+BEGIN
+IF p_lastname IS NULL OR p_name IS NULL THEN
+RAISE EXCEPTION 'Фамилия и имя пользователя не могут быть NULL.';
+END IF;
+SELECT COUNT(t.id)
+INTO books_count
+FROM ticket t
+JOIN users u ON t.user_id = u.id
+WHERE u.lastname = p_lastname AND u.name = p_name;
+RETURN books_count;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.count_books_by_user(p_lastname character varying, p_name character varying) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1411 (class 1255 OID 443431)
+-- Name: count_unique_readers_in_month(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.count_unique_readers_in_month(p_month integer) RETURNS bigint
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+readers_count BIGINT := 0;
+BEGIN
+IF p_month IS NULL OR p_month < 1 OR p_month > 12 THEN
+RAISE EXCEPTION 'Номер месяца должен быть от 1 до 12.';
+END IF;
+SELECT COUNT(DISTINCT t.user_id)
+INTO readers_count
+FROM ticket t
+WHERE EXTRACT(MONTH FROM t.date_of_issue) = p_month; -- EXTRACT(MONTH FROM ...) извлекает номер месяца из даты
+RETURN readers_count;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.count_unique_readers_in_month(p_month integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1412 (class 1255 OID 443432)
+-- Name: days_since_issue(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.days_since_issue(p_ticket_id integer) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+issue_date DATE;
+days_elapsed INTEGER;
+BEGIN
+SELECT date_of_issue
+INTO issue_date
+FROM ticket
+WHERE id = p_ticket_id;
+IF NOT FOUND THEN
+RAISE EXCEPTION 'Билет с ID % не найден.', p_ticket_id;
+END IF;
+days_elapsed := Kol_Dney(issue_date, CURRENT_DATE::date);
+RETURN days_elapsed;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.days_since_issue(p_ticket_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1413 (class 1255 OID 443433)
+-- Name: decrypt_data(text, text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.decrypt_data(p_encrypted text, p_key text DEFAULT 'default_key'::text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    result TEXT;
+BEGIN
+    result := convert_from(decrypt(decode(p_encrypted, 'hex'), p_key, 'aes'), 'UTF8');
+    RETURN result;
+EXCEPTION WHEN OTHERS THEN
+    RETURN 'Ошибка дешифрования';
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.decrypt_data(p_encrypted text, p_key text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1414 (class 1255 OID 443435)
+-- Name: decrypt_email(text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.decrypt_email(encrypted_email text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    decrypted_email TEXT;
+BEGIN
+    IF encrypted_email IS NULL THEN
+        RETURN NULL;
+    END IF;
+    
+    BEGIN
+        decrypted_email := convert_from(decrypt(decode(encrypted_email, 'base64'), 'my_secret_key', 'aes'), 'UTF8');
+        RETURN decrypted_email;
+    EXCEPTION WHEN OTHERS THEN
+        RETURN encrypted_email; -- Если не зашифровано, возвращаем как есть
+    END;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.decrypt_email(encrypted_email text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1415 (class 1255 OID 443436)
+-- Name: delete_book(integer); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.delete_book(IN p_bshifr integer)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_title VARCHAR(25);
+    v_vidano BOOLEAN;
+BEGIN
+    BEGIN
+        -- Получаем информацию о книге
+        SELECT title, vidano INTO v_title, v_vidano
+        FROM book WHERE bshifr = p_bshifr;
+        
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Книга с bshifr=% не найдена', p_bshifr;
+        END IF;
+        
+        -- Проверка: если книга на руках - запрещаем удаление
+        IF v_vidano = TRUE THEN
+            RAISE EXCEPTION 'Невозможно удалить книгу "%" - экземпляр находится на руках у читателя', v_title;
+        END IF;
+        
+        -- Удаляем книгу
+        DELETE FROM book WHERE bshifr = p_bshifr;
+        
+        COMMIT;
+        RAISE NOTICE 'Книга "% (bshifr=%) успешно удалена', v_title, p_bshifr;
+        
+    EXCEPTION
+        WHEN OTHERS THEN
+            ROLLBACK;
+            RAISE NOTICE 'ОШИБКА: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.delete_book(IN p_bshifr integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1416 (class 1255 OID 443437)
+-- Name: delete_reader(integer); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.delete_reader(IN p_user_id integer)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_books_count INTEGER;
+    v_username VARCHAR(50);
+    v_books_list TEXT;
+BEGIN
+    BEGIN
+        -- Получаем имя пользователя
+        SELECT lastname || ' ' || name INTO v_username
+        FROM users WHERE id = p_user_id;
+        
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Пользователь с ID % не найден', p_user_id;
+        END IF;
+        
+        -- Подсчет книг на руках
+        SELECT COUNT(*) INTO v_books_count
+        FROM ticket
+        WHERE user_id = p_user_id 
+          AND (date_of_return IS NULL OR date_of_return > CURRENT_DATE);
+        
+        -- Если есть книги - формируем список и выдаем ошибку
+        IF v_books_count > 0 THEN
+            SELECT STRING_AGG(b.title, ', ') INTO v_books_list
+            FROM ticket t
+            JOIN book b ON t.book_id = b.bshifr
+            WHERE t.user_id = p_user_id 
+              AND (t.date_of_return IS NULL OR t.date_of_return > CURRENT_DATE);
+            
+            RAISE EXCEPTION 'Невозможно удалить читателя "%". У него на руках % книг: %',
+                            v_username, v_books_count, v_books_list;
+        END IF;
+        
+        -- Удаляем пользователя
+        DELETE FROM users WHERE id = p_user_id;
+        
+        COMMIT;
+        RAISE NOTICE 'Читатель "% (ID: %) успешно удален', v_username, p_user_id;
+        
+    EXCEPTION
+        WHEN OTHERS THEN
+            ROLLBACK;
+            RAISE NOTICE 'ОШИБКА: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.delete_reader(IN p_user_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1417 (class 1255 OID 443438)
+-- Name: encrypt_data(text, text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.encrypt_data(p_data text, p_key text DEFAULT 'default_key'::text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    RETURN encode(encrypt(p_data::bytea, p_key, 'aes'), 'hex');
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.encrypt_data(p_data text, p_key text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1418 (class 1255 OID 443439)
+-- Name: encrypt_email(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.encrypt_email() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    IF NEW.email IS NOT NULL AND NEW.email != '' THEN
+        -- Используем симметричное шифрование с ключом 'secret_key'
+        NEW.email := encode(encrypt(NEW.email::bytea, 'my_secret_key', 'aes'), 'base64');
+    END IF;
+    RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.encrypt_email() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1419 (class 1255 OID 443440)
+-- Name: generate_random_id_for_usernew(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.generate_random_id_for_usernew() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    new_id INTEGER;
+    is_unique BOOLEAN := FALSE;
+    max_attempts INTEGER := 100; -- Количество попыток генерации ID
+    current_attempt INTEGER := 0;
+BEGIN
+    -- Если ID уже предоставлен (НЕ NULL), оставляем его.
+    IF NEW.id IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
+    -- Генерируем новый ID, пока он не станет уникальным.
+    WHILE NOT is_unique LOOP
+        -- Генерируем случайное число (диапазон 1 - 1000000).
+        new_id := floor(random() * 1000000 + 1);
+
+        -- Проверяем уникальность ID.
+        IF NOT EXISTS (SELECT 1 FROM usernew WHERE id = new_id) THEN
+            is_unique := TRUE;
+        END IF;
+        
+        current_attempt := current_attempt + 1;
+        -- Защита от бесконечного цикла.
+        IF current_attempt > max_attempts THEN
+            RAISE EXCEPTION 'Не удалось сгенерировать уникальный ID для usernew после % попыток.', max_attempts;
+        END IF;
+    END LOOP;
+    
+    -- Присваиваем сгенерированный ID.
+    NEW.id := new_id;
+    
+    RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.generate_random_id_for_usernew() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1420 (class 1255 OID 443441)
+-- Name: generate_sentence_from_dict(integer, text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.generate_sentence_from_dict(p_word_count integer, p_separator text DEFAULT ' '::text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_words TEXT[];
+    v_sentence TEXT := '';
+    v_capitalized_word TEXT;
+    i INTEGER;
+BEGIN
+    -- Получаем случайные слова
+    v_words := get_random_words(p_word_count);
+    
+    -- Формируем предложение
+    FOR i IN 1..array_length(v_words, 1) LOOP
+        IF i = 1 THEN
+            -- Первое слово с заглавной буквы
+            v_sentence := v_sentence || initcap(v_words[i]);
+        ELSE
+            v_sentence := v_sentence || p_separator || lower(v_words[i]);
+        END IF;
+    END LOOP;
+    
+    -- Добавляем точку в конце
+    v_sentence := v_sentence || '.';
+    
+    RETURN v_sentence;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.generate_sentence_from_dict(p_word_count integer, p_separator text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1421 (class 1255 OID 443442)
+-- Name: generate_uuid_and_time(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.generate_uuid_and_time() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    IF NEW.uuid IS NULL THEN
+        NEW.uuid := gen_random_uuid();
+    END IF;
+    NEW.time_created := CURRENT_TIMESTAMP;
+    RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.generate_uuid_and_time() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1849 (class 1255 OID 444742)
+-- Name: get_exhibit_count_by_author(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.get_exhibit_count_by_author(author_id integer) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    exhibit_count INTEGER;
+BEGIN
+    SELECT COUNT(*) INTO exhibit_count
+    FROM Exhibit
+    WHERE id_creator = author_id;
+    
+    RETURN exhibit_count;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.get_exhibit_count_by_author(author_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1422 (class 1255 OID 443443)
+-- Name: get_most_expensive_book_by_author(character varying); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.get_most_expensive_book_by_author(p_author_lastname character varying) RETURNS character varying
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+book_title_result VARCHAR(255);
+BEGIN
+IF p_author_lastname IS NULL THEN
+RAISE EXCEPTION 'Фамилия автора не может быть NULL.';
+END IF;
+SELECT b.title
+INTO book_title_result
+FROM book b
+JOIN author a ON b.id_author = a.id
+WHERE a.lastname = p_author_lastname 
+ORDER BY b.price DESC 
+LIMIT 1; 
+IF NOT FOUND THEN
+RAISE NOTICE 'Автор "%" не найден или у него нет книг в базе.', p_author_lastname;
+RETURN NULL; -- Возвращаем NULL, если ничего не найдено.
+END IF;
+RETURN book_title_result;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.get_most_expensive_book_by_author(p_author_lastname character varying) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1423 (class 1255 OID 443444)
+-- Name: get_random_sentence(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.get_random_sentence(word_count integer DEFAULT 5) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    words TEXT[];
+    sentence TEXT := '';
+    i INTEGER;
+BEGIN
+    words := get_random_words(word_count);
+    
+    FOR i IN 1..array_length(words, 1) LOOP
+        IF i > 1 THEN
+            sentence := sentence || ' ';
+        END IF;
+        sentence := sentence || words[i];
+    END LOOP;
+    
+    RETURN sentence || '.';
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.get_random_sentence(word_count integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1424 (class 1255 OID 443445)
+-- Name: get_random_words(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.get_random_words(word_count integer DEFAULT 1) RETURNS text[]
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    words_array TEXT[];
+    result_array TEXT[] := '{}';
+    i INTEGER;
+    random_index INTEGER;
+BEGIN
+    -- Пытаемся прочитать файл со словами
+    BEGIN
+        SELECT string_to_array(pg_read_file('/usr/share/dict/words', 0, 1000000), E'\n') INTO words_array;
+    EXCEPTION WHEN OTHERS THEN
+        -- Если файл не доступен, используем встроенный словарь
+        words_array := ARRAY['apple', 'table', 'chair', 'house', 'computer', 'program', 'database', 'security', 'encryption', 'hash'];
+    END;
+    
+    -- Проверяем, что массив не пустой
+    IF array_length(words_array, 1) IS NULL THEN
+        words_array := ARRAY['apple', 'table', 'chair', 'house', 'computer'];
+    END IF;
+    
+    -- Выбираем случайные слова
+    FOR i IN 1..word_count LOOP
+        random_index := 1 + floor(random() * array_length(words_array, 1))::INTEGER;
+        result_array := array_append(result_array, words_array[random_index]);
+    END LOOP;
+    
+    RETURN result_array;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.get_random_words(word_count integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1425 (class 1255 OID 443446)
+-- Name: get_ticket_details_refcursor(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.get_ticket_details_refcursor(p_ticket_id integer) RETURNS refcursor
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+ticket_cur REFCURSOR;
+BEGIN
+IF p_ticket_id IS NULL THEN
+RAISE EXCEPTION 'ID билета не может быть NULL.';
+END IF;
+OPEN ticket_cur FOR
+SELECT
+u.lastname AS user_lastname,
+b.title AS book_title,
+t.date_of_issue AS ticket_date
+FROM ticket t
+JOIN users u ON t.user_id = u.id
+JOIN book b ON t.book_id = b.bshifr
+WHERE t.id = p_ticket_id;
+RETURN ticket_cur;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.get_ticket_details_refcursor(p_ticket_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1838 (class 1255 OID 444504)
+-- Name: getaverageratingforexhibit(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.getaverageratingforexhibit(exhibit_id integer) RETURNS numeric
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    avg_rating DECIMAL(3,2);
+BEGIN
+    SELECT AVG(exhibit_rating) INTO avg_rating
+    FROM Viewing
+    WHERE id_exhibit = exhibit_id;
+    
+    RETURN COALESCE(avg_rating, 0);
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.getaverageratingforexhibit(exhibit_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1836 (class 1255 OID 444502)
+-- Name: getexhibitcountbyauthor(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.getexhibitcountbyauthor(author_id integer) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    exhibit_count INT;
+BEGIN
+    SELECT COUNT(*) INTO exhibit_count
+    FROM Exhibit
+    WHERE id_creator = author_id;
+    
+    RETURN exhibit_count;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.getexhibitcountbyauthor(author_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1837 (class 1255 OID 444503)
+-- Name: getexhibitsbyauthor(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.getexhibitsbyauthor(author_id integer) RETURNS TABLE(id_exhibit integer, type character varying, creation_date date, copy_price numeric, creation_cost numeric)
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    RETURN QUERY
+    SELECT e.id_exhibit, e.type, e.creation_date, e.copy_price, e.creation_cost
+    FROM Exhibit e
+    WHERE e.id_creator = author_id;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.getexhibitsbyauthor(author_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1840 (class 1255 OID 444506)
+-- Name: getmuseumtotalvalue(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.getmuseumtotalvalue(museum_id integer) RETURNS numeric
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    total_value DECIMAL(15,2);
+BEGIN
+    SELECT SUM(e.copy_price) INTO total_value
+    FROM Exhibit e
+    WHERE e.id_museum = museum_id;
+    
+    RETURN COALESCE(total_value, 0);
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.getmuseumtotalvalue(museum_id integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1839 (class 1255 OID 444505)
+-- Name: gettop5expensiveexhibits(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.gettop5expensiveexhibits() RETURNS TABLE(id_exhibit integer, type character varying, copy_price numeric, author_name character varying)
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    RETURN QUERY
+    SELECT e.id_exhibit, e.type, e.copy_price, c.full_name
+    FROM Exhibit e
+    JOIN Creator c ON e.id_creator = c.id_creator
+    ORDER BY e.copy_price DESC
+    LIMIT 5;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.gettop5expensiveexhibits() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1426 (class 1255 OID 443447)
+-- Name: hash_password(text, text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.hash_password(p_password text, p_salt text DEFAULT NULL::text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    salt TEXT;
+    hashed_password TEXT;
+BEGIN
+    -- Генерируем соль, если не передана
+    IF p_salt IS NULL THEN
+        salt := gen_salt('bf'); -- Blowfish алгоритм
+    ELSE
+        salt := p_salt;
+    END IF;
+    
+    -- Создаём хэш пароля
+    hashed_password := crypt(p_password, salt);
+    RETURN hashed_password;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.hash_password(p_password text, p_salt text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1427 (class 1255 OID 443448)
+-- Name: increase_price_by_cursor(); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.increase_price_by_cursor()
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_book RECORD;
+    v_new_price REAL;
+    v_increase REAL := 20; -- 20%
+    v_max_price REAL := 10000;
+    v_processed INTEGER := 0;
+    v_updated INTEGER := 0;
+BEGIN
+    -- Курсор для книг авторов, родившихся менее 1950 года
+    FOR v_book IN 
+        SELECT b.bshifr, b.title, b.price, a.lastname, a.name, a.year_b
+        FROM book b
+        JOIN author a ON b.id_author = a.id
+        WHERE a.year_b < 1950
+    LOOP
+        -- Создаем точку сохранения для каждой книги
+        PERFORM 1;
+        
+        BEGIN
+            -- Вычисляем новую цену (+20%)
+            v_new_price := v_book.price * (1 + v_increase / 100);
+            
+            RAISE NOTICE 'Книга: "%", Автор: % % (род. %), Цена: % -> %',
+                        v_book.title, v_book.lastname, v_book.name, 
+                        v_book.year_b, v_book.price, v_new_price;
+            
+            -- Проверка: не превышает ли новая цена 10000
+            IF v_new_price <= v_max_price THEN
+                UPDATE book SET price = v_new_price WHERE bshifr = v_book.bshifr;
+                v_updated := v_updated + 1;
+                RAISE NOTICE '  -> ЦЕНА УВЕЛИЧЕНА';
+            ELSE
+                -- Цена не изменяется, просто пропускаем
+                RAISE NOTICE '  -> ЦЕНА НЕ ИЗМЕНЕНА (%.2f превысила бы лимит %.0f)', v_new_price, v_max_price;
+            END IF;
+            
+            v_processed := v_processed + 1;
+            
+        EXCEPTION
+            WHEN OTHERS THEN
+                -- При ошибке просто пропускаем книгу
+                RAISE NOTICE '  -> ОШИБКА при обработке книги "%": %', v_book.title, SQLERRM;
+        END;
+    END LOOP;
+    
+    RAISE NOTICE '=========================================';
+    RAISE NOTICE 'Обработано книг: %, Обновлено: %', v_processed, v_updated;
+    RAISE NOTICE '=========================================';
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.increase_price_by_cursor() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1428 (class 1255 OID 443449)
+-- Name: issue_book(integer, integer, date); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.issue_book(IN p_user_id integer, IN p_book_bshifr integer, IN p_date_issue date)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_books_count INTEGER;
+    v_title VARCHAR(25);
+    v_username VARCHAR(50);
+    v_available BOOLEAN;
+BEGIN
+    BEGIN
+        -- Получаем имя пользователя
+        SELECT lastname || ' ' || name INTO v_username
+        FROM users WHERE id = p_user_id;
+        
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Пользователь с ID % не найден', p_user_id;
+        END IF;
+        
+        -- Получаем информацию о книге
+        SELECT title, vidano INTO v_title, v_available
+        FROM book WHERE bshifr = p_book_bshifr;
+        
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Книга с bshifr=% не найдена', p_book_bshifr;
+        END IF;
+        
+        -- Проверка доступности книги
+        IF v_available = TRUE THEN
+            RAISE EXCEPTION 'Книга "%" уже выдана другому читателю', v_title;
+        END IF;
+        
+        -- Подсчет книг на руках у читателя
+        SELECT COUNT(*) INTO v_books_count
+        FROM ticket
+        WHERE user_id = p_user_id 
+          AND (date_of_return IS NULL OR date_of_return > CURRENT_DATE);
+        
+        RAISE NOTICE 'У читателя "%" на руках % книг', v_username, v_books_count;
+        
+        -- Проверка лимита (не более 3)
+        IF v_books_count >= 3 THEN
+            RAISE EXCEPTION 'Читатель "%" уже имеет % книг на руках (максимум 3)', 
+                            v_username, v_books_count;
+        END IF;
+        
+        -- Выдача книги
+        INSERT INTO ticket (user_id, book_id, date_of_issue, date_of_return)
+        VALUES (p_user_id, p_book_bshifr, p_date_issue, NULL);
+        
+        UPDATE book SET vidano = TRUE WHERE bshifr = p_book_bshifr;
+        
+        COMMIT;
+        RAISE NOTICE 'Книга "%" выдана читателю "%"', v_title, v_username;
+        
+    EXCEPTION
+        WHEN OTHERS THEN
+            ROLLBACK;
+            RAISE NOTICE 'ОШИБКА: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.issue_book(IN p_user_id integer, IN p_book_bshifr integer, IN p_date_issue date) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1429 (class 1255 OID 443450)
+-- Name: kol_dney(date, date); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.kol_dney(date1 date, date2 date) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    num_days INTEGER;
+BEGIN
+    IF date1 IS NULL OR date2 IS NULL THEN
+        RAISE EXCEPTION 'Даты не могут быть NULL.';
+    END IF;
+
+    -- В PostgreSQL разница между двумя DATE автоматически возвращает количество дней.
+    num_days := date2 - date1;
+
+    RETURN num_days; -- Возвращаем разницу в днях
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.kol_dney(date1 date, date2 date) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1430 (class 1255 OID 443451)
+-- Name: month_name_by_number(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.month_name_by_number(month_num integer) RETURNS character varying
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+IF month_num IS NULL OR month_num < 1 OR month_num > 12 THEN
+RAISE EXCEPTION 'Номер месяца должен быть от 1 до 12.';
+END IF;
+RETURN CASE month_num
+WHEN 1 THEN 'Январь'
+WHEN 2 THEN 'Февраль'
+WHEN 3 THEN 'Март'
+WHEN 4 THEN 'Апрель'
+WHEN 5 THEN 'Май'
+WHEN 6 THEN 'Июнь'
+WHEN 7 THEN 'Июль'
+WHEN 8 THEN 'Август'
+WHEN 9 THEN 'Сентябрь'
+WHEN 10 THEN 'Октябрь'
+WHEN 11 THEN 'Ноябрь'
+WHEN 12 THEN 'Декабрь'
+ELSE 'Неизвестный месяц'
+END;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.month_name_by_number(month_num integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1431 (class 1255 OID 443452)
+-- Name: multi_hash(text, integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.multi_hash(p_value text, p_iterations integer DEFAULT 1) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    hashed_value TEXT;
+    i INTEGER;
+BEGIN
+    IF p_value IS NULL THEN
+        RETURN NULL;
+    END IF;
+    
+    hashed_value := p_value;
+    
+    FOR i IN 1..p_iterations LOOP
+        hashed_value := encode(digest(hashed_value, 'sha256'), 'hex');
+    END LOOP;
+    
+    RETURN hashed_value;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.multi_hash(p_value text, p_iterations integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1432 (class 1255 OID 443453)
+-- Name: normalize_region(text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.normalize_region(region_name text) RETURNS text
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    RETURN CASE
+        WHEN region_name ILIKE '%Санкт-Петербург%' OR region_name ILIKE '%СПб%' THEN 'Санкт-Петербург'
+        WHEN region_name ILIKE '%Ленинградская%' THEN 'Ленинградская область'
+        WHEN region_name ILIKE '%Новгород%' THEN 'Новгородская область'
+        ELSE clean_string(region_name)
+    END;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.normalize_region(region_name text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1433 (class 1255 OID 443454)
+-- Name: nvl(anyelement, anyelement); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.nvl(input_value anyelement, default_value anyelement) RETURNS anyelement
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    RETURN COALESCE(input_value, default_value);
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.nvl(input_value anyelement, default_value anyelement) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1434 (class 1255 OID 443455)
+-- Name: parse_salary(text); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.parse_salary(salary_text text) RETURNS TABLE(min_salary numeric, max_salary numeric)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    numbers NUMERIC[];
+BEGIN
+    IF salary_text IS NULL THEN
+        min_salary := NULL;
+        max_salary := NULL;
+        RETURN NEXT;
+        RETURN;
+    END IF;
+    
+    -- Извлечение чисел из строки
+    SELECT ARRAY(
+        SELECT REGEXP_MATCHES(salary_text, '(\d+[.,]?\d*)', 'g')
+    ) INTO numbers;
+    
+    IF array_length(numbers, 1) >= 1 THEN
+        min_salary := numbers[1]::NUMERIC;
+        IF array_length(numbers, 1) >= 2 THEN
+            max_salary := numbers[2]::NUMERIC;
+        ELSE
+            max_salary := min_salary;
+        END IF;
+    ELSE
+        min_salary := NULL;
+        max_salary := NULL;
+    END IF;
+    
+    RETURN NEXT;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.parse_salary(salary_text text) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1435 (class 1255 OID 443456)
+-- Name: proizv(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.proizv(n integer) RETURNS bigint
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    i INTEGER;
+    total_product BIGINT := 1; -- Инициализируем произведение единицей
+BEGIN
+    IF n IS NULL OR n <= 0 THEN
+        RAISE EXCEPTION 'Параметр n должен быть положительным целым числом.';
+    END IF;
+
+    -- Цикл от 1 до n
+    FOR i IN 1..n LOOP
+        total_product := total_product * i; -- Умножаем текущее число на произведение
+    END LOOP;
+
+    RETURN total_product; -- Возвращаем вычисленное произведение
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.proizv(n integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1436 (class 1255 OID 443457)
+-- Name: proverka(integer, integer, date, date); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.proverka(IN kodu integer, IN kodb integer, IN dvid date, IN dvoz date)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    rez VARCHAR(10) := 'ОК';
+    pr INTEGER;
+    v_last_id INTEGER;
+BEGIN
+    -- Начинаем транзакцию
+    BEGIN
+        -- Ввод записи в таблицу ticket (она же bilet)
+        INSERT INTO ticket (user_id, book_id, date_of_issue, date_of_return)
+        VALUES (kodU, kodB, dVid, dVoz)
+        RETURNING id INTO v_last_id;
+        
+        RAISE NOTICE 'Вставлена запись с ID: %', v_last_id;
+        
+        -- Вычисляем разницу между датой возврата и датой выдачи
+        SELECT (date_of_return - date_of_issue) INTO pr
+        FROM ticket WHERE id = v_last_id;
+        
+        RAISE NOTICE 'Разница между датами: % дней', pr;
+        
+        -- Проверка: если разница > 0, то фиксируем, иначе откатываем
+        IF pr > 0 THEN
+            COMMIT;
+            rez := 'ОК';
+            RAISE NOTICE 'Дата возврата корректна. Транзакция зафиксирована.';
+        ELSE
+            ROLLBACK;
+            rez := 'No';
+            RAISE NOTICE 'ОШИБКА: дата возврата (%) меньше или равна дате выдачи (%)', dVoz, dVid;
+            RAISE NOTICE 'Транзакция откачена.';
+        END IF;
+        
+        -- Выводим значение переменной rez
+        RAISE NOTICE 'Результат: %', rez;
+        
+    EXCEPTION
+        WHEN OTHERS THEN
+            ROLLBACK;
+            rez := 'Error';
+            RAISE NOTICE 'Ошибка при выполнении: %', SQLERRM;
+    END;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.proverka(IN kodu integer, IN kodb integer, IN dvid date, IN dvoz date) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1437 (class 1255 OID 443458)
+-- Name: register_user(character varying, text, text, character varying); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.register_user(p_username character varying, p_name text, p_password text, p_role_name character varying DEFAULT 'user'::character varying) RETURNS integer
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    v_salt TEXT;
+    v_role_id INTEGER;
+    v_encrypted_name TEXT;
+    v_user_id INTEGER;
+BEGIN
+    -- Получаем ID роли
+    SELECT id INTO v_role_id FROM roles WHERE role_name = p_role_name;
+    IF v_role_id IS NULL THEN
+        RAISE EXCEPTION 'Роль "%" не существует', p_role_name;
+    END IF;
+    
+    -- Генерируем соль
+    v_salt := gen_salt('bf');
+    
+    -- Шифруем имя пользователя (используем AES)
+    v_encrypted_name := encode(encrypt(p_name::bytea, 'master_key', 'aes'), 'base64');
+    
+    -- Вставляем пользователя
+    INSERT INTO system_users (username, encrypted_name, password_hash, salt, role_id)
+    VALUES (p_username, v_encrypted_name, crypt(p_password, v_salt), v_salt, v_role_id)
+    RETURNING id INTO v_user_id;
+    
+    RETURN v_user_id;
+EXCEPTION WHEN unique_violation THEN
+    RAISE EXCEPTION 'Пользователь с именем "%" уже существует', p_username;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.register_user(p_username character varying, p_name text, p_password text, p_role_name character varying) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1438 (class 1255 OID 443459)
+-- Name: summa(integer); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.summa(n integer) RETURNS bigint
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    i INTEGER;
+    total_sum BIGINT := 0; -- Инициализируем сумму нулем
+BEGIN
+    IF n IS NULL OR n <= 0 THEN
+        RAISE EXCEPTION 'Параметр n должен быть положительным целым числом.';
+    END IF;
+
+    -- Цикл от 1 до n
+    FOR i IN 1..n LOOP
+        total_sum := total_sum + i; -- Добавляем текущее число к сумме
+    END LOOP;
+
+    RETURN total_sum; -- Возвращаем вычисленную сумму
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.summa(n integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1439 (class 1255 OID 443460)
+-- Name: tek_vr(integer); Type: PROCEDURE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE PROCEDURE dyachkov_ya.tek_vr(IN n integer)
+    LANGUAGE plpgsql
+    AS $$
+DECLARE
+    i INTEGER;
+BEGIN
+    IF n IS NULL OR n <= 0 THEN
+        RAISE EXCEPTION 'Параметр N должен быть положительным числом.';
+    END IF;
+
+    RAISE NOTICE '--- Вывод текущего времени % раз ---', n;
+    
+    FOR i IN 1..n LOOP
+        -- NOW() возвращает текущую дату и время
+        RAISE NOTICE 'Итерация %: Текущее время: %', i, NOW();
+    END LOOP;
+END;
+$$;
+
+
+ALTER PROCEDURE dyachkov_ya.tek_vr(IN n integer) OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1440 (class 1255 OID 443461)
+-- Name: ticket_insert_trigger(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.ticket_insert_trigger() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+IF ( NEW.date_of_issue >= DATE '2024-01-01' AND NEW.date_of_issue < DATE '2024-02-01' ) THEN INSERT INTO ticket2024m1 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-02-01' AND NEW.date_of_issue < DATE '2024-03-01' ) THEN INSERT INTO ticket2024m2 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-03-01' AND NEW.date_of_issue < DATE '2024-04-01' ) THEN INSERT INTO ticket2024m3 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-04-01' AND NEW.date_of_issue < DATE '2024-05-01' ) THEN INSERT INTO ticket2024m4 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-05-01' AND NEW.date_of_issue < DATE '2024-06-01' ) THEN INSERT INTO ticket2024m5 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-06-01' AND NEW.date_of_issue < DATE '2024-07-01' ) THEN INSERT INTO ticket2024m6 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-07-01' AND NEW.date_of_issue < DATE '2024-08-01' ) THEN INSERT INTO ticket2024m7 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-08-01' AND NEW.date_of_issue < DATE '2024-09-01' ) THEN INSERT INTO ticket2024m8 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-09-01' AND NEW.date_of_issue < DATE '2024-10-01' ) THEN INSERT INTO ticket2024m9 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-10-01' AND NEW.date_of_issue < DATE '2024-11-01' ) THEN INSERT INTO ticket2024m10 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-11-01' AND NEW.date_of_issue < DATE '2024-12-01' ) THEN INSERT INTO ticket2024m11 VALUES (NEW.*);
+ELSIF ( NEW.date_of_issue >= DATE '2024-12-01' AND NEW.date_of_issue < DATE '2025-01-01' ) THEN INSERT INTO ticket2024m12 VALUES (NEW.*);
+ELSE RAISE EXCEPTION 'Date out of range for all partitions';
+END IF;
+RETURN NULL;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.ticket_insert_trigger() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1441 (class 1255 OID 443462)
+-- Name: track_price_change(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.track_price_change() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    -- Если цена изменилась и новая цена > 1000 рублей
+    IF OLD.price IS DISTINCT FROM NEW.price AND NEW.price > 1000 THEN
+        -- Автономное логирование (сохранится даже при ROLLBACK)
+        PERFORM autonomous_log(NEW.bshifr, OLD.price, NEW.price);
+        
+        RAISE NOTICE 'Зафиксирована попытка изменения цены книги bshifr=%: % -> %', 
+                     NEW.bshifr, OLD.price, NEW.price;
+    END IF;
+    
+    RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.track_price_change() OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 1841 (class 1255 OID 444522)
+-- Name: update_museum_profit(); Type: FUNCTION; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE FUNCTION dyachkov_ya.update_museum_profit() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+    -- Обновляем прибыль музея при добавлении нового экспоната
+    UPDATE Museum
+    SET profit = profit + (NEW.copy_price - NEW.creation_cost)
+    WHERE id_museum = NEW.id_museum;
+    
+    RETURN NEW;
+END;
+$$;
+
+
+ALTER FUNCTION dyachkov_ya.update_museum_profit() OWNER TO dyachkov_ya;
+
+SET default_tablespace = '';
+
+SET default_table_access_method = heap;
+
+--
+-- TOC entry 720 (class 1259 OID 450407)
+-- Name: auditlog; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.auditlog (
+    id_log integer NOT NULL,
+    id_user integer,
+    action character varying(255),
+    table_name character varying(100),
+    record_id integer,
+    action_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+    ip_address character varying(45),
+    details text
+);
+
+
+ALTER TABLE dyachkov_ya.auditlog OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 719 (class 1259 OID 450406)
+-- Name: auditlog_id_log_seq; Type: SEQUENCE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE SEQUENCE dyachkov_ya.auditlog_id_log_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER SEQUENCE dyachkov_ya.auditlog_id_log_seq OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 6622 (class 0 OID 0)
+-- Dependencies: 719
+-- Name: auditlog_id_log_seq; Type: SEQUENCE OWNED BY; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER SEQUENCE dyachkov_ya.auditlog_id_log_seq OWNED BY dyachkov_ya.auditlog.id_log;
+
+
+--
+-- TOC entry 325 (class 1259 OID 444440)
+-- Name: creator; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.creator (
+    id_creator integer NOT NULL,
+    full_name character varying(100) NOT NULL,
+    royalty numeric(10,2),
+    phone character varying(20),
+    address character varying(200)
+);
+
+
+ALTER TABLE dyachkov_ya.creator OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 327 (class 1259 OID 444460)
+-- Name: exhibit; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.exhibit (
+    id_exhibit integer NOT NULL,
+    type character varying(50),
+    creation_date date,
+    copy_price numeric(10,2),
+    creation_cost numeric(10,2),
+    id_creator integer,
+    id_museum integer
+);
+
+
+ALTER TABLE dyachkov_ya.exhibit OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 324 (class 1259 OID 444434)
+-- Name: museum; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.museum (
+    id_museum integer NOT NULL,
+    area numeric(10,2),
+    profit numeric(12,2),
+    phone character varying(20),
+    address character varying(200)
+);
+
+
+ALTER TABLE dyachkov_ya.museum OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 716 (class 1259 OID 450373)
+-- Name: roles; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.roles (
+    id_role integer NOT NULL,
+    role_name character varying(50) NOT NULL,
+    description text
+);
+
+
+ALTER TABLE dyachkov_ya.roles OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 715 (class 1259 OID 450372)
+-- Name: roles_id_role_seq; Type: SEQUENCE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE SEQUENCE dyachkov_ya.roles_id_role_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER SEQUENCE dyachkov_ya.roles_id_role_seq OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 6623 (class 0 OID 0)
+-- Dependencies: 715
+-- Name: roles_id_role_seq; Type: SEQUENCE OWNED BY; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER SEQUENCE dyachkov_ya.roles_id_role_seq OWNED BY dyachkov_ya.roles.id_role;
+
+
+--
+-- TOC entry 718 (class 1259 OID 450386)
+-- Name: users; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.users (
+    id_user integer NOT NULL,
+    username character varying(50) NOT NULL,
+    password_hash character varying(255) NOT NULL,
+    salt character varying(100) NOT NULL,
+    id_role integer NOT NULL,
+    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+    last_login timestamp without time zone,
+    is_active boolean DEFAULT true
+);
+
+
+ALTER TABLE dyachkov_ya.users OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 717 (class 1259 OID 450385)
+-- Name: users_id_user_seq; Type: SEQUENCE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE SEQUENCE dyachkov_ya.users_id_user_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER SEQUENCE dyachkov_ya.users_id_user_seq OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 6624 (class 0 OID 0)
+-- Dependencies: 717
+-- Name: users_id_user_seq; Type: SEQUENCE OWNED BY; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER SEQUENCE dyachkov_ya.users_id_user_seq OWNED BY dyachkov_ya.users.id_user;
+
+
+--
+-- TOC entry 328 (class 1259 OID 444476)
+-- Name: viewing; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.viewing (
+    id_ticket integer NOT NULL,
+    id_exhibit integer NOT NULL,
+    exhibit_rating integer,
+    CONSTRAINT viewing_exhibit_rating_check CHECK (((exhibit_rating >= 1) AND (exhibit_rating <= 5)))
+);
+
+
+ALTER TABLE dyachkov_ya.viewing OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 330 (class 1259 OID 444512)
+-- Name: v_exhibit_popularity; Type: VIEW; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE VIEW dyachkov_ya.v_exhibit_popularity AS
+ SELECT e.id_exhibit,
+    e.type,
+    count(v.id_ticket) AS views_count,
+    avg(v.exhibit_rating) AS average_rating
+   FROM (dyachkov_ya.exhibit e
+     LEFT JOIN dyachkov_ya.viewing v ON ((e.id_exhibit = v.id_exhibit)))
+  GROUP BY e.id_exhibit, e.type
+  ORDER BY (avg(v.exhibit_rating)) DESC;
+
+
+ALTER VIEW dyachkov_ya.v_exhibit_popularity OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 329 (class 1259 OID 444507)
+-- Name: v_exhibits_with_authors; Type: VIEW; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE VIEW dyachkov_ya.v_exhibits_with_authors AS
+ SELECT e.id_exhibit,
+    e.type,
+    e.creation_date,
+    e.copy_price,
+    e.creation_cost,
+    c.full_name AS author_name,
+    m.address AS museum_location
+   FROM ((dyachkov_ya.exhibit e
+     LEFT JOIN dyachkov_ya.creator c ON ((e.id_creator = c.id_creator)))
+     LEFT JOIN dyachkov_ya.museum m ON ((e.id_museum = m.id_museum)));
+
+
+ALTER VIEW dyachkov_ya.v_exhibits_with_authors OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 326 (class 1259 OID 444447)
+-- Name: visitor; Type: TABLE; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TABLE dyachkov_ya.visitor (
+    id_ticket integer NOT NULL,
+    full_name character varying(100) NOT NULL,
+    phone character varying(20),
+    visit_date date,
+    museum_rating integer,
+    id_museum integer,
+    CONSTRAINT visitor_museum_rating_check CHECK (((museum_rating >= 1) AND (museum_rating <= 5)))
+);
+
+
+ALTER TABLE dyachkov_ya.visitor OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 331 (class 1259 OID 444517)
+-- Name: v_museum_statistics; Type: VIEW; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE VIEW dyachkov_ya.v_museum_statistics AS
+ SELECT m.id_museum,
+    m.address,
+    count(DISTINCT e.id_exhibit) AS exhibits_count,
+    count(DISTINCT v.id_ticket) AS visitors_count,
+    sum(e.copy_price) AS total_exhibits_value
+   FROM ((dyachkov_ya.museum m
+     LEFT JOIN dyachkov_ya.exhibit e ON ((m.id_museum = e.id_museum)))
+     LEFT JOIN dyachkov_ya.visitor v ON ((m.id_museum = v.id_museum)))
+  GROUP BY m.id_museum, m.address;
+
+
+ALTER VIEW dyachkov_ya.v_museum_statistics OWNER TO dyachkov_ya;
+
+--
+-- TOC entry 6329 (class 2604 OID 450410)
+-- Name: auditlog id_log; Type: DEFAULT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.auditlog ALTER COLUMN id_log SET DEFAULT nextval('dyachkov_ya.auditlog_id_log_seq'::regclass);
+
+
+--
+-- TOC entry 6325 (class 2604 OID 450376)
+-- Name: roles id_role; Type: DEFAULT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.roles ALTER COLUMN id_role SET DEFAULT nextval('dyachkov_ya.roles_id_role_seq'::regclass);
+
+
+--
+-- TOC entry 6326 (class 2604 OID 450389)
+-- Name: users id_user; Type: DEFAULT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.users ALTER COLUMN id_user SET DEFAULT nextval('dyachkov_ya.users_id_user_seq'::regclass);
+
+
+--
+-- TOC entry 6616 (class 0 OID 450407)
+-- Dependencies: 720
+-- Data for Name: auditlog; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.auditlog (id_log, id_user, action, table_name, record_id, action_time, ip_address, details) FROM stdin;
+\.
+
+
+--
+-- TOC entry 6607 (class 0 OID 444440)
+-- Dependencies: 325
+-- Data for Name: creator; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.creator (id_creator, full_name, royalty, phone, address) FROM stdin;
+1	Иван Иванов	10000.00	+7-111-1111111	ул. Пушкина, 1
+2	Мария Петрова	15000.00	+7-222-2222222	ул. Лермонтова, 2
+3	Алексей Сидоров	12000.00	+7-333-3333333	ул. Гоголя, 3
+4	Елена Васильева	18000.00	+7-444-4444444	ул. Толстого, 4
+\.
+
+
+--
+-- TOC entry 6609 (class 0 OID 444460)
+-- Dependencies: 327
+-- Data for Name: exhibit; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.exhibit (id_exhibit, type, creation_date, copy_price, creation_cost, id_creator, id_museum) FROM stdin;
+101	Живопись	2020-01-15	200000.00	50000.00	1	1
+102	Скульптура	2019-10-20	150000.00	40000.00	2	1
+103	Графика	2021-05-10	80000.00	20000.00	1	2
+104	Живопись	2018-07-22	250000.00	60000.00	3	1
+105	Фотография	2022-03-15	50000.00	10000.00	2	2
+106	Скульптура	2020-11-30	120000.00	35000.00	4	3
+107	Икона	2017-09-05	300000.00	80000.00	1	1
+108	Графика	2019-12-18	50000.00	15000.00	3	3
+\.
+
+
+--
+-- TOC entry 6606 (class 0 OID 444434)
+-- Dependencies: 324
+-- Data for Name: museum; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.museum (id_museum, area, profit, phone, address) FROM stdin;
+1	1200.50	500000.00	+7-123-4567890	ул. Тверская, 5, Москва
+3	1500.00	750000.00	+7-555-1234567	ул. Ленина, 15, Екатеринбург
+2	800.00	400000.00	+7-987-6543210	пр. Невский, 10, Санкт-Петербург
+\.
+
+
+--
+-- TOC entry 6612 (class 0 OID 450373)
+-- Dependencies: 716
+-- Data for Name: roles; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.roles (id_role, role_name, description) FROM stdin;
+1	Admin	Полный доступ ко всем функциям
+2	Curator	Управление экспонатами и авторами
+3	Cashier	Работа с посетителями
+4	Guest	Только просмотр
+\.
+
+
+--
+-- TOC entry 6614 (class 0 OID 450386)
+-- Dependencies: 718
+-- Data for Name: users; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.users (id_user, username, password_hash, salt, id_role, created_at, last_login, is_active) FROM stdin;
+9	curator	1bc936e86f9f3f3082b9e81e5489639dda3661522011f0586d74630d30aa9c2f	wkcZ1ZKs7I9Fkrhs4V6P6uM0kx0IYbY8aRyYfa06+mE=	2	2026-06-01 16:30:13.375361	\N	t
+10	cashier	7f3c6eac18259fab2b0916dcd958ecbd6c5a7e1e11823fd64d23388852c57445	Yomt7dErmMWuTyE/Ut3HMN1gzzXuF9y0YFL8yVVObOU=	3	2026-06-01 16:30:13.402044	\N	t
+11	guest	c09bbc66d7d00cbd91441685c4f0a23811d59d2d57754fc0b82f7271df245428	dAO2P8AgdohaItFZI4JpZ8PH/JH/AXbXJELFl4DPEmY=	4	2026-06-01 16:30:13.433068	\N	t
+8	admin	119e41ff69e6c9fd2133b72169dd666a9caf9d24765483274de2730f00d85f5a	WLdeT8d1JEiq//inGEmb83MFydypm84/dldG8Bg366k=	1	2026-06-01 16:30:13.324373	2026-06-29 00:42:40.906973	t
+\.
+
+
+--
+-- TOC entry 6610 (class 0 OID 444476)
+-- Dependencies: 328
+-- Data for Name: viewing; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.viewing (id_ticket, id_exhibit, exhibit_rating) FROM stdin;
+1001	101	5
+1001	102	4
+1001	103	5
+1002	103	4
+1002	105	5
+1003	101	5
+1003	104	4
+1003	107	5
+1004	106	3
+1004	108	4
+1005	103	5
+1005	105	4
+1006	101	5
+1006	102	5
+1006	104	4
+\.
+
+
+--
+-- TOC entry 6608 (class 0 OID 444447)
+-- Dependencies: 326
+-- Data for Name: visitor; Type: TABLE DATA; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+COPY dyachkov_ya.visitor (id_ticket, full_name, phone, visit_date, museum_rating, id_museum) FROM stdin;
+1001	Петр Сидоров	+7-123-4567890	2023-06-01	5	1
+1002	Анна Ким	+7-234-5678901	2023-06-02	4	2
+1003	Олег Иванов	+7-345-6789012	2023-06-03	5	1
+1004	Наталья Смирнова	+7-456-7890123	2023-06-04	3	3
+1005	Дмитрий Петров	+7-567-8901234	2023-06-05	4	2
+1006	Екатерина Кузнецова	+7-678-9012345	2023-06-06	5	1
+\.
+
+
+--
+-- TOC entry 6625 (class 0 OID 0)
+-- Dependencies: 719
+-- Name: auditlog_id_log_seq; Type: SEQUENCE SET; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+SELECT pg_catalog.setval('dyachkov_ya.auditlog_id_log_seq', 1, false);
+
+
+--
+-- TOC entry 6626 (class 0 OID 0)
+-- Dependencies: 715
+-- Name: roles_id_role_seq; Type: SEQUENCE SET; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+SELECT pg_catalog.setval('dyachkov_ya.roles_id_role_seq', 9, true);
+
+
+--
+-- TOC entry 6627 (class 0 OID 0)
+-- Dependencies: 717
+-- Name: users_id_user_seq; Type: SEQUENCE SET; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+SELECT pg_catalog.setval('dyachkov_ya.users_id_user_seq', 11, true);
+
+
+--
+-- TOC entry 6352 (class 2606 OID 450416)
+-- Name: auditlog auditlog_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.auditlog
+    ADD CONSTRAINT auditlog_pkey PRIMARY KEY (id_log);
+
+
+--
+-- TOC entry 6336 (class 2606 OID 444446)
+-- Name: creator creator_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.creator
+    ADD CONSTRAINT creator_pkey PRIMARY KEY (id_creator);
+
+
+--
+-- TOC entry 6340 (class 2606 OID 444465)
+-- Name: exhibit exhibit_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.exhibit
+    ADD CONSTRAINT exhibit_pkey PRIMARY KEY (id_exhibit);
+
+
+--
+-- TOC entry 6334 (class 2606 OID 444439)
+-- Name: museum museum_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.museum
+    ADD CONSTRAINT museum_pkey PRIMARY KEY (id_museum);
+
+
+--
+-- TOC entry 6344 (class 2606 OID 450382)
+-- Name: roles roles_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.roles
+    ADD CONSTRAINT roles_pkey PRIMARY KEY (id_role);
+
+
+--
+-- TOC entry 6346 (class 2606 OID 450384)
+-- Name: roles roles_role_name_key; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.roles
+    ADD CONSTRAINT roles_role_name_key UNIQUE (role_name);
+
+
+--
+-- TOC entry 6348 (class 2606 OID 450398)
+-- Name: users users_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.users
+    ADD CONSTRAINT users_pkey PRIMARY KEY (id_user);
+
+
+--
+-- TOC entry 6350 (class 2606 OID 450400)
+-- Name: users users_username_key; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.users
+    ADD CONSTRAINT users_username_key UNIQUE (username);
+
+
+--
+-- TOC entry 6342 (class 2606 OID 444485)
+-- Name: viewing viewing_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.viewing
+    ADD CONSTRAINT viewing_pkey PRIMARY KEY (id_ticket, id_exhibit);
+
+
+--
+-- TOC entry 6338 (class 2606 OID 444454)
+-- Name: visitor visitor_pkey; Type: CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.visitor
+    ADD CONSTRAINT visitor_pkey PRIMARY KEY (id_ticket);
+
+
+--
+-- TOC entry 6360 (class 2620 OID 444523)
+-- Name: exhibit after_exhibit_insert; Type: TRIGGER; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+CREATE TRIGGER after_exhibit_insert AFTER INSERT ON dyachkov_ya.exhibit FOR EACH ROW EXECUTE FUNCTION dyachkov_ya.update_museum_profit();
+
+
+--
+-- TOC entry 6359 (class 2606 OID 450417)
+-- Name: auditlog auditlog_id_user_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.auditlog
+    ADD CONSTRAINT auditlog_id_user_fkey FOREIGN KEY (id_user) REFERENCES dyachkov_ya.users(id_user);
+
+
+--
+-- TOC entry 6354 (class 2606 OID 444466)
+-- Name: exhibit exhibit_id_creator_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.exhibit
+    ADD CONSTRAINT exhibit_id_creator_fkey FOREIGN KEY (id_creator) REFERENCES dyachkov_ya.creator(id_creator);
+
+
+--
+-- TOC entry 6355 (class 2606 OID 444471)
+-- Name: exhibit exhibit_id_museum_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.exhibit
+    ADD CONSTRAINT exhibit_id_museum_fkey FOREIGN KEY (id_museum) REFERENCES dyachkov_ya.museum(id_museum);
+
+
+--
+-- TOC entry 6358 (class 2606 OID 450401)
+-- Name: users users_id_role_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.users
+    ADD CONSTRAINT users_id_role_fkey FOREIGN KEY (id_role) REFERENCES dyachkov_ya.roles(id_role);
+
+
+--
+-- TOC entry 6356 (class 2606 OID 444491)
+-- Name: viewing viewing_id_exhibit_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.viewing
+    ADD CONSTRAINT viewing_id_exhibit_fkey FOREIGN KEY (id_exhibit) REFERENCES dyachkov_ya.exhibit(id_exhibit);
+
+
+--
+-- TOC entry 6357 (class 2606 OID 444486)
+-- Name: viewing viewing_id_ticket_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.viewing
+    ADD CONSTRAINT viewing_id_ticket_fkey FOREIGN KEY (id_ticket) REFERENCES dyachkov_ya.visitor(id_ticket);
+
+
+--
+-- TOC entry 6353 (class 2606 OID 444455)
+-- Name: visitor visitor_id_museum_fkey; Type: FK CONSTRAINT; Schema: dyachkov_ya; Owner: dyachkov_ya
+--
+
+ALTER TABLE ONLY dyachkov_ya.visitor
+    ADD CONSTRAINT visitor_id_museum_fkey FOREIGN KEY (id_museum) REFERENCES dyachkov_ya.museum(id_museum);
+
+
+-- Completed on 2026-06-30 18:28:57 MSK
+
+--
+-- PostgreSQL database dump complete
+--
+
+\unrestrict fMX8pe7fyPeennYB6eeK17lVC7rkBp0LxzYWCFMQgMflMYdB3BYjGmqxfiz2Aj2
+

+ 241 - 0
Руководство оператора.md

@@ -0,0 +1,241 @@
+# Руководство оператора программы «Учёт экспонатов музея»
+
+# Аннотация
+
+Настоящее руководство предназначено для оператора информационной системы «Учёт экспонатов музея». В документе приведены требования к подготовке рабочего места, порядок установки и проверки программы, правила выполнения основных операций и действия при возникновении ошибок.
+
+Руководство описывает работу с клиентским Windows-приложением. Развёртывание, настройка и администрирование сервера PostgreSQL выполняются ответственным специалистом отдельно.
+
+# Введение
+
+## Область применения
+
+Программа применяется для ведения сведений о музейных экспонатах. Оператор может просматривать каталог, добавлять новые экспонаты, изменять их цену, удалять записи и получать количество экспонатов выбранного автора.
+
+Данные хранятся в централизованной базе PostgreSQL, расположенной на отдельном сервере.
+
+## Основные возможности
+
+Оператору доступны следующие функции:
+
+- просмотр списка экспонатов в табличном виде;
+- обновление данных с сервера;
+- добавление нового экспоната;
+- выбор автора из справочника;
+- изменение цены выбранного экспоната;
+- удаление экспоната с подтверждением;
+- подсчёт количества экспонатов по идентификатору автора;
+- просмотр сообщений о результате операций;
+- получение сообщений при ошибках подключения или ввода данных.
+
+В текущей версии программа не требует ввода логина и пароля и не разделяет пользователей по ролям.
+
+# Подготовка к работе
+
+## Требования к рабочему месту
+
+Для работы программы необходимы:
+
+- компьютер под управлением Windows;
+- .NET Framework 4.7.2;
+- доступ к серверу PostgreSQL;
+- настроенные параметры подключения;
+- установленное клиентское приложение и его зависимости;
+- права учётной записи базы данных на чтение, добавление, изменение и удаление записей.
+
+Перед началом работы следует убедиться в наличии сетевого подключения и доступности сервера базы данных.
+
+## Порядок установки
+
+1. Закройте работающие экземпляры программы.
+2. Запустите файл `MuseumExhibitsSetup.exe`.
+3. Разрешите запуск установщика, если Windows выведет системное предупреждение.
+4. Выберите папку установки. При отсутствии специальных требований оставьте путь по умолчанию.
+5. Выберите создание ярлыка на рабочем столе и в меню «Пуск».
+6. Начните установку и дождитесь завершения копирования файлов.
+7. При появлении соответствующего запроса установите .NET Framework 4.7.2.
+8. Завершите работу мастера установки.
+9. Запустите приложение через созданный ярлык.
+
+Параметры подключения к PostgreSQL должны быть подготовлены ответственным специалистом до передачи приложения оператору.
+
+## Порядок проверки работоспособности
+
+После установки выполните контрольную проверку:
+
+1. Запустите программу.
+2. Убедитесь, что открылось главное окно «Музей изобразительных искусств — Учёт экспонатов».
+3. Проверьте, что таблица заполнилась сведениями об экспонатах.
+4. Проверьте, что в списке «Автор» отображаются доступные авторы.
+5. Нажмите кнопку **«Обновить»** и убедитесь, что появилось сообщение «Данные обновлены».
+6. Добавьте тестовую запись с уникальным идентификатором.
+7. Выберите созданную запись и измените её цену.
+8. Выполните подсчёт экспонатов известного автора.
+9. Удалите тестовую запись.
+10. Убедитесь, что после каждой операции таблица обновляется.
+
+Если все перечисленные действия выполняются без ошибок, программа готова к эксплуатации.
+
+# Описание операций
+
+## 1. Запуск программы
+
+1. Откройте меню «Пуск» или рабочий стол.
+2. Запустите ярлык программы «Учёт экспонатов музея».
+3. Дождитесь загрузки главного окна.
+4. Убедитесь, что список экспонатов и список авторов загрузились без сообщений об ошибке.
+
+При запуске приложение автоматически устанавливает соединение с сервером PostgreSQL.
+
+## 2. Просмотр данных
+
+Основная часть окна содержит таблицу экспонатов. Каждая строка соответствует отдельному экспонату.
+
+В таблице отображаются следующие столбцы:
+
+- `ID`;
+- `Тип`;
+- `Дата создания`;
+- `Цена`;
+- `Себестоимость`;
+- `Автор`;
+- `ID музея`.
+
+Для просмотра большого списка используйте вертикальную полосу прокрутки. Если все столбцы не помещаются в окне, используйте горизонтальную прокрутку.
+
+Выбор строки выполняется нажатием по ней левой кнопкой мыши.
+
+## 3. Обновление списка
+
+Для повторной загрузки сведений:
+
+1. Нажмите кнопку **«Обновить»**.
+2. Дождитесь выполнения запроса к серверу.
+3. Убедитесь, что в нижней части окна появилось сообщение «Данные обновлены».
+
+Обновление следует выполнять, если записи могли быть изменены другим пользователем или средствами администрирования базы данных.
+
+## 4. Добавление экспоната
+
+Для добавления нового экспоната заполните поля группы «Данные для добавления».
+
+### Поля формы
+
+- **ID** — уникальный целочисленный идентификатор экспоната.
+- **Тип** — наименование типа объекта, например «Живопись», «Скульптура» или «Графика».
+- **Дата** — дата создания экспоната.
+- **Цена** — цена экземпляра.
+- **Себестоимость** — затраты на создание или приобретение.
+- **Автор** — автор, выбранный из раскрывающегося списка.
+- **ID музея** — идентификатор существующего музея.
+
+### Порядок добавления
+
+1. Введите уникальный ID.
+2. Укажите тип экспоната.
+3. Выберите дату.
+4. Введите цену.
+5. Введите себестоимость.
+6. Выберите автора.
+7. Введите ID существующего музея.
+8. Проверьте введённые сведения.
+9. Нажмите кнопку **«Добавить»**.
+10. Убедитесь, что новая запись появилась в таблице.
+
+После успешного добавления программа очищает поля ввода и выводит сообщение «Экспонат добавлен!».
+
+Цена и себестоимость вводятся в числовом формате, принятом в настройках Windows. В русской локали дробная часть обычно отделяется запятой.
+
+Не используйте идентификатор, который уже принадлежит другому экспонату. Автор и музей должны существовать в соответствующих таблицах базы данных.
+
+## 5. Изменение цены экспоната
+
+В текущей версии программы можно изменить только цену выбранного экспоната.
+
+1. Выберите нужную строку в таблице.
+2. Нажмите кнопку **«Изменить цену»**.
+3. Введите новую цену в открывшемся окне.
+4. Нажмите кнопку подтверждения.
+5. Убедитесь, что значение в таблице изменилось.
+6. Проверьте появление сообщения «Цена обновлена!».
+
+Если строка не выбрана, программа выведет сообщение «Выберите экспонат для изменения».
+
+Если введено пустое или нечисловое значение, изменение не выполняется.
+
+## 6. Удаление экспоната
+
+1. Выберите удаляемый экспонат в таблице.
+2. Нажмите кнопку **«Удалить»**.
+3. Проверьте, что выбрана нужная строка.
+4. В окне подтверждения выберите ответ «Да».
+5. Убедитесь, что запись исчезла из таблицы.
+6. Проверьте появление сообщения «Экспонат удален!».
+
+Для отмены операции выберите ответ «Нет».
+
+Если строка не выбрана, программа выведет сообщение «Выберите экспонат для удаления».
+
+Удаление может быть запрещено базой данных, если с экспонатом связаны другие записи. В таком случае не следует удалять зависимости вручную без разрешения администратора базы данных.
+
+## 7. Подсчёт количества экспонатов автора
+
+Для подсчёта используется функция PostgreSQL `get_exhibit_count_by_author`.
+
+1. Нажмите кнопку **«Тест процедуры»**.
+2. Введите числовой идентификатор автора.
+3. Подтвердите ввод.
+4. Просмотрите результат в нижней части главного окна.
+
+Результат отображается в виде:
+
+`Автор с ID 1 создал 3 экспонатов`
+
+Если с указанным автором не связано ни одного экспоната, программа отображает значение `0`.
+
+Кнопка имеет техническое название «Тест процедуры», однако фактически выполняет подсчёт количества экспонатов автора.
+
+## 8. Завершение работы
+
+1. Дождитесь завершения текущей операции.
+2. Закройте дополнительные диалоговые окна.
+3. Закройте главное окно стандартной кнопкой Windows.
+
+Не выключайте компьютер и не завершайте процесс принудительно во время добавления, изменения или удаления записи.
+
+# Аварийные ситуации
+
+| Ситуация | Возможная причина | Действия оператора |
+|---|---|---|
+| Программа не устанавливается | Установочный файл повреждён; недостаточно прав или места на диске | Повторно получите установщик, проверьте свободное место и запустите его от имени администратора |
+| Программа не запускается | Отсутствует .NET Framework 4.7.2; повреждены файлы приложения или зависимости | Установите требуемую платформу, проверьте папку программы или выполните переустановку |
+| Появляется ошибка загрузки | Сервер PostgreSQL недоступен; отсутствует сеть; неверны параметры подключения | Проверьте сеть, повторите запуск и сообщите ответственному специалисту |
+| Таблица экспонатов не заполняется | Запрос завершился ошибкой; таблица `Exhibit` недоступна или не содержит записей | Нажмите «Обновить», затем обратитесь к администратору базы данных |
+| Список авторов не загружается | Таблица `Creator` недоступна; отсутствуют права или соединение | Проверьте подключение и сообщите ответственному специалисту |
+| Экспонат не добавляется | Поля заполнены неверно; ID уже используется; не существует автор или музей | Проверьте числовые значения, укажите уникальный ID и существующие связанные записи |
+| Цена не изменяется | Не выбрана строка; введено нечисловое значение; отсутствуют права на изменение | Выберите запись, повторно введите цену и при необходимости обратитесь к администратору |
+| Экспонат не удаляется | Запись используется в другой таблице; отсутствуют права на удаление | Не удаляйте связанные данные самостоятельно; сообщите администратору БД |
+| Не выполняется подсчёт по автору | Введено нечисловое значение; функция отсутствует или недоступна | Укажите числовой ID автора и сообщите ответственному специалисту, если ошибка повторяется |
+| Программа перестала отвечать | Потеряно соединение с сервером или запрос выполняется слишком долго | Подождите завершения операции, затем закройте программу средствами Windows и сообщите специалисту |
+
+При обнаружении повреждения или несогласованности данных прекратите операции изменения и сообщите ответственному специалисту. Не редактируйте базу данных вручную.
+
+# Термины и сокращения
+
+| Термин | Определение |
+|---|---|
+| Оператор | Пользователь, выполняющий действия в клиентском приложении |
+| Клиентское приложение | Установленная на рабочем компьютере программа |
+| БД | База данных |
+| СУБД | Система управления базами данных |
+| PostgreSQL | Серверная СУБД, в которой хранятся данные |
+| Экспонат | Объект, включённый в музейный каталог |
+| Автор | Создатель произведения или другого экспоната |
+| Музей | Организация, в которой хранится или учитывается экспонат |
+| Идентификатор | Уникальное числовое обозначение записи |
+| Цена | Указанная в базе данных стоимость экземпляра |
+| Себестоимость | Затраты на создание или приобретение экспоната |
+| Функция базы данных | Серверная программа, принимающая параметры и возвращающая результат |
+| Npgsql | Библиотека подключения приложений .NET к PostgreSQL |
+| .NET Framework | Платформа, необходимая для запуска программы |
+| Inno Setup | Средство создания установочного файла Windows |