Explorar o código

change file structure, add empty UT, add EmailStorageProcessor

Dalibor Votruba hai 1 ano
pai
achega
4f309d0f7c
Modificáronse 68 ficheiros con 2154 adicións e 92 borrados
  1. 12 0
      App/App.config
  2. 0 0
      App/Design.Designer.cs
  3. 0 0
      App/Design.cs
  4. 0 0
      App/Design.resx
  5. 147 0
      App/EmailStorage/EmailItem.cs
  6. 82 0
      App/EmailStorage/EmailProcessorConfiguration.cs
  7. 53 0
      App/EmailStorage/EmailStateEnum.cs
  8. 834 0
      App/EmailStorage/EmailStorageProcessor.cs
  9. 52 0
      App/EmailStorage/EmailTemplate.cs
  10. 186 0
      App/EmailStorage/Metadata.cs
  11. 0 0
      App/Extensions/CsvParserUtility.cs
  12. 0 0
      App/Extensions/CsvUtils.cs
  13. 0 0
      App/Extensions/ExtendedParametrizedString.cs
  14. 0 0
      App/Extensions/ListViewExtension.cs
  15. 0 0
      App/Extensions/StringExt.cs
  16. 0 0
      App/FormBatch.Designer.cs
  17. 0 0
      App/FormBatch.cs
  18. 0 0
      App/FormBatch.resx
  19. 256 0
      App/FormDigitalSend.Designer.cs
  20. 145 0
      App/FormDigitalSend.cs
  21. 132 0
      App/FormDigitalSend.resx
  22. 0 0
      App/FormLabelPrint.Designer.cs
  23. 0 0
      App/FormLabelPrint.cs
  24. 0 0
      App/FormLabelPrint.resx
  25. 19 6
      App/FormLauncher.Designer.cs
  26. 6 0
      App/FormLauncher.cs
  27. 0 0
      App/FormLauncher.resx
  28. 0 0
      App/FormProductEdit.Designer.cs
  29. 0 0
      App/FormProductEdit.cs
  30. 0 0
      App/FormProductEdit.resx
  31. 0 0
      App/LICENSE
  32. 0 0
      App/Labels/ExcelLabelGenerator.cs
  33. 0 0
      App/ProductStorage/CategoryDto.cs
  34. 0 0
      App/ProductStorage/IIdentifierDto.cs
  35. 0 0
      App/ProductStorage/ProductDto.cs
  36. 0 0
      App/ProductStorage/ProductStorage.cs
  37. 0 0
      App/ProductStorage/VariantDto.cs
  38. 0 0
      App/Program.cs
  39. 48 0
      App/Properties/AppSettings.Designer.cs
  40. 12 0
      App/Properties/AppSettings.settings
  41. 0 0
      App/Properties/PublishProfiles/FolderProfile.pubxml
  42. 0 0
      App/Properties/Resources.Designer.cs
  43. 0 0
      App/Properties/Resources.resx
  44. 6 1
      App/README.md
  45. 0 0
      App/Resources/logo.png
  46. 0 0
      App/Resources/mail_header.jpg
  47. 86 8
      App/dlgEditCfg.Designer.cs
  48. 17 0
      App/dlgEditCfg.cs
  49. 0 0
      App/dlgEditCfg.resx
  50. 0 0
      App/dlgImport.Designer.cs
  51. 0 0
      App/dlgImport.cs
  52. 0 0
      App/dlgImport.resx
  53. 0 0
      App/dlgLabelSettings.Designer.cs
  54. 0 0
      App/dlgLabelSettings.cs
  55. 0 0
      App/dlgLabelSettings.resx
  56. 0 0
      App/dlgProtocol.Designer.cs
  57. 0 0
      App/dlgProtocol.cs
  58. 0 0
      App/dlgProtocol.resx
  59. 0 0
      App/dlgSettings.Designer.cs
  60. 0 0
      App/dlgSettings.cs
  61. 0 0
      App/dlgSettings.resx
  62. 0 0
      App/logo_icon.ico
  63. 6 5
      App/qdr.app.studiou.orders2printpack.csproj
  64. 1 1
      Setup/Setup.vdproj
  65. 16 0
      Test/UnitTest1.cs
  66. 23 0
      Test/qdr.app.studiou.orders2printpack.Test.csproj
  67. 0 70
      qdr.app.studiou.orders2printpack.csproj.Backup.tmp
  68. 15 1
      qdr.app.studiou.orders2printpack.sln

+ 12 - 0
App.config → App/App.config

@@ -89,6 +89,18 @@
             <setting name="FormatDigitalContains" serializeAs="String">
                 <value>Digitální data</value>
             </setting>
+            <setting name="TSSApiKey" serializeAs="String">
+                <value>riyeVPRvz9QF1YmrhdSz9rgiVzhy3mSb</value>
+            </setting>
+            <setting name="TSSChunkSizeBytes" serializeAs="String">
+                <value>2097152</value>
+            </setting>
+            <setting name="TSSUri" serializeAs="String">
+                <value>https://www.studiou.cz/</value>
+            </setting>
+            <setting name="LastDigitalDir" serializeAs="String">
+                <value />
+            </setting>
         </qdr.app.studiou.orders2printpack.Properties.AppSettings>
     </userSettings>
 </configuration>

+ 0 - 0
Design.Designer.cs → App/Design.Designer.cs


+ 0 - 0
Design.cs → App/Design.cs


+ 0 - 0
Design.resx → App/Design.resx


+ 147 - 0
App/EmailStorage/EmailItem.cs

@@ -0,0 +1,147 @@
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Helper class for working with email items and their file operations.
+    /// Provides a convenient wrapper around MetadataItem with additional functionality
+    /// for file system operations, validation, and state management.
+    /// </summary>
+    public class EmailItem
+    {
+        #region Private Fields
+        
+        /// <summary>
+        /// The underlying metadata item containing email and processing information
+        /// </summary>
+        private readonly MetadataItem _metadata;
+        
+        /// <summary>
+        /// Base directory path where email item directories are located
+        /// </summary>
+        private readonly string _basePath;
+
+        #endregion
+
+        #region Constructor
+
+        /// <summary>
+        /// Initializes a new instance of the EmailItem class
+        /// </summary>
+        /// <param name="metadata">The metadata item containing email information</param>
+        /// <param name="basePath">Base directory path for file operations</param>
+        /// <exception cref="ArgumentNullException">Thrown when metadata or basePath is null</exception>
+        public EmailItem(MetadataItem metadata, string basePath)
+        {
+            _metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
+            _basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
+        }
+
+        #endregion
+
+        #region Public Properties
+
+        /// <summary>
+        /// Gets the underlying metadata item
+        /// </summary>
+        public MetadataItem Metadata => _metadata;
+        
+        /// <summary>
+        /// Gets the full directory path for this email item
+        /// Combines the base path with the directory name from metadata
+        /// </summary>
+        public string DirectoryPath => Path.Combine(_basePath, _metadata.DirectoryName);
+        
+        /// <summary>
+        /// Gets a value indicating whether the directory exists on the file system
+        /// </summary>
+        public bool DirectoryExists => Directory.Exists(DirectoryPath);
+        
+        /// <summary>
+        /// Gets all JPG files in the email item's directory
+        /// Returns an empty enumerable if the directory doesn't exist
+        /// </summary>
+        public IEnumerable<string> JpgFiles => 
+            DirectoryExists ? Directory.GetFiles(DirectoryPath, "*.jpg") : Enumerable.Empty<string>();
+        
+        /// <summary>
+        /// Gets the count of JPG files in the directory
+        /// </summary>
+        public int FileCount => JpgFiles.Count();
+        
+        /// <summary>
+        /// Gets the total size in bytes of all JPG files in the directory
+        /// </summary>
+        public long TotalFileSize => JpgFiles.Sum(f => new FileInfo(f).Length);
+        
+        /// <summary>
+        /// Gets a value indicating whether this item is ready for processing
+        /// Checks that the state is Ready, directory exists, and contains files
+        /// </summary>
+        public bool IsReadyForProcessing => 
+            _metadata.State == EmailStateEnum.Ready && DirectoryExists && FileCount > 0;
+        
+        /// <summary>
+        /// Gets a value indicating whether this item can be reset
+        /// Items cannot be reset while they are being processed or uploaded
+        /// </summary>
+        public bool CanBeReset => 
+            _metadata.State != EmailStateEnum.Processing && 
+            _metadata.State != EmailStateEnum.Uploading;
+
+        /// <summary>
+        /// Gets the normalized filename for the recipient (safe for file system)
+        /// Replaces @ with _at_ and . with _dot_ to create filesystem-safe names
+        /// </summary>
+        public string NormalizedRecipient => 
+            _metadata.EmailAddress.Replace("@", "_at_").Replace(".", "_dot_");
+
+        #endregion
+
+        #region Public Methods
+
+        /// <summary>
+        /// Generates the zip filename for this item based on recipient and timestamp
+        /// </summary>
+        /// <param name="timestamp">Optional timestamp to use, defaults to current time</param>
+        /// <returns>A formatted zip filename in the format: studioufoto_{recipient}_{timestamp}.zip</returns>
+        public string GenerateZipFileName(DateTime? timestamp = null) =>
+            $"studioufoto_{NormalizedRecipient}_{(timestamp ?? DateTime.Now):yyyyMMdd_HHmmss}.zip";
+
+        /// <summary>
+        /// Validates that the item is in a consistent state for processing
+        /// </summary>
+        /// <returns>A tuple containing validation result and list of issues found</returns>
+        public (bool IsValid, List<string> Issues) Validate()
+        {
+            var issues = new List<string>();
+
+            // Check required email address
+            if (string.IsNullOrEmpty(_metadata.EmailAddress))
+                issues.Add("Email address is required");
+
+            // Check directory and files existence
+            if (!DirectoryExists)
+                issues.Add($"Directory does not exist: {DirectoryPath}");
+            else if (FileCount == 0)
+                issues.Add("No JPG files found in directory");
+
+            // Check required email content
+            if (string.IsNullOrEmpty(_metadata.Subject))
+                issues.Add("Email subject is required");
+
+            if (string.IsNullOrEmpty(_metadata.Body))
+                issues.Add("Email body is required");
+
+            return (issues.Count == 0, issues);
+        }
+
+        /// <summary>
+        /// Gets a summary of the item for display purposes
+        /// Includes recipient, file count, current state, and associated orders
+        /// </summary>
+        /// <returns>A formatted summary string</returns>
+        public string GetSummary() =>
+            $"{_metadata.EmailAddress} - {FileCount} files, {_metadata.State}, Orders: {_metadata.Orders}";
+
+        #endregion
+    }
+}

+ 82 - 0
App/EmailStorage/EmailProcessorConfiguration.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Configuration class for the EmailStorageProcessor containing all necessary
+    /// settings for TSS (Temporary Shared Storage) API and SMTP server connectivity.
+    /// This class is designed to be serialized/deserialized from JSON configuration files.
+    /// </summary>
+    public class EmailProcessorConfiguration
+    {
+        #region TSS API Configuration
+
+        /// <summary>
+        /// API key for accessing the Temporary Shared Storage (TSS) service.
+        /// This key is required for file upload operations to the shared storage.
+        /// </summary>
+        [JsonPropertyName("tss-api-key")]
+        [JsonRequired]
+        public string TssAPIKey { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Base URL for the Temporary Shared Storage (TSS) API endpoint.
+        /// Should include the protocol (https://) and domain.
+        /// </summary>
+        [JsonPropertyName("tss-url")]
+        [JsonRequired]
+        public string TssUrl { get; set; } = string.Empty;
+
+        #endregion
+
+        #region SMTP Server Configuration
+
+        /// <summary>
+        /// SMTP server hostname or IP address for sending emails.
+        /// Examples: smtp.gmail.com, mail.company.com, 192.168.1.100
+        /// </summary>
+        [JsonPropertyName("smtp-server")]
+        [JsonRequired]
+        public string SmtpServer { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// SMTP server port number for email communication.
+        /// Common ports: 25 (plain), 587 (STARTTLS), 465 (SSL/TLS)
+        /// Defaults to 587 which is the standard submission port with STARTTLS.
+        /// </summary>
+        [JsonPropertyName("smtp-server-port")]
+        [JsonRequired]
+        public int SmtpServerPort { get; set; } = 587; // Default SMTP submission port
+        
+        /// <summary>
+        /// Username for SMTP server authentication.
+        /// This is typically an email address or account name depending on the server.
+        /// </summary>
+        [JsonPropertyName("smtp-server-user")]
+        [JsonRequired]
+        public string SmtpServerUser { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Password for SMTP server authentication.
+        /// For security, consider using app-specific passwords when available.
+        /// This value should be kept secure and not logged.
+        /// </summary>
+        [JsonPropertyName("smtp-server-pwd")]
+        [JsonRequired]
+        public string SmtpServerPassword { get; set; } = string.Empty;
+        
+        /// <summary>
+        /// Indicates whether to enable SSL/TLS encryption for SMTP communication.
+        /// Defaults to true for security. Most modern SMTP servers require encryption.
+        /// </summary>
+        [JsonPropertyName("smtp-enable-ssl")]
+        public bool SmtpEnableSsl { get; set; } = true; // Default to secure connection
+
+        #endregion
+    }
+}

+ 53 - 0
App/EmailStorage/EmailStateEnum.cs

@@ -0,0 +1,53 @@
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Enumeration representing the various states an email item can be in during processing.
+    /// These states track the progression of an email item through the complete workflow
+    /// from initial discovery to final delivery.
+    /// </summary>
+    public enum EmailStateEnum
+    {
+        /// <summary>
+        /// Initial state when an email item is discovered and ready for processing.
+        /// Items in this state have their directory scanned and metadata populated,
+        /// but no processing operations have begun.
+        /// </summary>
+        Ready,
+        
+        /// <summary>
+        /// The email item is currently being processed.
+        /// This includes file compression and preparation for upload.
+        /// Items in this state should not be modified by other operations.
+        /// </summary>
+        Processing,
+        
+        /// <summary>
+        /// The compressed file is being uploaded to the Temporary Shared Storage.
+        /// This state indicates active network operation in progress.
+        /// Items in this state should not be reset or reprocessed.
+        /// </summary>
+        Uploading,
+        
+        /// <summary>
+        /// The email is being prepared and sent to the recipient.
+        /// This includes email template processing, personalization,
+        /// and actual SMTP transmission.
+        /// </summary>
+        Sending,
+        
+        /// <summary>
+        /// The email has been successfully sent to the recipient.
+        /// This is a terminal success state - processing is complete.
+        /// Files are typically moved to the "sent" directory.
+        /// </summary>
+        Sent,
+        
+        /// <summary>
+        /// An error occurred during processing at any stage.
+        /// Items in this state require manual intervention or can be reset
+        /// to retry processing. The ErrorMessage property should contain
+        /// details about what went wrong.
+        /// </summary>
+        Error
+    }
+}

+ 834 - 0
App/EmailStorage/EmailStorageProcessor.cs

@@ -0,0 +1,834 @@
+using Quadarax.Application.TemporarySharedStorage.Client.Dtos.Request;
+using Quadarax.Application.TemporarySharedStorage.Client.Services;
+using Quadarax.Foundation.Core.Logging;
+using System.IO.Abstractions;
+using System.IO.Compression;
+using System.Net.Mail;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Delegate for handling email item state change events.
+    /// Allows subscribers to track processing progress and state transitions.
+    /// </summary>
+    /// <param name="sender">The processor instance that triggered the event</param>
+    /// <param name="recipient">Email address of the recipient being processed</param>
+    /// <param name="status">New state of the email item</param>
+    /// <param name="progress">Progress percentage (0-100)</param>
+    public delegate void EmailItemStateChangeHandler(object sender, string recipient, EmailStateEnum status, int progress);
+
+    /// <summary>
+    /// Main processor class responsible for handling the complete email workflow:
+    /// 1. Scanning directories for photo files
+    /// 2. Compressing files into ZIP archives
+    /// 3. Uploading to Temporary Shared Storage
+    /// 4. Sending personalized emails with download links
+    /// 5. Managing file lifecycle and cleanup
+    /// 
+    /// This class implements IDisposable to properly clean up resources like SMTP connections.
+    /// </summary>
+    public class EmailStorageProcessor : IDisposable
+    {
+        #region *** Private fields ***
+        
+        /// <summary>
+        /// Base input directory where recipient folders containing JPG files are located
+        /// </summary>
+        private readonly string _inputPath;
+        
+        /// <summary>
+        /// Email template used for generating personalized emails
+        /// </summary>
+        private readonly EmailTemplate _emailTemplate;
+        
+        /// <summary>
+        /// Configuration containing API keys, SMTP settings, and other operational parameters
+        /// </summary>
+        private readonly EmailProcessorConfiguration _configuration;
+        
+        /// <summary>
+        /// Logger instance for this processor
+        /// </summary>
+        private readonly ILogger _logger;
+        
+        /// <summary>
+        /// Typed log instance for structured logging
+        /// </summary>
+        private readonly ILog _log;
+        
+        /// <summary>
+        /// File system abstraction for testable file operations
+        /// </summary>
+        private readonly IFileSystem _fileSystem;
+        
+        /// <summary>
+        /// Service for uploading files to Temporary Shared Storage
+        /// </summary>
+        private readonly FileUploadService _uploadService;
+        
+        /// <summary>
+        /// SMTP client for sending emails to recipients
+        /// </summary>
+        private readonly SmtpClient _smtpClient;
+        
+        /// <summary>
+        /// Cancellation token source for graceful shutdown of processing operations
+        /// </summary>
+        private CancellationTokenSource _cancellationTokenSource;
+        
+        /// <summary>
+        /// Lock object for thread-safe metadata file operations
+        /// </summary>
+        private readonly object _metadataLock = new object();
+        
+        /// <summary>
+        /// Flag to track if this instance has been disposed
+        /// </summary>
+        private bool _disposed = false;
+
+        // Required directory paths for file management
+        
+        /// <summary>
+        /// Directory for files currently being processed (compressed but not yet uploaded)
+        /// </summary>
+        private readonly string _processingDir;
+        
+        /// <summary>
+        /// Directory for files ready to be sent (uploaded and email being sent)
+        /// </summary>
+        private readonly string _sendingDir;
+        
+        /// <summary>
+        /// Directory for files that have been successfully sent
+        /// </summary>
+        private readonly string _sentDir;
+        
+        /// <summary>
+        /// Path to the metadata.json file that tracks all processing state
+        /// </summary>
+        private readonly string _metadataFile;
+        
+        #endregion
+
+         #region *** Properties ***
+
+        /// <summary>
+        /// Current metadata containing all email items and their processing state
+        /// </summary>
+        public Metadata Metadata { get; set; } = new Metadata();
+        
+        /// <summary>
+        /// Total number of email items being tracked
+        /// </summary>
+        public int CountTotal => Metadata.Items.Count;
+        
+        /// <summary>
+        /// Number of items currently being processed or uploaded
+        /// </summary>
+        public int CountProcessing => Metadata.Items.Count(x => x.State == EmailStateEnum.Processing || x.State == EmailStateEnum.Uploading);
+        
+        /// <summary>
+        /// Number of items currently being sent (email transmission in progress)
+        /// </summary>
+        public int CountSending => Metadata.Items.Count(x => x.State == EmailStateEnum.Sending);
+        
+        /// <summary>
+        /// Number of items that have been successfully sent
+        /// </summary>
+        public int CountSent => Metadata.Items.Count(x => x.State == EmailStateEnum.Sent);
+
+        #endregion
+        
+        #region *** Events ***
+
+        /// <summary>
+        /// Event fired when an email item's state changes during processing.
+        /// Useful for UI updates, progress tracking, and monitoring.
+        /// </summary>
+        public event EmailItemStateChangeHandler? EmailItemStateChanged;
+
+        #endregion
+
+        #region *** Constructor ***
+        
+        /// <summary>
+        /// Initializes a new instance of the EmailStorageProcessor.
+        /// Sets up all required services, directory structure, and loads existing metadata.
+        /// </summary>
+        /// <param name="fs">File system abstraction for file operations</param>
+        /// <param name="path">Base path where recipient directories are located</param>
+        /// <param name="emailTemplate">Template for generating personalized emails</param>
+        /// <param name="configuration">Configuration with API keys and SMTP settings</param>
+        /// <param name="logger">Logger for operational logging</param>
+        /// <exception cref="ArgumentNullException">Thrown when any required parameter is null</exception>
+        public EmailStorageProcessor(IFileSystem fs, string path, EmailTemplate emailTemplate, EmailProcessorConfiguration configuration, ILogger logger)
+        {
+            // Validate all required dependencies
+            _fileSystem = fs ?? throw new ArgumentNullException(nameof(fs));
+            _inputPath = path ?? throw new ArgumentNullException(nameof(path));
+            _emailTemplate = emailTemplate ?? throw new ArgumentNullException(nameof(emailTemplate));
+            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+            
+            // Initialize logging
+            _log = _logger.GetLogger(typeof(EmailStorageProcessor));
+            _cancellationTokenSource = new CancellationTokenSource();
+
+            // Setup directory paths using IFileSystem for testability
+            _processingDir = _fileSystem.Path.Combine(_inputPath, ".processing");
+            _sendingDir = _fileSystem.Path.Combine(_inputPath, ".sending");
+            _sentDir = _fileSystem.Path.Combine(_inputPath, ".sent");
+            _metadataFile = _fileSystem.Path.Combine(_inputPath, "metadata.json");
+
+            // Initialize external services
+            _uploadService = new FileUploadService(configuration.TssUrl, configuration.TssAPIKey, logger);
+            
+            // Setup SMTP client with provided configuration
+            _smtpClient = new SmtpClient(configuration.SmtpServer, configuration.SmtpServerPort)
+            {
+                Credentials = new System.Net.NetworkCredential(configuration.SmtpServerUser, configuration.SmtpServerPassword),
+                EnableSsl = configuration.SmtpEnableSsl
+            };
+
+            // Initialize the processing environment
+            InitializeDirectoryStructure();
+            LoadOrCreateMetadata();
+        }
+        #endregion
+      
+
+        #region *** Public Methods ***
+
+        /// <summary>
+        /// Processes a single email item through the complete workflow:
+        /// 1. Compress JPG files into ZIP archive
+        /// 2. Upload to Temporary Shared Storage
+        /// 3. Generate personalized email with download link
+        /// 4. Send email to recipient
+        /// 5. Move files to appropriate directory based on success/failure
+        /// </summary>
+        /// <param name="recipient">Email address of the recipient to process</param>
+        public async Task ProcessItemAsync(string recipient)
+        {
+            try
+            {
+                // Find the metadata item for this recipient
+                var item = Metadata.Items.FirstOrDefault(x => x.EmailAddress == recipient);
+                if (item == null)
+                {
+                    _log.Log(LogSeverityEnum.Error, $"Item with recipient '{recipient}' not found.");
+                    return;
+                }
+
+                _log.Log(LogSeverityEnum.Info, $"Starting processing for recipient: {recipient}");
+
+                // Execute the complete workflow
+                await CompressFilesAsync(item);              // Step 1: Create ZIP archive
+                await UploadCompressedFileAsync(item);       // Step 2: Upload to TSS
+                await PrepareEmailAndMoveAsync(item);        // Step 3: Prepare email content
+                await SendEmailAsync(item);                  // Step 4: Send email
+                await HandleSendingResultAsync(item, true);  // Step 5: Handle success
+
+                _log.Log(LogSeverityEnum.Info, $"Successfully processed recipient: {recipient}");
+            }
+            catch (Exception ex)
+            {
+                // Log error and mark item as failed
+                _log.Log(LogSeverityEnum.Error, $"Error processing recipient '{recipient}': {ex.Message}");
+                var item = Metadata.Items.FirstOrDefault(x => x.EmailAddress == recipient);
+                if (item != null)
+                {
+                    item.ErrorMessage = ex.Message;
+                    await HandleSendingResultAsync(item, false);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Processes all email items that are in Ready state sequentially.
+        /// Can be cancelled using the Stop() method which sets the cancellation token.
+        /// </summary>
+        public async Task ProcessAllAsync()
+        {
+            // Get all items that are ready for processing
+            var readyItems = Metadata.Items.Where(x => x.State == EmailStateEnum.Ready).ToList();
+            
+            foreach (var item in readyItems)
+            {
+                // Check for cancellation request
+                if (_cancellationTokenSource.Token.IsCancellationRequested)
+                    break;
+
+                await ProcessItemAsync(item.EmailAddress);
+            }
+        }
+
+        /// <summary>
+        /// Resets an email item back to Ready state and cleans up associated files.
+        /// This allows reprocessing of items that failed or need to be sent again.
+        /// Items cannot be reset while actively being processed or uploaded.
+        /// </summary>
+        /// <param name="recipient">Email address of the recipient to reset</param>
+        public void ResetItem(string recipient)
+        {
+            try
+            {
+                var item = Metadata.Items.FirstOrDefault(x => x.EmailAddress == recipient);
+                if (item == null)
+                {
+                    _log.Log(LogSeverityEnum.Warn, $"Item with recipient '{recipient}' not found for reset.");
+                    return;
+                }
+
+                // Clean up any existing processed files
+                CleanupItemFiles(item);
+
+                // Reset all processing-related fields to initial state
+                item.State = EmailStateEnum.Ready;
+                item.FileNameAttachment = string.Empty;
+                item.SizeAttachment = 0;
+                item.UploadPermalink = string.Empty;
+                item.DownloadPassword = string.Empty;
+                item.ProcessedDate = null;
+                item.SentDate = null;
+                item.ErrorMessage = null;
+
+                // Notify listeners and save changes
+                UpdateItemState(item, EmailStateEnum.Ready, 0);
+                SaveMetadata();
+
+                _log.Log(LogSeverityEnum.Info, $"Reset item for recipient: {recipient}");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error resetting item for recipient '{recipient}': {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Gracefully stops all processing operations by setting the cancellation token.
+        /// Currently running operations will complete, but no new ones will start.
+        /// </summary>
+        public void Stop()
+        {
+            _cancellationTokenSource?.Cancel();
+            _log.Log(LogSeverityEnum.Info, "Processing stop requested.");
+        }
+
+        #endregion
+
+        #region Private Methods
+
+        /// <summary>
+        /// Creates the required directory structure for file management.
+        /// Sets up .processing, .sending, and .sent directories if they don't exist.
+        /// </summary>
+        private void InitializeDirectoryStructure()
+        {
+            try
+            {
+                _fileSystem.Directory.CreateDirectory(_processingDir);
+                _fileSystem.Directory.CreateDirectory(_sendingDir);
+                _fileSystem.Directory.CreateDirectory(_sentDir);
+                _log.Log(LogSeverityEnum.Info, "Directory structure initialized.");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error initializing directory structure: {ex.Message}");
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// Loads existing metadata from metadata.json file or creates new metadata structure.
+        /// After loading, scans the input directory for new or changed recipient folders.
+        /// </summary>
+        private void LoadOrCreateMetadata()
+        {
+            try
+            {
+                lock (_metadataLock)
+                {
+                    if (_fileSystem.File.Exists(_metadataFile))
+                    {
+                        // Load existing metadata from file
+                        var json = _fileSystem.File.ReadAllText(_metadataFile);
+                        Metadata = JsonSerializer.Deserialize<Metadata>(json) ?? new Metadata();
+                        _log.Log(LogSeverityEnum.Info, "Metadata loaded from file.");
+                    }
+                    else
+                    {
+                        // Create new metadata structure
+                        Metadata = new Metadata();
+                        _log.Log(LogSeverityEnum.Info, "New metadata created.");
+                    }
+
+                    // Scan directories and update metadata with current state
+                    ScanAndUpdateMetadata();
+                }
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error loading/creating metadata: {ex.Message}");
+                Metadata = new Metadata();
+            }
+        }
+
+        /// <summary>
+        /// Scans the input directory for recipient folders and updates metadata accordingly.
+        /// - Adds new items for newly discovered directories
+        /// - Updates file counts and order information for existing items
+        /// - Removes items for directories that no longer exist
+        /// </summary>
+        private void ScanAndUpdateMetadata()
+        {
+            try
+            {
+                // Get all directories that don't start with "." (hidden/system directories)
+                var directories = _fileSystem.Directory.GetDirectories(_inputPath)
+                    .Where(d => !_fileSystem.Path.GetFileName(d).StartsWith("."))
+                    .ToList();
+
+                foreach (var dir in directories)
+                {
+                    var dirName = _fileSystem.Path.GetFileName(dir);
+                    var emailAddress = dirName; // Assume directory name is the email address
+
+                    var existingItem = Metadata.Items.FirstOrDefault(x => x.DirectoryName == dirName);
+                    if (existingItem == null)
+                    {
+                        // Create new metadata item for this directory
+                        var newItem = CreateMetadataItem(dir, dirName, emailAddress);
+                        Metadata.Items.Add(newItem);
+                        _log.Log(LogSeverityEnum.Info, $"Added new item for directory: {dirName}");
+                    }
+                    else
+                    {
+                        // Update existing item with current file information
+                        UpdateMetadataItem(existingItem, dir);
+                    }
+                }
+
+                // Clean up metadata for directories that no longer exist
+                var existingDirs = directories.Select(d => _fileSystem.Path.GetFileName(d)).ToHashSet();
+                Metadata.Items.RemoveAll(item => !existingDirs.Contains(item.DirectoryName));
+
+                // Save updated metadata
+                Metadata.Modified = DateTime.Now;
+                SaveMetadata();
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error scanning and updating metadata: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Creates a new MetadataItem for a discovered directory.
+        /// Scans JPG files and extracts order numbers from filenames.
+        /// </summary>
+        /// <param name="directoryPath">Full path to the directory</param>
+        /// <param name="dirName">Directory name (typically email address)</param>
+        /// <param name="emailAddress">Email address for this item</param>
+        /// <returns>Newly created MetadataItem</returns>
+        private MetadataItem CreateMetadataItem(string directoryPath, string dirName, string emailAddress)
+        {
+            var jpgFiles = _fileSystem.Directory.GetFiles(directoryPath, "*.jpg");
+            var orders = ExtractOrderNumbers(jpgFiles);
+
+            return new MetadataItem
+            {
+                DirectoryName = dirName,
+                EmailAddress = emailAddress,
+                State = EmailStateEnum.Ready,
+                Subject = _emailTemplate.Subject,
+                Body = _emailTemplate.Body,
+                InternalFilesCount = jpgFiles.Length,
+                Orders = string.Join(",", orders.OrderBy(x => x))
+            };
+        }
+
+        /// <summary>
+        /// Updates an existing MetadataItem with current file information.
+        /// Refreshes file count and order numbers without changing processing state.
+        /// </summary>
+        /// <param name="item">Existing metadata item to update</param>
+        /// <param name="directoryPath">Path to the directory to scan</param>
+        private void UpdateMetadataItem(MetadataItem item, string directoryPath)
+        {
+            var jpgFiles = _fileSystem.Directory.GetFiles(directoryPath, "*.jpg");
+            var orders = ExtractOrderNumbers(jpgFiles);
+
+            item.InternalFilesCount = jpgFiles.Length;
+            item.Orders = string.Join(",", orders.OrderBy(x => x));
+        }
+
+        /// <summary>
+        /// Extracts order numbers from JPG filenames using regex pattern matching.
+        /// Expects filenames in format: "{orderNumber}_{description}.jpg"
+        /// Example: "1001_photo1.jpg" extracts order "1001"
+        /// </summary>
+        /// <param name="filePaths">Array of file paths to process</param>
+        /// <returns>List of unique order numbers found</returns>
+        private List<string> ExtractOrderNumbers(string[] filePaths)
+        {
+            var orders = new List<string>();
+            var pattern = @"^(\d+)_.*\.jpg$"; // Match: digits_anything.jpg
+
+            foreach (var filePath in filePaths)
+            {
+                var fileName = _fileSystem.Path.GetFileName(filePath);
+                var match = Regex.Match(fileName, pattern);
+                if (match.Success)
+                {
+                    orders.Add(match.Groups[1].Value);
+                }
+            }
+
+            return orders.Distinct().ToList();
+        }
+
+        /// <summary>
+        /// Compresses all JPG files in the item's directory into a ZIP archive.
+        /// Updates processing state and progress throughout the operation.
+        /// Generated ZIP filename includes normalized recipient and timestamp.
+        /// </summary>
+        /// <param name="item">Metadata item to process</param>
+        private async Task CompressFilesAsync(MetadataItem item)
+        {
+            UpdateItemState(item, EmailStateEnum.Processing, 10);
+
+            var sourceDir = _fileSystem.Path.Combine(_inputPath, item.DirectoryName);
+            var normalizedRecipient = NormalizeRecipientForFileName(item.EmailAddress);
+            var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+            var zipFileName = $"studioufoto_{normalizedRecipient}_{timestamp}.zip";
+            var zipPath = _fileSystem.Path.Combine(_processingDir, zipFileName);
+
+            try
+            {
+                var jpgFiles = _fileSystem.Directory.GetFiles(sourceDir, "*.jpg");
+                
+                // Create ZIP archive using FileSystem abstraction for testability
+                using (var zipFileStream = _fileSystem.File.Create(zipPath))
+                using (var zip = new ZipArchive(zipFileStream, ZipArchiveMode.Create))
+                {
+                    for (int i = 0; i < jpgFiles.Length; i++)
+                    {
+                        var file = jpgFiles[i];
+                        var entryName = _fileSystem.Path.GetFileName(file);
+                        var entry = zip.CreateEntry(entryName);
+                        
+                        // Copy file content to ZIP entry
+                        using (var entryStream = entry.Open())
+                        using (var fileStream = _fileSystem.File.OpenRead(file))
+                        {
+                            await fileStream.CopyToAsync(entryStream);
+                        }
+                        
+                        // Update progress (10-40% for compression phase)
+                        var progress = 10 + (int)((i + 1.0) / jpgFiles.Length * 30);
+                        UpdateItemState(item, EmailStateEnum.Processing, progress);
+                    }
+                }
+
+                // Update metadata with compression results
+                var fileInfo = _fileSystem.FileInfo.New(zipPath);
+                item.FileNameAttachment = zipFileName;
+                item.SizeAttachment = (int)fileInfo.Length;
+                item.ProcessedDate = DateTime.Now;
+
+                SaveMetadata();
+                _log.Log(LogSeverityEnum.Info, $"Compressed {jpgFiles.Length} files to: {zipFileName}");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error compressing files for {item.EmailAddress}: {ex.Message}");
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// Uploads the compressed ZIP file to Temporary Shared Storage service.
+        /// Generates download password and retrieves permalink for recipient access.
+        /// </summary>
+        /// <param name="item">Metadata item with compressed file to upload</param>
+        private async Task UploadCompressedFileAsync(MetadataItem item)
+        {
+            UpdateItemState(item, EmailStateEnum.Uploading, 50);
+
+            var zipPath = _fileSystem.Path.Combine(_processingDir, item.FileNameAttachment);
+            
+            try
+            {
+                // Prepare upload request with file details and security settings
+                var uploadRequest = new UploadFileRequest
+                {
+                    FilePath = zipPath,
+                    Description = $"Studio photos for {item.EmailAddress}",
+                    Password = GenerateDownloadPassword(),
+                    Message = "Studio photos ready for download",
+                    Reference = $"STUDIO_{NormalizeRecipientForFileName(item.EmailAddress)}",
+                    ActiveFrom = DateTime.Now,
+                    ActiveTo = DateTime.Now.AddDays(30), // 30-day expiration
+                    ChunkSize = 1048576 // 1MB chunks for reliable upload
+                };
+
+                // Execute upload and capture result
+                var result = await _uploadService.UploadFileAsync(uploadRequest);
+                
+                // Store upload results in metadata
+                item.UploadPermalink = result.Item1;
+                item.DownloadPassword = uploadRequest.Password;
+
+                UpdateItemState(item, EmailStateEnum.Uploading, 80);
+                SaveMetadata();
+
+                _log.Log(LogSeverityEnum.Info, $"Successfully uploaded file for {item.EmailAddress}. Permalink: {result.Item1}");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error uploading file for {item.EmailAddress}: {ex.Message}");
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// Personalizes the email template with recipient-specific information and
+        /// moves the ZIP file from processing to sending directory.
+        /// Replaces template placeholders with actual values.
+        /// </summary>
+        /// <param name="item">Metadata item to prepare for sending</param>
+        private async Task PrepareEmailAndMoveAsync(MetadataItem item)
+        {
+            try
+            {
+                // Personalize email body by replacing template placeholders
+                var personalizedBody = _emailTemplate.Body
+                    .Replace("{{DOWNLOAD_LINK}}", item.UploadPermalink)
+                    .Replace("{{PASSWORD}}", item.DownloadPassword)
+                    .Replace("{{RECIPIENT}}", item.EmailAddress)
+                    .Replace("{{ORDERS}}", item.Orders);
+
+                item.Body = personalizedBody;
+
+                // Move ZIP file from processing to sending directory
+                var sourceFile = _fileSystem.Path.Combine(_processingDir, item.FileNameAttachment);
+                var targetFile = _fileSystem.Path.Combine(_sendingDir, item.FileNameAttachment);
+                
+                if (_fileSystem.File.Exists(sourceFile))
+                {
+                    _fileSystem.File.Move(sourceFile, targetFile);
+                }
+
+                UpdateItemState(item, EmailStateEnum.Sending, 90);
+                SaveMetadata();
+
+                _log.Log(LogSeverityEnum.Info, $"Prepared email and moved file to sending for {item.EmailAddress}");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error preparing email for {item.EmailAddress}: {ex.Message}");
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// Sends the personalized email to the recipient using SMTP.
+        /// Email contains download link, password, and order information.
+        /// </summary>
+        /// <param name="item">Metadata item with email details to send</param>
+        private async Task SendEmailAsync(MetadataItem item)
+        {
+            try
+            {
+                using (var message = new MailMessage())
+                {
+                    message.To.Add(item.EmailAddress);
+                    message.Subject = item.Subject;
+                    message.Body = item.Body;
+                    message.IsBodyHtml = true; // Support HTML formatting in email body
+
+                    await _smtpClient.SendMailAsync(message);
+                }
+
+                item.SentDate = DateTime.Now;
+                _log.Log(LogSeverityEnum.Info, $"Email sent successfully to {item.EmailAddress}");
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error sending email to {item.EmailAddress}: {ex.Message}");
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// Handles the final result of email sending operation.
+        /// Moves files to appropriate directory and updates item state based on success/failure.
+        /// </summary>
+        /// <param name="item">Metadata item that was processed</param>
+        /// <param name="success">Whether the entire process completed successfully</param>
+        private async Task HandleSendingResultAsync(MetadataItem item, bool success)
+        {
+            try
+            {
+                var sourceFile = _fileSystem.Path.Combine(_sendingDir, item.FileNameAttachment);
+                
+                if (success)
+                {
+                    // Move to sent folder for successful completions
+                    var targetFile = _fileSystem.Path.Combine(_sentDir, item.FileNameAttachment);
+                    if (_fileSystem.File.Exists(sourceFile))
+                    {
+                        _fileSystem.File.Move(sourceFile, targetFile);
+                    }
+                    
+                    UpdateItemState(item, EmailStateEnum.Sent, 100);
+                    _log.Log(LogSeverityEnum.Info, $"Successfully completed processing for {item.EmailAddress}");
+                }
+                else
+                {
+                    // Keep in sending folder for failed attempts, set error state
+                    UpdateItemState(item, EmailStateEnum.Error, 0);
+                    _log.Log(LogSeverityEnum.Error, $"Failed to complete processing for {item.EmailAddress}");
+                }
+
+                SaveMetadata();
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error handling sending result for {item.EmailAddress}: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Updates an item's state and fires the state change event for subscribers.
+        /// Provides a centralized way to track state transitions.
+        /// </summary>
+        /// <param name="item">Item whose state is changing</param>
+        /// <param name="state">New state value</param>
+        /// <param name="progress">Progress percentage (0-100)</param>
+        private void UpdateItemState(MetadataItem item, EmailStateEnum state, int progress)
+        {
+            item.State = state;
+            EmailItemStateChanged?.Invoke(this, item.EmailAddress, state, progress);
+        }
+
+        /// <summary>
+        /// Saves the current metadata to the metadata.json file in a thread-safe manner.
+        /// Updates the modification timestamp and formats JSON for readability.
+        /// </summary>
+        private void SaveMetadata()
+        {
+            try
+            {
+                lock (_metadataLock)
+                {
+                    Metadata.Modified = DateTime.Now;
+                    var json = JsonSerializer.Serialize(Metadata, new JsonSerializerOptions 
+                    { 
+                        WriteIndented = true // Pretty-print for human readability
+                    });
+                    _fileSystem.File.WriteAllText(_metadataFile, json);
+                }
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error saving metadata: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Removes all processed files associated with an email item.
+        /// Cleans up ZIP files from processing, sending, and sent directories.
+        /// Used during item reset operations.
+        /// </summary>
+        /// <param name="item">Item whose files should be cleaned up</param>
+        private void CleanupItemFiles(MetadataItem item)
+        {
+            try
+            {
+                var filesToClean = new[]
+                {
+                    _fileSystem.Path.Combine(_processingDir, item.FileNameAttachment),
+                    _fileSystem.Path.Combine(_sendingDir, item.FileNameAttachment),
+                    _fileSystem.Path.Combine(_sentDir, item.FileNameAttachment)
+                };
+
+                foreach (var file in filesToClean)
+                {
+                    if (_fileSystem.File.Exists(file))
+                    {
+                        _fileSystem.File.Delete(file);
+                        _log.Log(LogSeverityEnum.Info, $"Deleted file: {file}");
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                _log.Log(LogSeverityEnum.Error, $"Error cleaning up files for {item.EmailAddress}: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// Normalizes an email address for use in filenames by replacing problematic characters.
+        /// Converts @ to _at_ and . to _dot_ to create filesystem-safe names.
+        /// </summary>
+        /// <param name="email">Email address to normalize</param>
+        /// <returns>Filesystem-safe version of the email address</returns>
+        private string NormalizeRecipientForFileName(string email)
+        {
+            return email.Replace("@", "_at_").Replace(".", "_dot_");
+        }
+
+        /// <summary>
+        /// Generates a cryptographically secure random password for file downloads.
+        /// Creates a 12-character password using uppercase, lowercase, and numeric characters.
+        /// </summary>
+        /// <returns>Randomly generated password string</returns>
+        private string GenerateDownloadPassword()
+        {
+            var random = new Random();
+            const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+            return new string(Enumerable.Repeat(chars, 12)
+                .Select(s => s[random.Next(s.Length)]).ToArray());
+        }
+
+        #endregion
+
+        #region IDisposable
+
+        /// <summary>
+        /// Disposes of the processor and all managed resources.
+        /// Implements the Dispose pattern for proper resource cleanup.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Protected implementation of Dispose pattern.
+        /// Properly disposes of cancellation token and SMTP client resources.
+        /// </summary>
+        /// <param name="disposing">True if disposing managed resources</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_disposed)
+            {
+                if (disposing)
+                {
+                    // Dispose managed resources
+                    _cancellationTokenSource?.Cancel();
+                    _cancellationTokenSource?.Dispose();
+                    _smtpClient?.Dispose();
+                }
+                _disposed = true;
+            }
+        }
+
+        #endregion
+    }
+}

+ 52 - 0
App/EmailStorage/EmailTemplate.cs

@@ -0,0 +1,52 @@
+using System.Text.Json.Serialization;
+
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Represents an email template that can be used to send personalized emails to recipients.
+    /// Templates support placeholder replacement for dynamic content such as download links,
+    /// passwords, recipient information, and order details.
+    /// 
+    /// Common placeholders that can be used in Subject and Body:
+    /// - {{DOWNLOAD_LINK}} - The download URL for the shared file
+    /// - {{PASSWORD}} - The password required to access the download
+    /// - {{RECIPIENT}} - The recipient's email address
+    /// - {{ORDERS}} - Comma-separated list of order numbers
+    /// </summary>
+    public class EmailTemplate
+    {
+        /// <summary>
+        /// Unique identifier/code for this email template.
+        /// Used to reference specific templates in configuration or selection logic.
+        /// Example: "STUDIO_PHOTOS", "ORDER_CONFIRMATION", etc.
+        /// </summary>
+        [JsonPropertyName("code")]
+        [JsonRequired]
+        public string Code { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Email subject line template.
+        /// Can contain placeholders that will be replaced with actual values
+        /// when the email is prepared for sending.
+        /// Example: "Your Studio Photos are Ready - Orders {{ORDERS}}"
+        /// </summary>
+        [JsonPropertyName("subject")]
+        [JsonRequired]
+        public string Subject { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Email body template in HTML or plain text format.
+        /// Supports placeholder replacement for personalization.
+        /// Should include instructions for downloading files and using passwords.
+        /// 
+        /// Example body might include:
+        /// - Greeting with {{RECIPIENT}}
+        /// - Download instructions with {{DOWNLOAD_LINK}}
+        /// - Password information with {{PASSWORD}}
+        /// - Order reference with {{ORDERS}}
+        /// </summary>
+        [JsonPropertyName("body")]
+        [JsonRequired]
+        public string Body { get; set; } = string.Empty;
+    }
+}

+ 186 - 0
App/EmailStorage/Metadata.cs

@@ -0,0 +1,186 @@
+using System.Text.Json.Serialization;
+
+namespace qdr.app.studiou.orders2printpack.EmailStorage
+{
+    /// <summary>
+    /// Root metadata container that holds information about the overall processing system
+    /// and maintains a collection of individual email items. This is the main data structure
+    /// that gets serialized to/from the metadata.json file.
+    /// </summary>
+    public class Metadata
+    {
+        /// <summary>
+        /// Timestamp when this metadata file was first created.
+        /// This helps track the age of the processing batch and provides audit information.
+        /// </summary>
+        [JsonRequired]
+        [JsonPropertyName("metadata-created")]
+        public DateTime Created { get; set; } = DateTime.Now;
+        
+        /// <summary>
+        /// Timestamp of the last modification to this metadata.
+        /// Updated whenever items are added, removed, or their state changes.
+        /// Null when no modifications have been made since creation.
+        /// </summary>
+        [JsonPropertyName("metadata-modified")]
+        public DateTime? Modified { get; set; } = null;
+
+        /// <summary>
+        /// Collection of individual email items being processed.
+        /// Each item represents one recipient with their associated files and processing state.
+        /// </summary>
+        [JsonRequired]
+        [JsonPropertyName("items")]
+        public List<MetadataItem> Items { get; set; } = new List<MetadataItem>();
+    }
+
+    /// <summary>
+    /// Represents a single email item with all associated processing information.
+    /// Contains details about the recipient, files, processing state, and email content.
+    /// This is the core data structure that tracks each individual email through the workflow.
+    /// </summary>
+    public class MetadataItem
+    {
+        #region Directory and File Information
+
+        /// <summary>
+        /// Name of the directory containing files for this email item.
+        /// Typically matches or is derived from the recipient's email address.
+        /// Used to locate the source files for processing.
+        /// </summary>
+        [JsonPropertyName("directory-name")]
+        [JsonRequired]
+        public string DirectoryName { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Current processing state of this email item.
+        /// Tracks progression through Ready -> Processing -> Uploading -> Sending -> Sent.
+        /// Can also be Error if something goes wrong.
+        /// </summary>
+        [JsonPropertyName("state")]
+        [JsonRequired]
+        public EmailStateEnum State { get; set; } = EmailStateEnum.Ready;
+
+        #endregion
+
+        #region Recipient Information
+
+        /// <summary>
+        /// Email address of the recipient who will receive the processed files.
+        /// This is the primary identifier for the email item.
+        /// </summary>
+        [JsonPropertyName("email-address")]
+        [JsonRequired]
+        public string EmailAddress { get; set; } = string.Empty;
+
+        #endregion
+
+        #region Email Content
+
+        /// <summary>
+        /// Subject line for the email that will be sent to the recipient.
+        /// Initially populated from the email template, may be personalized during processing.
+        /// </summary>
+        [JsonPropertyName("subject")]
+        [JsonRequired]
+        public string Subject { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Body content of the email in HTML or plain text format.
+        /// Initially from template, gets personalized with download links, passwords, etc.
+        /// </summary>
+        [JsonPropertyName("body")]
+        [JsonRequired]
+        public string Body { get; set; } = string.Empty;
+
+        #endregion
+
+        #region File and Attachment Information
+
+        /// <summary>
+        /// Filename of the compressed attachment that will be uploaded and sent.
+        /// Generated during processing and includes timestamp for uniqueness.
+        /// Example: "studioufoto_user_at_example_dot_com_20240530_143022.zip"
+        /// </summary>
+        [JsonPropertyName("attachment")]
+        [JsonRequired]
+        public string FileNameAttachment { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Size of the compressed attachment file in bytes.
+        /// Populated after compression is complete, used for upload progress tracking.
+        /// </summary>
+        [JsonPropertyName("attachment-size")]
+        [JsonRequired]
+        public int SizeAttachment { get; set; } = 0;
+
+        /// <summary>
+        /// Number of individual files (typically JPG images) contained within the compressed attachment.
+        /// Derived from scanning the source directory during metadata update.
+        /// </summary>
+        [JsonPropertyName("attachment-content-count")]
+        [JsonRequired]
+        public int InternalFilesCount { get; set; } = 0;
+
+        #endregion
+
+        #region Business Logic
+
+        /// <summary>
+        /// Comma-separated list of order numbers extracted from the filenames.
+        /// Used for business reference and included in email communications.
+        /// Example: "1001,1002,1003" extracted from files like "1001_photo1.jpg", "1002_photo1.jpg"
+        /// </summary>
+        [JsonPropertyName("orders")]
+        [JsonRequired]
+        public string Orders { get; set; } = string.Empty;
+
+        #endregion
+
+        #region Upload and Download Information
+
+        /// <summary>
+        /// Permanent link URL where the recipient can download their files.
+        /// Generated by the Temporary Shared Storage service after successful upload.
+        /// This link is included in the email sent to the recipient.
+        /// </summary>
+        [JsonPropertyName("upload-permalink")]
+        public string UploadPermalink { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Password required to access the download link.
+        /// Generated during upload process for security.
+        /// Included in the email so recipient can access their files.
+        /// </summary>
+        [JsonPropertyName("download-password")]
+        public string DownloadPassword { get; set; } = string.Empty;
+
+        #endregion
+
+        #region Audit and Tracking
+
+        /// <summary>
+        /// Timestamp when this item completed processing (compression and upload).
+        /// Null until processing is complete, used for audit and performance tracking.
+        /// </summary>
+        [JsonPropertyName("processed-date")]
+        public DateTime? ProcessedDate { get; set; }
+        
+        /// <summary>
+        /// Timestamp when the email was successfully sent to the recipient.
+        /// Null until email sending is complete, marks final completion of the workflow.
+        /// </summary>
+        [JsonPropertyName("sent-date")]
+        public DateTime? SentDate { get; set; }
+        
+        /// <summary>
+        /// Error message if processing failed at any stage.
+        /// Null when no errors have occurred, populated when State is set to Error.
+        /// Used for debugging and manual intervention.
+        /// </summary>
+        [JsonPropertyName("error-message")]
+        public string? ErrorMessage { get; set; }
+
+        #endregion
+    }
+}

+ 0 - 0
Extensions/CsvParserUtility.cs → App/Extensions/CsvParserUtility.cs


+ 0 - 0
Extensions/CsvUtils.cs → App/Extensions/CsvUtils.cs


+ 0 - 0
Extensions/ExtendedParametrizedString.cs → App/Extensions/ExtendedParametrizedString.cs


+ 0 - 0
Extensions/ListViewExtension.cs → App/Extensions/ListViewExtension.cs


+ 0 - 0
Extensions/StringExt.cs → App/Extensions/StringExt.cs


+ 0 - 0
FormBatch.Designer.cs → App/FormBatch.Designer.cs


+ 0 - 0
FormBatch.cs → App/FormBatch.cs


+ 0 - 0
FormBatch.resx → App/FormBatch.resx


+ 256 - 0
App/FormDigitalSend.Designer.cs

@@ -0,0 +1,256 @@
+namespace qdr.app.studiou.orders2printpack
+{
+    partial class FormDigitalSend
+    {
+        /// <summary>
+        /// Required designer variable.
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        /// Clean up any resources being used.
+        /// </summary>
+        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows Form Designer generated code
+
+        /// <summary>
+        /// Required method for Designer support - do not modify
+        /// the contents of this method with the code editor.
+        /// </summary>
+        private void InitializeComponent()
+        {
+            menuStrip1 = new MenuStrip();
+            toolStripMenuItem1 = new ToolStripMenuItem();
+            mmOpenDir = new ToolStripMenuItem();
+            operaceToolStripMenuItem = new ToolStripMenuItem();
+            mmSend = new ToolStripMenuItem();
+            lvData = new ListView();
+            colDDStatus = new ColumnHeader();
+            colDDRecipient = new ColumnHeader();
+            colDDOrders = new ColumnHeader();
+            colDDFiles = new ColumnHeader();
+            colDDFilesSize = new ColumnHeader();
+            statusStrip1 = new StatusStrip();
+            tssCount = new ToolStripStatusLabel();
+            tssSent = new ToolStripStatusLabel();
+            tssProgress = new ToolStripProgressBar();
+            tssNotSentSize = new ToolStripStatusLabel();
+            tssFolder = new ToolStripStatusLabel();
+            panel1 = new Panel();
+            dlgOpenDir = new FolderBrowserDialog();
+            splitContainer1 = new SplitContainer();
+            lbLog = new ListBox();
+            menuStrip1.SuspendLayout();
+            statusStrip1.SuspendLayout();
+            ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
+            splitContainer1.Panel1.SuspendLayout();
+            splitContainer1.Panel2.SuspendLayout();
+            splitContainer1.SuspendLayout();
+            SuspendLayout();
+            // 
+            // menuStrip1
+            // 
+            menuStrip1.Items.AddRange(new ToolStripItem[] { toolStripMenuItem1, operaceToolStripMenuItem });
+            menuStrip1.Location = new Point(0, 0);
+            menuStrip1.Name = "menuStrip1";
+            menuStrip1.Size = new Size(800, 24);
+            menuStrip1.TabIndex = 0;
+            menuStrip1.Text = "menuStrip1";
+            // 
+            // toolStripMenuItem1
+            // 
+            toolStripMenuItem1.DropDownItems.AddRange(new ToolStripItem[] { mmOpenDir });
+            toolStripMenuItem1.Name = "toolStripMenuItem1";
+            toolStripMenuItem1.Size = new Size(57, 20);
+            toolStripMenuItem1.Text = "Soubor";
+            // 
+            // mmOpenDir
+            // 
+            mmOpenDir.Name = "mmOpenDir";
+            mmOpenDir.Size = new Size(272, 22);
+            mmOpenDir.Text = "Načti adresář s digitálním obsahem ...";
+            mmOpenDir.Click += mmOpenDir_Click;
+            // 
+            // operaceToolStripMenuItem
+            // 
+            operaceToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { mmSend });
+            operaceToolStripMenuItem.Name = "operaceToolStripMenuItem";
+            operaceToolStripMenuItem.Size = new Size(63, 20);
+            operaceToolStripMenuItem.Text = "Operace";
+            // 
+            // mmSend
+            // 
+            mmSend.Name = "mmSend";
+            mmSend.Size = new Size(180, 22);
+            mmSend.Text = "Pošli";
+            mmSend.Click += mmSend_Click;
+            // 
+            // lvData
+            // 
+            lvData.AllowColumnReorder = true;
+            lvData.Columns.AddRange(new ColumnHeader[] { colDDStatus, colDDRecipient, colDDOrders, colDDFiles, colDDFilesSize });
+            lvData.Dock = DockStyle.Fill;
+            lvData.FullRowSelect = true;
+            lvData.GridLines = true;
+            lvData.Location = new Point(0, 0);
+            lvData.Name = "lvData";
+            lvData.Size = new Size(800, 183);
+            lvData.TabIndex = 2;
+            lvData.UseCompatibleStateImageBehavior = false;
+            lvData.View = View.Details;
+            // 
+            // colDDStatus
+            // 
+            colDDStatus.Text = "Stav";
+            // 
+            // colDDRecipient
+            // 
+            colDDRecipient.Text = "Příjemce / Zákazník";
+            colDDRecipient.Width = 240;
+            // 
+            // colDDOrders
+            // 
+            colDDOrders.Text = "Objednávky";
+            colDDOrders.Width = 240;
+            // 
+            // colDDFiles
+            // 
+            colDDFiles.Text = "Počet souborů";
+            colDDFiles.Width = 80;
+            // 
+            // colDDFilesSize
+            // 
+            colDDFilesSize.Text = "Velikost (MB)";
+            colDDFilesSize.Width = 120;
+            // 
+            // statusStrip1
+            // 
+            statusStrip1.Items.AddRange(new ToolStripItem[] { tssCount, tssSent, tssProgress, tssNotSentSize, tssFolder });
+            statusStrip1.Location = new Point(0, 428);
+            statusStrip1.Name = "statusStrip1";
+            statusStrip1.Size = new Size(800, 22);
+            statusStrip1.TabIndex = 3;
+            statusStrip1.Text = "statusStrip1";
+            // 
+            // tssCount
+            // 
+            tssCount.Name = "tssCount";
+            tssCount.Size = new Size(83, 17);
+            tssCount.Text = "Počet Celkem:";
+            // 
+            // tssSent
+            // 
+            tssSent.Name = "tssSent";
+            tssSent.Size = new Size(52, 17);
+            tssSent.Text = "Posláno:";
+            // 
+            // tssProgress
+            // 
+            tssProgress.Name = "tssProgress";
+            tssProgress.Size = new Size(100, 16);
+            // 
+            // tssNotSentSize
+            // 
+            tssNotSentSize.Name = "tssNotSentSize";
+            tssNotSentSize.Size = new Size(110, 17);
+            tssNotSentSize.Text = "Neposláno velikost:";
+            // 
+            // tssFolder
+            // 
+            tssFolder.Name = "tssFolder";
+            tssFolder.Size = new Size(43, 17);
+            tssFolder.Text = "Složka:";
+            // 
+            // panel1
+            // 
+            panel1.Dock = DockStyle.Top;
+            panel1.Location = new Point(0, 24);
+            panel1.Name = "panel1";
+            panel1.Size = new Size(800, 38);
+            panel1.TabIndex = 4;
+            // 
+            // splitContainer1
+            // 
+            splitContainer1.Dock = DockStyle.Fill;
+            splitContainer1.Location = new Point(0, 62);
+            splitContainer1.Name = "splitContainer1";
+            splitContainer1.Orientation = Orientation.Horizontal;
+            // 
+            // splitContainer1.Panel1
+            // 
+            splitContainer1.Panel1.Controls.Add(lvData);
+            // 
+            // splitContainer1.Panel2
+            // 
+            splitContainer1.Panel2.Controls.Add(lbLog);
+            splitContainer1.Size = new Size(800, 366);
+            splitContainer1.SplitterDistance = 183;
+            splitContainer1.TabIndex = 5;
+            // 
+            // lbLog
+            // 
+            lbLog.Dock = DockStyle.Fill;
+            lbLog.FormattingEnabled = true;
+            lbLog.Location = new Point(0, 0);
+            lbLog.Name = "lbLog";
+            lbLog.Size = new Size(800, 179);
+            lbLog.TabIndex = 0;
+            // 
+            // FormDigitalSend
+            // 
+            AutoScaleDimensions = new SizeF(7F, 15F);
+            AutoScaleMode = AutoScaleMode.Font;
+            ClientSize = new Size(800, 450);
+            Controls.Add(splitContainer1);
+            Controls.Add(panel1);
+            Controls.Add(statusStrip1);
+            Controls.Add(menuStrip1);
+            MainMenuStrip = menuStrip1;
+            Name = "FormDigitalSend";
+            Text = "Odeslání digitálních dat";
+            menuStrip1.ResumeLayout(false);
+            menuStrip1.PerformLayout();
+            statusStrip1.ResumeLayout(false);
+            statusStrip1.PerformLayout();
+            splitContainer1.Panel1.ResumeLayout(false);
+            splitContainer1.Panel2.ResumeLayout(false);
+            ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
+            splitContainer1.ResumeLayout(false);
+            ResumeLayout(false);
+            PerformLayout();
+        }
+
+        #endregion
+
+        private MenuStrip menuStrip1;
+        private ToolStripMenuItem toolStripMenuItem1;
+        private ToolStripMenuItem mmOpenDir;
+        private ListView lvData;
+        private ColumnHeader colDDStatus;
+        private ColumnHeader colDDRecipient;
+        private ColumnHeader colDDOrders;
+        private ColumnHeader colDDFiles;
+        private ColumnHeader colDDFilesSize;
+        private ToolStripMenuItem operaceToolStripMenuItem;
+        private ToolStripMenuItem mmSend;
+        private StatusStrip statusStrip1;
+        private ToolStripStatusLabel tssCount;
+        private ToolStripStatusLabel tssSent;
+        private ToolStripProgressBar tssProgress;
+        private ToolStripStatusLabel tssNotSentSize;
+        private Panel panel1;
+        private ToolStripStatusLabel tssFolder;
+        private FolderBrowserDialog dlgOpenDir;
+        private SplitContainer splitContainer1;
+        private ListBox lbLog;
+    }
+}

+ 145 - 0
App/FormDigitalSend.cs

@@ -0,0 +1,145 @@
+using qdr.app.studiou.orders2printpack.Properties;
+using System.IO.Abstractions;
+
+namespace qdr.app.studiou.orders2printpack
+{
+    public partial class FormDigitalSend : Form
+    {
+        #region *** Cnstants ***
+        private const string CS_DigitalDirSent = ".sent";
+        private const string CS_DigitalDirTemp = ".temp";
+        private const string CS_DigitalMetadata = "metadata.json";
+        #endregion
+
+        #region *** Fields ***
+        private IFileSystem _fileSystem = new FileSystem();
+        private string? _openedDigitalDir = null;
+        private string? _openedDigitalDirSent = null;
+        private string? _openedDigitalDirTemp = null;
+        #endregion
+
+        #region *** Constructors ***
+        public FormDigitalSend()
+        {
+            InitializeComponent();
+        }
+        #endregion
+
+        #region *** Form Handlers ***
+        private void mmOpenDir_Click(object sender, EventArgs e)
+        {
+            dlgOpenDir.Description = "Otevřít další složku s digitálním obsahem ...";
+            dlgOpenDir.UseDescriptionForTitle = true;
+            dlgOpenDir.InitialDirectory = AppSettings.Default.LastDigitalDir;
+            if (dlgOpenDir.ShowDialog() == DialogResult.OK)
+            {
+                AppSettings.Default.LastDigitalDir = dlgOpenDir.SelectedPath;
+                AppSettings.Default.Save();
+                OpenDigitalDir(_openedDigitalDir);
+            }
+        }
+
+  
+
+        private void mmSend_Click(object sender, EventArgs e)
+        {
+
+        }
+        #endregion
+
+        #region *** Private Methods ***
+        #region **** Error Handling *****
+        private void BlockErrorHandled(Action block)
+        {
+            try
+            {
+                block();
+            }
+            catch (Exception ex)
+            {
+                MessageBox.Show(this, ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
+                RefreshToolButtons();
+            }
+        }
+        #endregion
+        #region **** Refresh UI *****
+        private void RefreshToolButtons()
+        {
+            throw new NotImplementedException();
+        }
+        #endregion
+
+        #region **** Digital Directory Operations *****
+        private void OpenDigitalDir(string selectedPath)
+        {
+            BlockErrorHandled(()=>{
+                EnsureDigitalDirStructureExists(selectedPath);
+                
+            });
+        }
+
+        private void EnsureDigitalDirStructureExists(string selectedPath)
+        {
+            if (string.IsNullOrEmpty(selectedPath) || !_fileSystem.Directory.Exists(selectedPath))
+            {
+                throw new DirectoryNotFoundException($"Adresář '{selectedPath}' neexistuje.");
+            }
+            _openedDigitalDir = selectedPath;
+            _openedDigitalDirSent = _fileSystem.Path.Combine(selectedPath, CS_DigitalDirSent);
+            _openedDigitalDirTemp = _fileSystem.Path.Combine(selectedPath, CS_DigitalDirTemp);
+
+            if (!_fileSystem.Directory.Exists(_openedDigitalDirSent))
+            {
+                _fileSystem.Directory.CreateDirectory(_openedDigitalDirSent);
+                Log($"Vytvořen adresář pro odeslaná data '{_openedDigitalDirSent}'");
+            }
+            if (!_fileSystem.Directory.Exists(_openedDigitalDirTemp))
+            {
+                _fileSystem.Directory.CreateDirectory(_openedDigitalDirTemp);
+                Log($"Vytvořen pracovní adresář '{_openedDigitalDirTemp}'");
+            }
+            EnsureDigitalMetadataExists(selectedPath);
+        }
+
+        private void EnsureDigitalMetadataExists(string selectedPath)
+        {
+            var metadataFilePath = _fileSystem.Path.Combine(selectedPath, CS_DigitalMetadata);
+            if (!_fileSystem.File.Exists(metadataFilePath))
+            {
+                RefreshMetadataFile(metadataFilePath);
+            }
+        }
+
+        private void RefreshMetadataFile(string metadataFilePath)
+        {
+            Log($"Aktualizuji soubor s metadaty '{metadataFilePath}'...");
+
+            var metadata = new EmailStorage.Metadata();
+            _fileSystem.File.WriteAllText(metadataFilePath, System.Text.Json.JsonSerializer.Serialize(metadata, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
+            Log($"Soubor s metadaty '{metadataFilePath}' byl vytvořen.");
+        }
+        #endregion
+        #region **** Logging *****
+        private void LogBreak()
+        {
+            Log(new string('-', 80));
+        }
+        private void LogCaption(string message)
+        {
+                        Log($"*** {message} ***");
+        }
+        private void LogError(string message)
+        {
+                        Log($"[ERROR] {message}");
+        }
+        private void Log(string message)
+        {
+            lbLog.BeginUpdate();
+            lbLog.Items.Insert(0, message);
+            lbLog.EndUpdate();
+        }
+        #endregion
+        #endregion
+
+    }
+}

+ 132 - 0
App/FormDigitalSend.resx

@@ -0,0 +1,132 @@
+<?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.Runtime.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:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <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" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </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" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+    <value>17, 17</value>
+  </metadata>
+  <metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+    <value>149, 17</value>
+  </metadata>
+  <metadata name="dlgOpenDir.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+    <value>282, 17</value>
+  </metadata>
+  <metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
+    <value>37</value>
+  </metadata>
+</root>

+ 0 - 0
FormLabelPrint.Designer.cs → App/FormLabelPrint.Designer.cs


+ 0 - 0
FormLabelPrint.cs → App/FormLabelPrint.cs


+ 0 - 0
FormLabelPrint.resx → App/FormLabelPrint.resx


+ 19 - 6
FormLauncher.Designer.cs → App/FormLauncher.Designer.cs

@@ -36,6 +36,7 @@
             butLabalPrint = new Button();
             lVersion = new Label();
             butEditCfg = new Button();
+            butDigitalDataSend = new Button();
             ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
             SuspendLayout();
             // 
@@ -62,7 +63,7 @@
             // 
             // butProductEdit
             // 
-            butProductEdit.Location = new Point(209, 42);
+            butProductEdit.Location = new Point(209, 76);
             butProductEdit.Name = "butProductEdit";
             butProductEdit.Size = new Size(253, 27);
             butProductEdit.TabIndex = 3;
@@ -72,7 +73,7 @@
             // 
             // butResetCfg
             // 
-            butResetCfg.Location = new Point(390, 116);
+            butResetCfg.Location = new Point(390, 150);
             butResetCfg.Name = "butResetCfg";
             butResetCfg.Size = new Size(72, 25);
             butResetCfg.TabIndex = 4;
@@ -82,7 +83,7 @@
             // 
             // butLabalPrint
             // 
-            butLabalPrint.Location = new Point(209, 75);
+            butLabalPrint.Location = new Point(209, 109);
             butLabalPrint.Name = "butLabalPrint";
             butLabalPrint.Size = new Size(253, 27);
             butLabalPrint.TabIndex = 5;
@@ -93,7 +94,7 @@
             // lVersion
             // 
             lVersion.AutoSize = true;
-            lVersion.Location = new Point(10, 130);
+            lVersion.Location = new Point(10, 175);
             lVersion.Name = "lVersion";
             lVersion.Size = new Size(70, 15);
             lVersion.TabIndex = 6;
@@ -101,7 +102,7 @@
             // 
             // butEditCfg
             // 
-            butEditCfg.Location = new Point(312, 116);
+            butEditCfg.Location = new Point(312, 150);
             butEditCfg.Name = "butEditCfg";
             butEditCfg.Size = new Size(72, 25);
             butEditCfg.TabIndex = 7;
@@ -109,11 +110,22 @@
             butEditCfg.UseVisualStyleBackColor = true;
             butEditCfg.Click += butEditCfg_Click;
             // 
+            // butDigitalDataSend
+            // 
+            butDigitalDataSend.Location = new Point(209, 43);
+            butDigitalDataSend.Name = "butDigitalDataSend";
+            butDigitalDataSend.Size = new Size(253, 27);
+            butDigitalDataSend.TabIndex = 8;
+            butDigitalDataSend.Text = "Odeslání digitálních dat";
+            butDigitalDataSend.UseVisualStyleBackColor = true;
+            butDigitalDataSend.Click += butDigitalDataSend_Click;
+            // 
             // FormLauncher
             // 
             AutoScaleDimensions = new SizeF(7F, 15F);
             AutoScaleMode = AutoScaleMode.Font;
-            ClientSize = new Size(477, 154);
+            ClientSize = new Size(479, 199);
+            Controls.Add(butDigitalDataSend);
             Controls.Add(butEditCfg);
             Controls.Add(lVersion);
             Controls.Add(butLabalPrint);
@@ -143,5 +155,6 @@
         private Button butLabalPrint;
         private Label lVersion;
         private Button butEditCfg;
+        private Button butDigitalDataSend;
     }
 }

+ 6 - 0
FormLauncher.cs → App/FormLauncher.cs

@@ -57,5 +57,11 @@ namespace qdr.app.studiou.orders2printpack
             var dlg = new dlgEditCfg();
             dlg.ShowDialog(this);
         }
+
+        private void butDigitalDataSend_Click(object sender, EventArgs e)
+        {
+            var form = new FormDigitalSend();
+            form.Show(this);
+        }
     }
 }

+ 0 - 0
FormLauncher.resx → App/FormLauncher.resx


+ 0 - 0
FormProductEdit.Designer.cs → App/FormProductEdit.Designer.cs


+ 0 - 0
FormProductEdit.cs → App/FormProductEdit.cs


+ 0 - 0
FormProductEdit.resx → App/FormProductEdit.resx


+ 0 - 0
LICENSE → App/LICENSE


+ 0 - 0
Labels/ExcelLabelGenerator.cs → App/Labels/ExcelLabelGenerator.cs


+ 0 - 0
ProductStorage/CategoryDto.cs → App/ProductStorage/CategoryDto.cs


+ 0 - 0
ProductStorage/IIdentifierDto.cs → App/ProductStorage/IIdentifierDto.cs


+ 0 - 0
ProductStorage/ProductDto.cs → App/ProductStorage/ProductDto.cs


+ 0 - 0
ProductStorage/ProductStorage.cs → App/ProductStorage/ProductStorage.cs


+ 0 - 0
ProductStorage/VariantDto.cs → App/ProductStorage/VariantDto.cs


+ 0 - 0
Program.cs → App/Program.cs


+ 48 - 0
Properties/AppSettings.Designer.cs → App/Properties/AppSettings.Designer.cs

@@ -348,5 +348,53 @@ namespace qdr.app.studiou.orders2printpack.Properties {
                 this["FormatDigitalContains"] = value;
             }
         }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("riyeVPRvz9QF1YmrhdSz9rgiVzhy3mSb")]
+        public string TSSApiKey {
+            get {
+                return ((string)(this["TSSApiKey"]));
+            }
+            set {
+                this["TSSApiKey"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("2097152")]
+        public int TSSChunkSizeBytes {
+            get {
+                return ((int)(this["TSSChunkSizeBytes"]));
+            }
+            set {
+                this["TSSChunkSizeBytes"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("https://www.studiou.cz/")]
+        public string TSSUri {
+            get {
+                return ((string)(this["TSSUri"]));
+            }
+            set {
+                this["TSSUri"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("")]
+        public string LastDigitalDir {
+            get {
+                return ((string)(this["LastDigitalDir"]));
+            }
+            set {
+                this["LastDigitalDir"] = value;
+            }
+        }
     }
 }

+ 12 - 0
Properties/AppSettings.settings → App/Properties/AppSettings.settings

@@ -84,5 +84,17 @@
     <Setting Name="FormatDigitalContains" Type="System.String" Scope="User">
       <Value Profile="(Default)">Digitální data</Value>
     </Setting>
+    <Setting Name="TSSApiKey" Type="System.String" Scope="User">
+      <Value Profile="(Default)">riyeVPRvz9QF1YmrhdSz9rgiVzhy3mSb</Value>
+    </Setting>
+    <Setting Name="TSSChunkSizeBytes" Type="System.Int32" Scope="User">
+      <Value Profile="(Default)">2097152</Value>
+    </Setting>
+    <Setting Name="TSSUri" Type="System.String" Scope="User">
+      <Value Profile="(Default)">https://www.studiou.cz/</Value>
+    </Setting>
+    <Setting Name="LastDigitalDir" Type="System.String" Scope="User">
+      <Value Profile="(Default)" />
+    </Setting>
   </Settings>
 </SettingsFile>

+ 0 - 0
Properties/PublishProfiles/FolderProfile.pubxml → App/Properties/PublishProfiles/FolderProfile.pubxml


+ 0 - 0
Properties/Resources.Designer.cs → App/Properties/Resources.Designer.cs


+ 0 - 0
Properties/Resources.resx → App/Properties/Resources.resx


+ 6 - 1
README.md → App/README.md

@@ -1,8 +1,13 @@
 # qdr.app.studiou.orders2printpack
 
 Proprietální řešení zpracování WooCommerce objednávek pro online tiskovou službu (WinForm)
-
 ## Changelog
+### 1.3.0 (2025-05-26)
+- Přidán TSS REST API client (v.1.0.0)
+- Rozšíření konfigurace o hodnoty pro TSS REST API client
+- Přidána obrazovka "Odeslání digitálních dat"
+
+
 ### 1.2.4 (2025-05-19)
 - Rozšíření podpory přípravy balíčků k tisku o genenrování digitálního obsahu podle cílového emailu
 - Přidán do konfigurace email a identifikace formátu digitálního obsahu

+ 0 - 0
Resources/logo.png → App/Resources/logo.png


+ 0 - 0
Resources/mail_header.jpg → App/Resources/mail_header.jpg


+ 86 - 8
dlgEditCfg.Designer.cs → App/dlgEditCfg.Designer.cs

@@ -54,11 +54,18 @@
             label12 = new Label();
             tbFormatDigital = new TextBox();
             label13 = new Label();
+            label14 = new Label();
+            tbTssApiKey = new TextBox();
+            label15 = new Label();
+            tbTssChunkSize = new TextBox();
+            label16 = new Label();
+            tbTssUrl = new TextBox();
+            label17 = new Label();
             SuspendLayout();
             // 
             // butCancel
             // 
-            butCancel.Location = new Point(392, 394);
+            butCancel.Location = new Point(392, 532);
             butCancel.Margin = new Padding(3, 2, 3, 2);
             butCancel.Name = "butCancel";
             butCancel.Size = new Size(82, 22);
@@ -69,7 +76,7 @@
             // 
             // butOk
             // 
-            butOk.Location = new Point(12, 394);
+            butOk.Location = new Point(12, 532);
             butOk.Margin = new Padding(3, 2, 3, 2);
             butOk.Name = "butOk";
             butOk.Size = new Size(82, 22);
@@ -252,15 +259,15 @@
             // label12
             // 
             label12.AutoSize = true;
-            label12.Location = new Point(19, 320);
+            label12.Location = new Point(19, 459);
             label12.Name = "label12";
-            label12.Size = new Size(45, 15);
+            label12.Size = new Size(48, 15);
             label12.TabIndex = 25;
-            label12.Text = "Ostatní";
+            label12.Text = "Ostatní:";
             // 
             // tbFormatDigital
             // 
-            tbFormatDigital.Location = new Point(20, 355);
+            tbFormatDigital.Location = new Point(20, 492);
             tbFormatDigital.Name = "tbFormatDigital";
             tbFormatDigital.Size = new Size(100, 23);
             tbFormatDigital.TabIndex = 27;
@@ -268,18 +275,82 @@
             // label13
             // 
             label13.AutoSize = true;
-            label13.Location = new Point(19, 337);
+            label13.Location = new Point(19, 474);
             label13.Name = "label13";
             label13.Size = new Size(95, 15);
             label13.TabIndex = 26;
             label13.Text = "Formát: Digitální";
             // 
+            // label14
+            // 
+            label14.AutoSize = true;
+            label14.Location = new Point(19, 323);
+            label14.Name = "label14";
+            label14.Size = new Size(171, 15);
+            label14.TabIndex = 28;
+            label14.Text = "Zpracování digitálního obsahu:";
+            // 
+            // tbTssApiKey
+            // 
+            tbTssApiKey.Location = new Point(20, 367);
+            tbTssApiKey.Name = "tbTssApiKey";
+            tbTssApiKey.Size = new Size(312, 23);
+            tbTssApiKey.TabIndex = 30;
+            // 
+            // label15
+            // 
+            label15.AutoSize = true;
+            label15.Location = new Point(19, 349);
+            label15.Name = "label15";
+            label15.Size = new Size(66, 15);
+            label15.TabIndex = 29;
+            label15.Text = "TSS APIKEY";
+            // 
+            // tbTssChunkSize
+            // 
+            tbTssChunkSize.Location = new Point(336, 367);
+            tbTssChunkSize.Name = "tbTssChunkSize";
+            tbTssChunkSize.Size = new Size(100, 23);
+            tbTssChunkSize.TabIndex = 32;
+            // 
+            // label16
+            // 
+            label16.AutoSize = true;
+            label16.Location = new Point(335, 349);
+            label16.Name = "label16";
+            label16.Size = new Size(99, 15);
+            label16.TabIndex = 31;
+            label16.Text = "ChunkSize (Bajty)";
+            // 
+            // tbTssUrl
+            // 
+            tbTssUrl.Location = new Point(19, 415);
+            tbTssUrl.Name = "tbTssUrl";
+            tbTssUrl.Size = new Size(312, 23);
+            tbTssUrl.TabIndex = 34;
+            // 
+            // label17
+            // 
+            label17.AutoSize = true;
+            label17.Location = new Point(18, 397);
+            label17.Name = "label17";
+            label17.Size = new Size(43, 15);
+            label17.TabIndex = 33;
+            label17.Text = "TSS Url";
+            // 
             // dlgEditCfg
             // 
             AutoScaleDimensions = new SizeF(7F, 15F);
             AutoScaleMode = AutoScaleMode.Font;
-            ClientSize = new Size(503, 488);
+            ClientSize = new Size(509, 627);
             ControlBox = false;
+            Controls.Add(tbTssUrl);
+            Controls.Add(label17);
+            Controls.Add(tbTssChunkSize);
+            Controls.Add(label16);
+            Controls.Add(tbTssApiKey);
+            Controls.Add(label15);
+            Controls.Add(label14);
             Controls.Add(tbFormatDigital);
             Controls.Add(label13);
             Controls.Add(label12);
@@ -343,5 +414,12 @@
         private Label label12;
         private TextBox tbFormatDigital;
         private Label label13;
+        private Label label14;
+        private TextBox tbTssApiKey;
+        private Label label15;
+        private TextBox tbTssChunkSize;
+        private Label label16;
+        private TextBox tbTssUrl;
+        private Label label17;
     }
 }

+ 17 - 0
dlgEditCfg.cs → App/dlgEditCfg.cs

@@ -36,6 +36,20 @@ namespace qdr.app.studiou.orders2printpack
             AppSettings.Default.CSVDelimiter = tbCSVDelimiter.Text;
             AppSettings.Default.MapColEmail = tbEmail.Text;
             AppSettings.Default.FormatDigitalContains = tbFormatDigital.Text;
+
+            AppSettings.Default.TSSApiKey = tbTssApiKey.Text;
+            if (!Uri.TryCreate(tbTssUrl.Text, UriKind.Absolute, out Uri tssUri) || string.IsNullOrWhiteSpace(tbTssUrl.Text))
+            {
+                MessageBox.Show("Vložte prosím validní TSS URL (https://www.some.com).", "Nevalidní vstup", MessageBoxButtons.OK, MessageBoxIcon.Error);
+                return;
+            }
+            tbTssUrl.Text = AppSettings.Default.TSSUri;
+            if (!int.TryParse(tbTssChunkSize.Text, out int chunkSize) || chunkSize <= 0)
+            {
+                MessageBox.Show("Vložte prosím kladné celé číslo pro velikost bloku dat pro TSS Chunk Size.", "Nevalidní vstup", MessageBoxButtons.OK, MessageBoxIcon.Error);
+                return;
+            }
+            AppSettings.Default.TSSChunkSizeBytes = chunkSize;
             AppSettings.Default.Save();
             Close();
         }
@@ -59,6 +73,9 @@ namespace qdr.app.studiou.orders2printpack
             tbCSVDelimiter.Text = AppSettings.Default.CSVDelimiter;
             tbEmail.Text = AppSettings.Default.MapColEmail;
             tbFormatDigital.Text = AppSettings.Default.FormatDigitalContains;
+            tbTssApiKey.Text = AppSettings.Default.TSSApiKey;
+            tbTssChunkSize.Text = AppSettings.Default.TSSChunkSizeBytes.ToString();
+            tbTssUrl.Text = AppSettings.Default.TSSUri;
         }
         #endregion
 

+ 0 - 0
dlgEditCfg.resx → App/dlgEditCfg.resx


+ 0 - 0
dlgImport.Designer.cs → App/dlgImport.Designer.cs


+ 0 - 0
dlgImport.cs → App/dlgImport.cs


+ 0 - 0
dlgImport.resx → App/dlgImport.resx


+ 0 - 0
dlgLabelSettings.Designer.cs → App/dlgLabelSettings.Designer.cs


+ 0 - 0
dlgLabelSettings.cs → App/dlgLabelSettings.cs


+ 0 - 0
dlgLabelSettings.resx → App/dlgLabelSettings.resx


+ 0 - 0
dlgProtocol.Designer.cs → App/dlgProtocol.Designer.cs


+ 0 - 0
dlgProtocol.cs → App/dlgProtocol.cs


+ 0 - 0
dlgProtocol.resx → App/dlgProtocol.resx


+ 0 - 0
dlgSettings.Designer.cs → App/dlgSettings.Designer.cs


+ 0 - 0
dlgSettings.cs → App/dlgSettings.cs


+ 0 - 0
dlgSettings.resx → App/dlgSettings.resx


+ 0 - 0
logo_icon.ico → App/logo_icon.ico


+ 6 - 5
qdr.app.studiou.orders2printpack.csproj → App/qdr.app.studiou.orders2printpack.csproj

@@ -10,15 +10,15 @@
     <Company>Quadarax</Company>
     <Product>Order2PrintPack</Product>
     <Description>Proprietální řešení tvorby balíčků obrázků pro online objednávky pro tiskovou službu.</Description>
-    <Copyright>Quadarax (c) 2024</Copyright>
+    <Copyright>Quadarax (c) 2025</Copyright>
     <PackageIcon>logo_icon.png</PackageIcon>
     <Title>StudioU Orders2PrintPack</Title>
     <SignAssembly>True</SignAssembly>
-    <AssemblyOriginatorKeyFile>bo_strong_key_pair.snk</AssemblyOriginatorKeyFile>
+    <AssemblyOriginatorKeyFile>..\bo_strong_key_pair.snk</AssemblyOriginatorKeyFile>
     <Platforms>AnyCPU;x86;x64</Platforms>
-    <AssemblyVersion>1.2.0.0</AssemblyVersion>
-    <FileVersion>1.2.0.0</FileVersion>
-    <Version>1.2.4</Version>
+    <AssemblyVersion>1.3.0.0</AssemblyVersion>
+    <FileVersion>1.3.0.0</FileVersion>
+    <Version>1.3.0</Version>
 	<ApplicationHighDpiMode>SystemAware</ApplicationHighDpiMode>
     <ForceDesignerDpiUnaware>true</ForceDesignerDpiUnaware>
   </PropertyGroup>
@@ -36,6 +36,7 @@
 
   <ItemGroup>
     <PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
+    <PackageReference Include="qdr.app.tss.client" Version="1.0.0-alpha" />
     <PackageReference Include="qdr.fnd.core" Version="0.0.8-alpha" />
   </ItemGroup>
 

+ 1 - 1
Setup/Setup.vdproj

@@ -124,7 +124,7 @@
         {
             "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_BD891965051B4B7DBDEE39DCD2A5C6EE"
             {
-            "SourcePath" = "8:..\\logo_icon.ico"
+            "SourcePath" = "8:..\\App\\logo_icon.ico"
             "TargetName" = "8:logo_icon.ico"
             "Tag" = "8:"
             "Folder" = "8:_96DC0B7F01834A1BA713E7E8A0904D3E"

+ 16 - 0
Test/UnitTest1.cs

@@ -0,0 +1,16 @@
+namespace Test
+{
+    public class Tests
+    {
+        [SetUp]
+        public void Setup()
+        {
+        }
+
+        [Test]
+        public void Test1()
+        {
+            Assert.Pass();
+        }
+    }
+}

+ 23 - 0
Test/qdr.app.studiou.orders2printpack.Test.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net9.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="coverlet.collector" Version="6.0.2" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
+    <PackageReference Include="NUnit" Version="4.2.2" />
+    <PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="NUnit.Framework" />
+  </ItemGroup>
+
+</Project>

+ 0 - 70
qdr.app.studiou.orders2printpack.csproj.Backup.tmp

@@ -1,70 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <OutputType>WinExe</OutputType>
-    <TargetFramework>net8.0-windows</TargetFramework>
-    <Nullable>enable</Nullable>
-    <UseWindowsForms>true</UseWindowsForms>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <ApplicationIcon>logo_icon.ico</ApplicationIcon>
-    <Company>Quadarax</Company>
-    <Product>Order2PrintPack</Product>
-    <Description>Proprietální řešení tvorby balíčků obrázků pro online objednávky pro tiskovou službu.</Description>
-    <Copyright>Quadarax (c) 2025</Copyright>
-    <PackageIcon>logo_icon.png</PackageIcon>
-    <Title>StudioU Orders2PrintPack</Title>
-    <SignAssembly>True</SignAssembly>
-    <AssemblyOriginatorKeyFile>bo_strong_key_pair.snk</AssemblyOriginatorKeyFile>
-    <Platforms>AnyCPU;x86;x64</Platforms>
-    <AssemblyVersion>1.0.2.0</AssemblyVersion>
-    <FileVersion>1.0.2.0</FileVersion>
-    <Version>1.0.2</Version>
-	<ForceDesignerDpiUnaware>true</ForceDesignerDpiUnaware>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <Content Include="logo_icon.ico" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Include="C:\Temp\pedro\logo_icon.png">
-      <Pack>True</Pack>
-      <PackagePath>\</PackagePath>
-    </None>
-  </ItemGroup>
-
-  <ItemGroup>
-    <PackageReference Include="qdr.fnd.core" Version="0.0.5-alpha" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Compile Update="Properties\AppSettings.Designer.cs">
-      <DesignTimeSharedInput>True</DesignTimeSharedInput>
-      <AutoGen>True</AutoGen>
-      <DependentUpon>AppSettings.settings</DependentUpon>
-    </Compile>
-    <Compile Update="Properties\Resources.Designer.cs">
-      <DesignTime>True</DesignTime>
-      <AutoGen>True</AutoGen>
-      <DependentUpon>Resources.resx</DependentUpon>
-    </Compile>
-  </ItemGroup>
-
-  <ItemGroup>
-    <EmbeddedResource Update="Properties\Resources.resx">
-      <Generator>ResXFileCodeGenerator</Generator>
-      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
-    </EmbeddedResource>
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Update="Properties\AppSettings.settings">
-      <Generator>SettingsSingleFileGenerator</Generator>
-      <LastGenOutput>AppSettings.Designer.cs</LastGenOutput>
-    </None>
-    <None Update="README.md">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
-
-</Project>

+ 15 - 1
qdr.app.studiou.orders2printpack.sln

@@ -3,10 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 VisualStudioVersion = 17.11.35312.102
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "qdr.app.studiou.orders2printpack", "qdr.app.studiou.orders2printpack.csproj", "{B85B4AB7-BD36-431F-A7C1-D83C9C073AE2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "qdr.app.studiou.orders2printpack", "App\qdr.app.studiou.orders2printpack.csproj", "{B85B4AB7-BD36-431F-A7C1-D83C9C073AE2}"
 EndProject
 Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "Setup\Setup.vdproj", "{6525CEB5-7957-4B38-A78B-9A1955DEDDDF}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qdr.app.studiou.orders2printpack.Test", "Test\qdr.app.studiou.orders2printpack.Test.csproj", "{C52407EF-452E-4F1C-9A3F-C80A78849314}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -38,6 +40,18 @@ Global
 		{6525CEB5-7957-4B38-A78B-9A1955DEDDDF}.Release|x64.Build.0 = Release
 		{6525CEB5-7957-4B38-A78B-9A1955DEDDDF}.Release|x86.ActiveCfg = Release
 		{6525CEB5-7957-4B38-A78B-9A1955DEDDDF}.Release|x86.Build.0 = Release
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|x64.Build.0 = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Debug|x86.Build.0 = Debug|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|x64.ActiveCfg = Release|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|x64.Build.0 = Release|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|x86.ActiveCfg = Release|Any CPU
+		{C52407EF-452E-4F1C-9A3F-C80A78849314}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE