using Microsoft.EntityFrameworkCore; using eprintServer.Data; using eprintServer.Entities; using eprintServer.Responses; using eprintServer.Requests; using eprintServer.DTOs; using eprintServer.Enums; using System.Text.Json; using System.Text.Json.Serialization; using eprintServer.Requests.CustomizableProduct; namespace eprintServer.Services; public class CustomizableProductService { private readonly DataContext _context; public CustomizableProductService(DataContext context) { _context = context; } /* public async Task CreateProductAsync(CreateCustomizableProductRequest request) { var category = await _context.ProductCategories.FindAsync(request.CategoryId); if (category == null) throw new Exception("Category not found"); var product = new CustomizableProduct { BasePrice = request.BasePrice, IsCanvasEnabled = request.IsCanvasEnabled, MinimumOrderQuantity = request.MinimumOrderQuantity, BaseDeliveryDays = request.BaseDeliveryDays, CategoryId = request.CategoryId, QuantityPricingConfig = request.QuantityPricingConfig, UnitWeight = request.UnitWeight }; if (request.AllowedQuantities != null && request.AllowedQuantities.Any()) { product.AllowedQuantities = request.AllowedQuantities; } foreach (var nameDto in request.Names) { product.Names.Add(new DynamicValue { Value = nameDto.Value, LanguageTag = nameDto.LanguageTag }); } foreach (var descDto in request.Descriptions) { product.Descriptions.Add(new DynamicValue { Value = descDto.Value, LanguageTag = descDto.LanguageTag }); } _context.CustomizableProducts.Add(product); await _context.SaveChangesAsync(); return await GetProductByIdAsync(product.Id); } */ public async Task CreateProductAsync(CreateCustomizableProductRequest request) { var category = await _context.ProductCategories.FindAsync(request.CategoryId); if (category == null) throw new Exception("Category not found"); var product = new CustomizableProduct { BasePrice = request.BasePrice, IsCanvasEnabled = request.IsCanvasEnabled, MinimumOrderQuantity = request.MinimumOrderQuantity, BaseDeliveryDays = request.BaseDeliveryDays, CategoryId = request.CategoryId, // UPDATED: Only assign if it's not an empty/default object to prevent mapping issues QuantityPricingConfig = request.QuantityPricingConfig?.Tiers != null && request.QuantityPricingConfig.Tiers.Any() ? request.QuantityPricingConfig : null, UnitWeight = request.UnitWeight }; // UPDATED: Added a null check and count check for AllowedQuantities // This ensures that 'null' passed from frontend doesn't trigger unexpected behavior if (request.AllowedQuantities != null && request.AllowedQuantities.Count > 0) { product.AllowedQuantities = request.AllowedQuantities; } else { product.AllowedQuantities = new List(); // UPDATED: Initialize to empty list if null } // UPDATED: Added null check for Names before iteration to prevent NullReferenceException if (request.Names != null) { foreach (var nameDto in request.Names) { product.Names.Add(new DynamicValue { Value = nameDto.Value, LanguageTag = nameDto.LanguageTag }); } } // UPDATED: Added null check for Descriptions before iteration if (request.Descriptions != null) { foreach (var descDto in request.Descriptions) { product.Descriptions.Add(new DynamicValue { Value = descDto.Value, LanguageTag = descDto.LanguageTag }); } } _context.CustomizableProducts.Add(product); await _context.SaveChangesAsync(); return await GetProductByIdAsync(product.Id); } public async Task UpdateProductAsync(long id, UpdateCustomizableProductRequest request) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .FirstOrDefaultAsync(p => p.Id == id); if (product == null) throw new Exception("Product not found"); if (request.BasePrice.HasValue) product.BasePrice = request.BasePrice.Value; if (request.UnitWeight.HasValue) { product.UnitWeight = request.UnitWeight.Value; } if (request.AllowedQuantities != null && request.AllowedQuantities.Any()) { product.AllowedQuantities = new List(request.AllowedQuantities); } if (request.IsCanvasEnabled.HasValue) product.IsCanvasEnabled = request.IsCanvasEnabled.Value; if (request.MinimumOrderQuantity.HasValue) product.MinimumOrderQuantity = request.MinimumOrderQuantity.Value; if (request.BaseDeliveryDays.HasValue) product.BaseDeliveryDays = request.BaseDeliveryDays.Value; if (request.CategoryId.HasValue) { var category = await _context.ProductCategories.FindAsync(request.CategoryId.Value); if (category == null) throw new Exception("Category not found"); product.CategoryId = request.CategoryId.Value; } if (request.Archive.HasValue) product.Archive = request.Archive.Value; //if (request.QuantityPricingConfig != null) product.QuantityPricingConfig = request.QuantityPricingConfig; if (request.QuantityPricingConfig != null) { product.QuantityPricingConfig = request.QuantityPricingConfig.FirstOrDefault(); } if (request.Names != null) { _context.DynamicValues.RemoveRange(product.Names); product.Names.Clear(); foreach (var nameDto in request.Names) { product.Names.Add(new DynamicValue { Value = nameDto.Value, LanguageTag = nameDto.LanguageTag }); } } if (request.Descriptions != null) { _context.DynamicValues.RemoveRange(product.Descriptions); product.Descriptions.Clear(); foreach (var descDto in request.Descriptions) { product.Descriptions.Add(new DynamicValue { Value = descDto.Value, LanguageTag = descDto.LanguageTag }); } } product.Timestamp = DateTime.UtcNow; await _context.SaveChangesAsync(); return await GetProductByIdAsync(product.Id); } /* public async Task GetProductByIdAsync(long id) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .Include(p => p.Steps.OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .FirstOrDefaultAsync(p => p.Id == id); if (product == null) throw new Exception("Product not found"); return product; } */ /* public async Task GetProductByIdAsync(long id) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .Include(p => p.Steps.OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .Include(p => p.Mockups) // <--- include the Mockups list .ThenInclude(m => m.File) // if Mockup has File .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) // if Mockup has Thumbnail .FirstOrDefaultAsync(p => p.Id == id); if (product == null) return null; return product; } */ public async Task GetProductByIdAsync(long id) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) // --- 3D Feature Includes --- .Include(p => p.TextureSlots) .ThenInclude(ts => ts.File) // Include the FileModel for each texture slot .Include(p => p.UvMapImage) // Include the FileModel for the UV Map .Include(p => p.ThreeDModelFile) // Include the actual 3D file (GLB/OBJ) // --- End 3D Includes --- .Include(p => p.Steps.OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .Include(p => p.Mockups) .ThenInclude(m => m.File) .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) .FirstOrDefaultAsync(p => p.Id == id); if (product == null) throw new Exception("Product not found"); return product; } /* public async Task GetProductDetailByIdAsync(long id) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Image) .Include(p => p.Descriptions) .Include(p => p.Category) .ThenInclude(c => c.Name) .Include(p => p.Steps.Where(s => !s.Archive).OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .FirstOrDefaultAsync(p => p.Id == id && !p.Archive); if (product == null) throw new Exception("Product not found"); // Map to DTO return new CustomizableProductDetailDto { Id = product.Id, Names = product.Names.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), Descriptions = product.Descriptions.Select(d => new DynamicValueDto { Value = d.Value, LanguageTag = d.LanguageTag }).ToList(), BasePrice = product.BasePrice, IsCanvasEnabled = product.IsCanvasEnabled, MinimumOrderQuantity = product.MinimumOrderQuantity, BaseDeliveryDays = product.BaseDeliveryDays, Image = product.Image, Archive = product.Archive, QuantityPricingConfig = product.QuantityPricingConfig, // JSONB property - no Include needed Category = new CategoryDto { Id = product.Category.Id, Name = product.Category.Name.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList() }, Steps = product.Steps.Select(s => new StepDetailDto { Id = s.Id, StepOrder = s.StepOrder, StepType = s.StepType.ToString(), StepName = s.StepName.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), FormElements = s.FormElements.Select(fe => new FormElementDetailDto { Id = fe.Id, Label = fe.Label.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), InputType = fe.InputType.ToString(), FieldName = fe.FieldName, IsRequired = fe.IsRequired, ElementOrder = fe.ElementOrder, DefaultValue = fe.DefaultValue, ValidationRules = fe.ValidationRules, Placeholder = fe.Placeholder, // MAPPING PRICING DATA PriceConfigType = fe.PriceConfigType?.ToString(), ColorPricingConfig = fe.ColorPricingConfig, QuantityPricingConfig = fe.QuantityPricingConfig, Options = fe.Options.Select(o => new FormElementOptionDetailDto { Id = o.Id, OptionLabel = o.OptionLabel.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), OptionValue = o.OptionValue, OptionOrder = o.OptionOrder, // MAPPING OPTION PRICE PriceConfig = o.PriceConfig }).ToList() }).ToList() }).ToList() }; } */ public async Task GetProductDetailByIdAsync(long id) { // Adding .AsNoTracking() for speed and .AsSplitQuery() to fix the 4.75s delay var product = await _context.CustomizableProducts .AsNoTracking() .AsSplitQuery() .Include(p => p.Names) .Include(p => p.Image) .Include(p => p.Descriptions) .Include(p => p.Category) .ThenInclude(c => c.Name) // --- 3D INCLUDES --- .Include(p => p.UvMapImage) .Include(p => p.ThreeDModelFile) .Include(p => p.TextureSlots) .ThenInclude(ts => ts.File) // --- STEPS & ELEMENTS --- .Include(p => p.Steps.Where(s => !s.Archive).OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) // --- MOCKUPS --- .Include(p => p.Mockups) .ThenInclude(m => m.File) .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) .FirstOrDefaultAsync(p => p.Id == id && !p.Archive); if (product == null) throw new Exception("Product not found"); // Mapping logic remains EXACTLY the same to ensure response consistency return new CustomizableProductDetailDto { Id = product.Id, IsBranchingEnabled = product.IsBranchingEnabled, Names = product.Names.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), Descriptions = product.Descriptions.Select(d => new DynamicValueDto { Value = d.Value, LanguageTag = d.LanguageTag }).ToList(), AllowedQuantities = product.AllowedQuantities, BasePrice = product.BasePrice, IsCanvasEnabled = product.IsCanvasEnabled, MinimumOrderQuantity = product.MinimumOrderQuantity, BaseDeliveryDays = product.BaseDeliveryDays, UnitWeight = product.UnitWeight, Image = product.Image, Archive = product.Archive, QuantityPricingConfig = product.QuantityPricingConfig, Has3DPreview = product.Has3DPreview, ThreeDConfig = product.ThreeDConfig, UvMapImage = product.UvMapImage, ThreeDModelFile = product.ThreeDModelFile, TextureSlots = product.TextureSlots.Select(ts => new TextureSlotDto { Id = ts.Id, Name = ts.Name, TextureType = ts.TextureType.ToString(), UvCoordinates = ts.UvCoordinates, File = ts.File }).ToList(), Category = new CategoryDto { Id = product.Category.Id, Name = product.Category.Name.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList() }, Steps = product.Steps.Select(s => new StepDetailDto { Id = s.Id, NextStepId = s.NextStepId, StepOrder = s.StepOrder, StepType = s.StepType.ToString(), StepName = s.StepName.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), FormElements = s.FormElements.Select(fe => new FormElementDetailDto { Id = fe.Id, Label = fe.Label.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), InputType = fe.InputType.ToString(), FieldName = fe.FieldName, IsRequired = fe.IsRequired, ElementOrder = fe.ElementOrder, DefaultValue = fe.DefaultValue, ValidationRules = fe.ValidationRules, Placeholder = fe.Placeholder, PriceConfigType = fe.PriceConfigType?.ToString(), ColorPricingConfig = fe.ColorPricingConfig, QuantityPricingConfig = fe.QuantityPricingConfig, Options = fe.Options.Select(o => new FormElementOptionDetailDto { Id = o.Id, NextStepId = o.NextStepId, OptionLabel = o.OptionLabel.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), OptionValue = o.OptionValue, OptionOrder = o.OptionOrder, PriceConfig = o.PriceConfig }).ToList() }).ToList() }).ToList(), Mockups = product.Mockups.Select(m => new MockupDetailDto { Id = m.Id, Name = m.Name, DesignWidth = m.DesignWidth, DesignHeight = m.DesignHeight, PrintWidth = m.PrintWidth, PrintHeight = m.PrintHeight, File = m.File, Thumbnail = m.Thumbnail }).ToList() }; } /* public async Task GetProductDetailByIdAsync(long id) { var product = await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Image) .Include(p => p.Descriptions) .Include(p => p.Category) .ThenInclude(c => c.Name) // --- NEW 3D INCLUDES --- .Include(p => p.UvMapImage) .Include(p => p.ThreeDModelFile) .Include(p => p.TextureSlots) .ThenInclude(ts => ts.File) // ----------------------- .Include(p => p.Steps.Where(s => !s.Archive).OrderBy(s => s.StepOrder)) .ThenInclude(s => s.StepName) .Include(p => p.Steps) .ThenInclude(s => s.FormElements.OrderBy(fe => fe.ElementOrder)) .ThenInclude(fe => fe.Label) .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options.OrderBy(o => o.OptionOrder)) .ThenInclude(o => o.OptionLabel) .Include(p => p.Mockups) .ThenInclude(m => m.File) .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) .FirstOrDefaultAsync(p => p.Id == id && !p.Archive); if (product == null) throw new Exception("Product not found"); return new CustomizableProductDetailDto { Id = product.Id, Names = product.Names.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), Descriptions = product.Descriptions.Select(d => new DynamicValueDto { Value = d.Value, LanguageTag = d.LanguageTag }).ToList(), AllowedQuantities = product.AllowedQuantities, BasePrice = product.BasePrice, IsCanvasEnabled = product.IsCanvasEnabled, MinimumOrderQuantity = product.MinimumOrderQuantity, BaseDeliveryDays = product.BaseDeliveryDays, UnitWeight = product.UnitWeight, Image = product.Image, Archive = product.Archive, QuantityPricingConfig = product.QuantityPricingConfig, // --- MAPPING 3D DATA --- Has3DPreview = product.Has3DPreview, ThreeDConfig = product.ThreeDConfig, // JSONB maps directly UvMapImage = product.UvMapImage, ThreeDModelFile = product.ThreeDModelFile, TextureSlots = product.TextureSlots.Select(ts => new TextureSlotDto { Id = ts.Id, Name = ts.Name, TextureType = ts.TextureType.ToString(), UvCoordinates = ts.UvCoordinates, File = ts.File }).ToList(), // ----------------------- Category = new CategoryDto { Id = product.Category.Id, Name = product.Category.Name.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList() }, Steps = product.Steps.Select(s => new StepDetailDto { Id = s.Id, StepOrder = s.StepOrder, StepType = s.StepType.ToString(), StepName = s.StepName.Select(n => new DynamicValueDto { Value = n.Value, LanguageTag = n.LanguageTag }).ToList(), FormElements = s.FormElements.Select(fe => new FormElementDetailDto { Id = fe.Id, Label = fe.Label.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), InputType = fe.InputType.ToString(), FieldName = fe.FieldName, IsRequired = fe.IsRequired, ElementOrder = fe.ElementOrder, DefaultValue = fe.DefaultValue, ValidationRules = fe.ValidationRules, Placeholder = fe.Placeholder, PriceConfigType = fe.PriceConfigType?.ToString(), ColorPricingConfig = fe.ColorPricingConfig, QuantityPricingConfig = fe.QuantityPricingConfig, Options = fe.Options.Select(o => new FormElementOptionDetailDto { Id = o.Id, OptionLabel = o.OptionLabel.Select(l => new DynamicValueDto { Value = l.Value, LanguageTag = l.LanguageTag }).ToList(), OptionValue = o.OptionValue, OptionOrder = o.OptionOrder, PriceConfig = o.PriceConfig }).ToList() }).ToList() }).ToList(), Mockups = product.Mockups.Select(m => new MockupDetailDto { Id = m.Id, Name = m.Name, DesignWidth = m.DesignWidth, DesignHeight = m.DesignHeight, PrintWidth = m.PrintWidth, PrintHeight = m.PrintHeight, File = m.File, Thumbnail = m.Thumbnail }).ToList() }; } */ /* public async Task> GetAllProductsAsync(bool includeArchived = false) { var query = _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .AsQueryable(); if (!includeArchived) query = query.Where(p => !p.Archive); return await query.OrderByDescending(p => p.Timestamp).ToListAsync(); } */ public async Task> GetAllProductsAsync(bool includeArchived = false) { var query = _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .Include(p => p.Mockups) // ✅ include Mockups .ThenInclude(m => m.File) // optional: include File .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) // optional: include Thumbnail .AsQueryable(); if (!includeArchived) query = query.Where(p => !p.Archive); return await query .OrderByDescending(p => p.Timestamp) .ToListAsync(); } /* public async Task> GetAllProductsPgAsync( int page = 0, int size = 5, bool includeArchived = false, long? categoryId = null, decimal? lowPrice = null, decimal? highPrice = null, string? search = null) // Added search parameter { var query = _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .AsQueryable(); // 1. Filtering Logic if (!includeArchived) query = query.Where(p => !p.Archive); if (categoryId.HasValue) query = query.Where(p => p.CategoryId == categoryId.Value); if (lowPrice.HasValue) query = query.Where(p => p.BasePrice > lowPrice.Value); if (highPrice.HasValue) query = query.Where(p => p.BasePrice < highPrice.Value); // 2. Search Logic (Filtering by Names list) if (!string.IsNullOrWhiteSpace(search)) { string lowerSearch = search.ToLower(); query = query.Where(p => p.Names.Any(n => n.Value.ToLower().Contains(lowerSearch))); } // 3. Get Total Count before pagination int totalCount = await query.CountAsync(); // 4. Apply Pagination and Sorting var items = await query .OrderByDescending(p => p.Timestamp) .Skip(page * size) .Take(size) .ToListAsync(); // 5. Return the PagedResult wrapper return new PagedResult(items, totalCount, page, size); } */ public async Task> GetAllProductsPgAsync( int page = 0, int size = 5, bool includeArchived = false, long? categoryId = null, decimal? lowPrice = null, decimal? highPrice = null, string? search = null) { var query = _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Category) .Include(p => p.Image) .Include(p => p.Mockups) .ThenInclude(m => m.File) .Include(p => p.Mockups) .ThenInclude(m => m.Thumbnail) .AsQueryable(); // Filtering if (!includeArchived) query = query.Where(p => !p.Archive); if (categoryId.HasValue) query = query.Where(p => p.CategoryId == categoryId.Value); if (lowPrice.HasValue) query = query.Where(p => p.BasePrice > lowPrice.Value); if (highPrice.HasValue) query = query.Where(p => p.BasePrice < highPrice.Value); // Search by Names if (!string.IsNullOrWhiteSpace(search)) { string lowerSearch = search.ToLower(); query = query.Where(p => p.Names.Any(n => n.Value.ToLower().Contains(lowerSearch))); } // Total count int totalCount = await query.CountAsync(); // Apply pagination var items = await query .OrderByDescending(p => p.Timestamp) .Skip(page * size) .Take(size) .ToListAsync(); return new PagedResult(items, totalCount, page, size); } public async Task DeleteProductAsync(long id) { var product = await _context.CustomizableProducts.FindAsync(id); if (product == null) return false; _context.CustomizableProducts.Remove(product); await _context.SaveChangesAsync(); return true; } public async Task> GetProductsByCategoryAsync(long categoryId) { return await _context.CustomizableProducts .Include(p => p.Names) .Include(p => p.Descriptions) .Include(p => p.Image) .Where(p => p.CategoryId == categoryId && !p.Archive) .OrderByDescending(p => p.Timestamp) .ToListAsync(); } public async Task UpdateProductImageAsync(long productId, FileModel fileModel) { var product = await _context.CustomizableProducts .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { return null; } product.SetImage(fileModel); await _context.SaveChangesAsync(); return product; } // NEW METHODS FOR MOCKUPS public async Task AddMockupToProductAsync(long productId, FileModel fileModel, FileModel? thumbnail, string name, double dWidth, double dHeight, double pWidth, double pHeight) { var product = await _context.CustomizableProducts.FindAsync(productId); if (product == null) throw new Exception("Product not found"); var mockup = new Mockup { CustomizableProductId = productId, FileId = fileModel.Id, //File = fileModel, Name = name, DesignWidth = dWidth, DesignHeight = dHeight, PrintWidth = pWidth, PrintHeight = pHeight }; if (thumbnail != null) { mockup.ThumbnailFileId = thumbnail.Id; } _context.Mockups.Add(mockup); await _context.SaveChangesAsync(); return mockup; } public async Task UpdateMockupAsync(long mockupId, FileModel? fileModel, FileModel? thumbnail, string? name, double? dWidth, double? dHeight, double? pWidth, double? pHeight) { var mockup = await _context.Mockups.FindAsync(mockupId); if (mockup == null) return null; if (fileModel != null) { mockup.FileId = fileModel.Id; //mockup.File = fileModel; } if (thumbnail != null) { mockup.ThumbnailFileId = thumbnail.Id; } if (name != null) mockup.Name = name; if (dWidth.HasValue) mockup.DesignWidth = dWidth.Value; if (dHeight.HasValue) mockup.DesignHeight = dHeight.Value; if (pWidth.HasValue) mockup.PrintWidth = pWidth.Value; if (pHeight.HasValue) mockup.PrintHeight = pHeight.Value; await _context.SaveChangesAsync(); return mockup; } public async Task DeleteMockupAsync(long mockupId) { var mockup = await _context.Mockups .Include(m => m.File) .FirstOrDefaultAsync(m => m.Id == mockupId); if (mockup == null) return false; if (mockup.File != null) { _context.FileModels.Remove(mockup.File); } _context.Mockups.Remove(mockup); await _context.SaveChangesAsync(); return true; } public async Task Patch3DConfiguration(long id, Patch3DRequest request) { var product = await _context.CustomizableProducts .Include(p => p.TextureSlots) .FirstOrDefaultAsync(p => p.Id == id); if (product == null) return null; // Update basic flags and JSON configs if provided if (request.Has3DPreview.HasValue) product.Has3DPreview = request.Has3DPreview.Value; if (request.ThreeDConfig != null) product.ThreeDConfig = request.ThreeDConfig; if (request.UvMapImageId.HasValue) product.UvMapImageId = request.UvMapImageId; if (request.ThreeDModelFileId.HasValue) product.ThreeDModelFileId = request.ThreeDModelFileId; // Update TextureSlots if (request.TextureSlots != null) { foreach (var slotReq in request.TextureSlots) { if (slotReq.Id.HasValue && slotReq.Id > 0) { // Find existing slot in the list var existingSlot = product.TextureSlots.FirstOrDefault(s => s.Id == slotReq.Id); if (existingSlot != null) { if (slotReq.Name != null) existingSlot.Name = slotReq.Name; if (slotReq.TextureType.HasValue) existingSlot.TextureType = slotReq.TextureType.Value; if (slotReq.UvCoordinates != null) existingSlot.UvCoordinates = slotReq.UvCoordinates; if (slotReq.FileId.HasValue) existingSlot.FileId = slotReq.FileId; } } else { // Add new slot product.TextureSlots.Add(new TextureSlot { Name = slotReq.Name ?? "Default Slot", TextureType = slotReq.TextureType ?? TextureType.customizable, UvCoordinates = slotReq.UvCoordinates ?? new UvCoordinates(), FileId = slotReq.FileId }); } } } await _context.SaveChangesAsync(); return product; } public async Task GetProduct3DData(long id) { return await _context.CustomizableProducts .Where(p => p.Id == id) .Select(p => new { p.Id, p.Has3DPreview, p.ThreeDConfig, p.TextureSlots, p.UvMapImage, p.ThreeDModelFile }) .FirstOrDefaultAsync(); } public async Task UpdateTextureSlotFileAsync(long textureSlotId, FileModel? fileModel) { var slot = await _context.TextureSlots .FirstOrDefaultAsync(s => s.Id == textureSlotId); if (slot == null) throw new Exception("Texture slot not found"); if (fileModel != null) { slot.FileId = fileModel.Id; slot.File = fileModel; // Link the newly created FileModel } await _context.SaveChangesAsync(); return slot; } public async Task Update3DFilesAsync(long productId, FileModel? uvMapFile, FileModel? model3dFile) { var product = await _context.CustomizableProducts .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) throw new Exception("Product not found"); if (uvMapFile != null) { product.UvMapImageId = uvMapFile.Id; product.UvMapImage = uvMapFile; } if (model3dFile != null) { product.ThreeDModelFileId = model3dFile.Id; product.ThreeDModelFile = model3dFile; } await _context.SaveChangesAsync(); return product; } public async Task SyncBranchingScenarioAsync(long productId, SyncBranchingScenarioRequest request) { var product = await _context.CustomizableProducts .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options) .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) return false; // 1. Update Step-level connections foreach (var stepConn in request.StepConnections) { var step = product.Steps.FirstOrDefault(s => s.Id == stepConn.StepId); if (step != null) { step.NextStepId = stepConn.TargetStepId; } } // 2. Update Option-level connections (Branches) var allOptions = product.Steps.SelectMany(s => s.FormElements).SelectMany(fe => fe.Options); foreach (var optConn in request.OptionConnections) { var option = allOptions.FirstOrDefault(o => o.Id == optConn.OptionId); if (option != null) { option.NextStepId = optConn.TargetStepId; } } // 3. Enable the branching logic for the client side product.IsBranchingEnabled = true; await _context.SaveChangesAsync(); return true; } public async Task DisableBranchingAsync(long productId) { var product = await _context.CustomizableProducts .Include(p => p.Steps) .ThenInclude(s => s.FormElements) .ThenInclude(fe => fe.Options) .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) return false; // Reset the switch product.IsBranchingEnabled = false; // Optional: Clear all IDs to completely reset the database state foreach (var step in product.Steps) step.NextStepId = null; var allOptions = product.Steps.SelectMany(s => s.FormElements).SelectMany(fe => fe.Options); foreach (var opt in allOptions) opt.NextStepId = null; await _context.SaveChangesAsync(); return true; } }