diff --git a/README.md b/README.md index 2a1ff1b..3214ddc 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to version: '3.8' services: apiservice: - image: blazordevlab/cleanaspire-api:0.0.49 + image: blazordevlab/cleanaspire-api:0.0.50 environment: - ASPNETCORE_ENVIRONMENT=Development - AllowedHosts=* @@ -108,7 +108,7 @@ services: webfrontend: - image: blazordevlab/cleanaspire-clientapp:0.0.49 + image: blazordevlab/cleanaspire-clientapp:0.0.50 ports: - "8016:80" - "8017:443" diff --git a/src/CleanAspire.Api/CleanAspire.Api.csproj b/src/CleanAspire.Api/CleanAspire.Api.csproj index 83eb66c..187a1f4 100644 --- a/src/CleanAspire.Api/CleanAspire.Api.csproj +++ b/src/CleanAspire.Api/CleanAspire.Api.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs index 3160010..87698a6 100644 --- a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs +++ b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs @@ -70,7 +70,7 @@ public static async Task> ProjectToPaginatedDataAsync - + diff --git a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor index 585ca50..fefdd87 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor @@ -78,9 +78,14 @@ MudDialog.Close(DialogResult.Ok(true)); _saving = false; }, + invalid => + { + Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error); + _saving = false; + }, error => { - Snackbar.Add(L["Failed to create product."], Severity.Error); + Snackbar.Add(error.Message ?? L["Failed to create product."], Severity.Error); _saving = false; } ); diff --git a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor index 9bf3a03..c2dd9a2 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor @@ -102,17 +102,11 @@ } private async Task Save() { - var online = await OnlineStatusInterop.GetOnlineStatusAsync(); - if (!online) - { - Snackbar.Add(L["You are offline. Please check your internet connection."], Severity.Error); - return; - } editForm?.Validate(); if (success == true) { _saving = true; - var result = await ApiClientService.ExecuteAsync(() => ApiClient.Products.PutAsync(model)); + var result = await ProductServiceProxy.UpdateProductAsync(model); _saving = result.Match( ok => { @@ -121,12 +115,12 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error); return false; }, error => { - Snackbar.Add(error.Message, Severity.Error); + Snackbar.Add(error.Message ?? L["Failed to save."], Severity.Error); return false; } diff --git a/src/CleanAspire.ClientApp/Pages/Products/Index.razor b/src/CleanAspire.ClientApp/Pages/Products/Index.razor index 8c9a9a8..83b8b17 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Index.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Index.razor @@ -120,12 +120,6 @@ } private async Task Delete() { - var online = await OnlineStatusInterop.GetOnlineStatusAsync(); - if (!online) - { - Snackbar.Add(L["You are offline. Please check your internet connection."], Severity.Error); - return; - } await DialogServiceHelper.ShowConfirmationDialog("delete confirm", L["Are you sure you want to delete the selected items?"], async () => { if (_selectedItems.Any()) @@ -142,7 +136,7 @@ }, error => { - Snackbar.Add(L["Failed to delete selected items."], Severity.Error); + Snackbar.Add(error.Message??L["Failed to delete selected items."], Severity.Error); }); } diff --git a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs index deb43fc..25b9ded 100644 --- a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs @@ -16,6 +16,8 @@ namespace CleanAspire.ClientApp.Services.Proxies; public class ProductServiceProxy { private const string OFFLINECREATECOMMANDCACHEKEY = "OfflineCreateCommand:Product"; + private const string OFFLINEUPDATECOMMANDCACHEKEY = "OfflineUpdateCommand:Product"; + private const string OFFLINEDELETECOMMANDCACHEKEY = "OfflineDeleteCommand:Product"; private readonly NavigationManager _navigationManager; private readonly WebpushrService _webpushrService; private readonly ApiClient _apiClient; @@ -120,7 +122,7 @@ public async Task> GetProductByIdAsync(s } } - public async Task> CreateProductAsync(CreateProductCommand command) + public async Task> CreateProductAsync(CreateProductCommand command) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); if (isOnline) @@ -134,12 +136,24 @@ public async Task> CreateProductAsync(CreateProd return response; } + catch (HttpValidationProblemDetails ex) + { + return new ApiClientValidationError(ex.Detail, ex); + } + catch (ProblemDetails ex) + { + return new ApiClientError(ex.Detail, ex); + } catch (ApiException ex) { - return ex; + return new ApiClientError(ex.Message, ex); + } + catch (Exception ex) + { + return new ApiClientError(ex.Message, ex); } } - else + else if(_offlineModeState.Enabled) { var cachedCommands = await _indexedDbCache.GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY) ?? new List(); @@ -172,9 +186,80 @@ public async Task> CreateProductAsync(CreateProd } return productDto; } + return new ApiClientError("Offline mode is disabled. Please enable offline mode to create products in offline mode.", new Exception("Offline mode is disabled.")); } - - public async Task> DeleteProductsAsync(List productIds) + public async Task> UpdateProductAsync(UpdateProductCommand command) + { + var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); + if (isOnline) + { + try + { + var response = await _apiClient.Products.PutAsync(command); + return true; + } + catch (HttpValidationProblemDetails ex) + { + return new ApiClientValidationError(ex.Detail, ex); + } + catch (ProblemDetails ex) + { + return new ApiClientError(ex.Detail, ex); + } + catch (ApiException ex) + { + return new ApiClientError(ex.Message, ex); + } + catch (Exception ex) + { + return new ApiClientError(ex.Message, ex); + } + } + else if (_offlineModeState.Enabled) + { + var cachedCommands = await _indexedDbCache.GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY) + ?? new List(); + cachedCommands.Add(command); + await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY, cachedCommands, new[] { "product_commands" }); + var productDto = new ProductDto() + { + Id = command.Id, + Category = command.Category, + Currency = command.Currency, + Description = command.Description, + Name = command.Name, + Price = command.Price, + Sku = command.Sku, + Uom = command.Uom + }; + var productCacheKey = GenerateProductCacheKey(productDto.Id); + await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, productCacheKey, productDto, new[] { "product" }); + var cachedPaginatedProducts = await _indexedDbCache.GetDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination" }); + if (cachedPaginatedProducts != null && cachedPaginatedProducts.Any()) + { + foreach (var dic in cachedPaginatedProducts) + { + var key = dic.Key; + var paginatedProducts = dic.Value; + var item = paginatedProducts.Items.FirstOrDefault(x => x.Id == productDto.Id); + if (item != null) + { + item.Category = productDto.Category; + item.Currency = productDto.Currency; + item.Description = productDto.Description; + item.Name = productDto.Name; + item.Price = productDto.Price; + item.Sku = productDto.Sku; + item.Uom = productDto.Uom; + } + await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, key, paginatedProducts, new[] { "products_pagination" }); + } + } + return true; + } + return new ApiClientError("Offline mode is disabled. Please enable offline mode to update products in offline mode.", new Exception("Offline mode is disabled.")); + } + public async Task> DeleteProductsAsync(List productIds) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); if (isOnline) @@ -182,48 +267,155 @@ public async Task> DeleteProductsAsync(List pro try { await _apiClient.Products.DeleteAsync(new DeleteProductCommand() { Ids = productIds }); - await _indexedDbCache.DeleteDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination","product" }); + await _indexedDbCache.DeleteDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination", "product" }); return true; } + catch (ProblemDetails ex) + { + return new ApiClientError(ex.Detail, ex); + } catch (ApiException ex) { - return ex; + return new ApiClientError(ex.Message, ex); } } - return true; + else if (_offlineModeState.Enabled) + { + var cachedDeleteCommands = await _indexedDbCache + .GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINEDELETECOMMANDCACHEKEY) + ?? new List(); + + cachedDeleteCommands.Add(new DeleteProductCommand() { Ids = productIds }); + await _indexedDbCache.SaveDataAsync( + IndexedDbCache.DATABASENAME, + OFFLINEDELETECOMMANDCACHEKEY, + cachedDeleteCommands, + new[] { "product_commands" } + ); + + foreach (var productId in productIds) + { + var productCacheKey = GenerateProductCacheKey(productId); + await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, productCacheKey); + } + var cachedPaginatedProducts = await _indexedDbCache + .GetDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination" }); + + if (cachedPaginatedProducts != null && cachedPaginatedProducts.Any()) + { + foreach (var dic in cachedPaginatedProducts) + { + var key = dic.Key; + var paginatedProducts = dic.Value; + paginatedProducts.Items = paginatedProducts.Items + .Where(product => !productIds.Contains(product.Id)) + .ToList(); + + paginatedProducts.TotalItems = paginatedProducts.Items.Count; + await _indexedDbCache.SaveDataAsync( + IndexedDbCache.DATABASENAME, + key, + paginatedProducts, + new[] { "products_pagination" } + ); + } + } + return true; + } + return new ApiClientError("Offline mode is disabled. Please enable offline mode to delete products in offline mode.", new Exception("Offline mode is disabled.")); } public async Task SyncOfflineCachedDataAsync() { var cachedCreateProductCommands = await _indexedDbCache.GetDataAsync>( IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY); - if (cachedCreateProductCommands != null && cachedCreateProductCommands.Any()) + var cachedUpdateProductCommands = await _indexedDbCache.GetDataAsync>( + IndexedDbCache.DATABASENAME, + OFFLINEUPDATECOMMANDCACHEKEY); + var cachedDeleteProductCommands = await _indexedDbCache.GetDataAsync>( + IndexedDbCache.DATABASENAME, + OFFLINEDELETECOMMANDCACHEKEY); + var totalCount = (cachedCreateProductCommands?.Count ?? 0) + (cachedUpdateProductCommands?.Count ?? 0) + (cachedDeleteProductCommands?.Count ?? 0); + var processedCount = 0; + if (totalCount > 0) { - var count = cachedCreateProductCommands.Count; - var processedCount = 0; - _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Starting sync: 0/{count} ...", count, processedCount); + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Starting sync: 0/{totalCount} ...", totalCount, processedCount); await Task.Delay(500); - foreach (var command in cachedCreateProductCommands) + + if (cachedCreateProductCommands != null && cachedCreateProductCommands.Any()) { - var result = await CreateProductAsync(command); - result.Switch( - productDto => - { - processedCount++; - _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{count} Success.", count, processedCount); - }, - apiException => - { - processedCount++; - _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{count} Failed ({apiException.Message}).", count, processedCount); - }); - await Task.Delay(500); - } - _offlineSyncService.SetSyncStatus(SyncStatus.Completed, $"Sync completed: {processedCount}/{count} processed.", count, processedCount); - await Task.Delay(1200); - await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY); - _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0); + foreach (var command in cachedCreateProductCommands) + { + var result = await CreateProductAsync(command); + result.Switch( + productDto => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount); + }, + invalid => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({invalid.Message}).", totalCount, processedCount); + }, + error => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount); + }); + await Task.Delay(500); + } + await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY); + } + if (cachedUpdateProductCommands != null && cachedUpdateProductCommands.Any()) + { + foreach (var command in cachedUpdateProductCommands) + { + var result = await UpdateProductAsync(command); + result.Switch( + productDto => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount); + }, + invalid => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({invalid.Message}).", totalCount, processedCount); + }, + error => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount); + }); + await Task.Delay(500); + } + await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY); + } + if (cachedDeleteProductCommands != null && cachedDeleteProductCommands.Any()) + { + foreach (var command in cachedDeleteProductCommands) + { + var result = await DeleteProductsAsync(command.Ids); + result.Switch( + ok => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount); + }, + error => + { + processedCount++; + _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount); + }); + await Task.Delay(500); + } + await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINEDELETECOMMANDCACHEKEY); + } } + _offlineSyncService.SetSyncStatus(SyncStatus.Completed, $"Sync completed: {processedCount}/{totalCount} processed.", totalCount, processedCount); + await Task.Delay(1200); + _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0); } private string GeneratePaginationCacheKey(ProductsWithPaginationQuery query) diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json index c32edd8..fa07868 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Progressive Web Application", - "Version": "v0.0.49", + "Version": "v0.0.50", "ServiceBaseUrl": "https://localhost:7341" } } diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json index 1b52b80..89d5b6b 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Progressive Web Application", - "Version": "v0.0.49", + "Version": "v0.0.50", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } }