This commit is contained in:
Курнат Андрей
2026-04-03 21:06:10 +03:00
parent 1b20b39aca
commit a2b4762702
16 changed files with 1170 additions and 33 deletions

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace XLAB2
{
internal sealed class DocumentNumberDirectory
{
public DocumentNumberDirectory(IReadOnlyList<GroupOption> accountingBooks, IReadOnlyList<GroupOption> documentTypes)
{
AccountingBooks = accountingBooks ?? Array.Empty<GroupOption>();
DocumentTypes = documentTypes ?? Array.Empty<GroupOption>();
}
public IReadOnlyList<GroupOption> AccountingBooks { get; private set; }
public IReadOnlyList<GroupOption> DocumentTypes { get; private set; }
}
internal sealed class DocumentNumberDirectoryService
{
private readonly string _filePath;
public DocumentNumberDirectoryService(string filePath)
{
_filePath = filePath;
}
public DocumentNumberDirectory LoadDirectory()
{
var configuration = LoadConfiguration();
return new DocumentNumberDirectory(
NormalizeEntries(configuration.AccountingBooks, "книг учета"),
NormalizeEntries(configuration.DocumentTypes, "типов документов"));
}
public IReadOnlyList<GroupOption> LoadAccountingBooks()
{
return LoadDirectory().AccountingBooks;
}
public void SaveAccountingBooks(IReadOnlyList<GroupOption> accountingBooks)
{
var configuration = LoadConfiguration();
var normalizedAccountingBooks = NormalizeWritableEntries(accountingBooks, "книг учета");
var normalizedDocumentTypes = NormalizeEntries(configuration.DocumentTypes, "типов документов");
configuration.AccountingBooks = ToConfigurationEntries(normalizedAccountingBooks);
configuration.DocumentTypes = ToConfigurationEntries(normalizedDocumentTypes);
SaveConfiguration(configuration);
}
private static IReadOnlyList<GroupOption> NormalizeEntries(IEnumerable<DocumentNumberDirectoryEntry> entries, string sectionName)
{
var items = new List<GroupOption>();
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries ?? Array.Empty<DocumentNumberDirectoryEntry>())
{
if (entry == null)
{
continue;
}
var key = DocumentNumberFormatter.NormalizePart(entry.Key);
var title = DocumentNumberFormatter.NormalizePart(entry.Title);
if (key == null || title == null || !seenKeys.Add(key))
{
continue;
}
items.Add(new GroupOption
{
Key = key,
Title = title
});
}
if (items.Count == 0)
{
throw new InvalidOperationException(string.Format("Справочник номеров документов не содержит {0}.", sectionName));
}
return items;
}
private static IReadOnlyList<GroupOption> NormalizeWritableEntries(IEnumerable<GroupOption> entries, string sectionName)
{
var items = new List<GroupOption>();
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries ?? Array.Empty<GroupOption>())
{
if (entry == null)
{
continue;
}
var key = DocumentNumberFormatter.NormalizePart(entry.Key);
if (key == null)
{
throw new InvalidOperationException("Не задан ключ книги учета.");
}
var title = DocumentNumberFormatter.NormalizePart(entry.Title);
if (title == null)
{
throw new InvalidOperationException("Не задан номер книги учета.");
}
if (!seenKeys.Add(key))
{
throw new InvalidOperationException(string.Format("Ключ книги учета \"{0}\" повторяется.", key));
}
if (!seenTitles.Add(title))
{
throw new InvalidOperationException(string.Format("Номер книги учета \"{0}\" повторяется.", title));
}
items.Add(new GroupOption
{
Key = key,
Title = title
});
}
if (items.Count == 0)
{
throw new InvalidOperationException(string.Format("Справочник номеров документов не содержит {0}.", sectionName));
}
return items;
}
private DocumentNumberDirectoryConfiguration LoadConfiguration()
{
if (string.IsNullOrWhiteSpace(_filePath))
{
throw new InvalidOperationException("Не задан путь к справочнику номеров документов.");
}
if (!File.Exists(_filePath))
{
throw new InvalidOperationException(string.Format("Не найден справочник номеров документов: {0}.", _filePath));
}
using (var stream = File.OpenRead(_filePath))
{
var configuration = JsonSerializer.Deserialize<DocumentNumberDirectoryConfiguration>(stream, CreateReadSerializerOptions());
if (configuration == null)
{
throw new InvalidOperationException("Справочник номеров документов пуст или поврежден.");
}
return configuration;
}
}
private void SaveConfiguration(DocumentNumberDirectoryConfiguration configuration)
{
var directoryPath = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
var json = JsonSerializer.Serialize(configuration, CreateWriteSerializerOptions());
File.WriteAllText(_filePath, json);
}
private static List<DocumentNumberDirectoryEntry> ToConfigurationEntries(IEnumerable<GroupOption> entries)
{
var items = new List<DocumentNumberDirectoryEntry>();
foreach (var entry in entries ?? Array.Empty<GroupOption>())
{
if (entry == null)
{
continue;
}
items.Add(new DocumentNumberDirectoryEntry
{
Key = entry.Key,
Title = entry.Title
});
}
return items;
}
private static JsonSerializerOptions CreateReadSerializerOptions()
{
return new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
private static JsonSerializerOptions CreateWriteSerializerOptions()
{
return new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
}
}
internal static class DocumentNumberFormatter
{
public const int MaxLength = 60;
public static string Build(string documentTypeTitle, string accountingBookTitle, string documentSequenceNumber)
{
var normalizedDocumentTypeTitle = NormalizePart(documentTypeTitle);
var normalizedAccountingBookTitle = NormalizePart(accountingBookTitle);
var normalizedDocumentSequenceNumber = NormalizePart(documentSequenceNumber);
if (normalizedDocumentTypeTitle == null
|| normalizedAccountingBookTitle == null
|| normalizedDocumentSequenceNumber == null)
{
return string.Empty;
}
return string.Format("{0} № {1}-{2}", normalizedDocumentTypeTitle, normalizedAccountingBookTitle, normalizedDocumentSequenceNumber);
}
public static string NormalizePart(string value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
internal sealed class DocumentNumberDirectoryConfiguration
{
public List<DocumentNumberDirectoryEntry> AccountingBooks { get; set; }
public List<DocumentNumberDirectoryEntry> DocumentTypes { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
}
internal sealed class DocumentNumberDirectoryEntry
{
public string Key { get; set; }
public string Title { get; set; }
}
}