refactor(ProductCategoriesPage) , refactor(ICrudApiRest)

- NEW VERSION OF PRODUCT CATEGORIES PAGE
- Change products categories page style and ux
- Change icrud api rest and get guid in instead of dto or model
subProduct
Amir Hossein Khademi 2024-06-07 23:03:55 +03:30
parent ea84220cac
commit 55931702a9
8 changed files with 314 additions and 110 deletions

View File

@ -63,12 +63,12 @@
<MudGrid Spacing="0">
<MudItem md="3" lg="2">
<MudItem xs="0" md="3" lg="2">
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<SideBarUi/>
</MudHidden>
</MudItem>
<MudItem sm="12" md="9" lg="10">
<MudItem xs="12" sm="12" md="9" lg="10">
<div>
@Body

View File

@ -5,43 +5,142 @@
@inject ISnackbar Snackbar
@inject IUserUtility UserUtility
@* <style>
.mud-input-label {
background-color: var(--mud-palette-background-grey);
}
</style> *@
<MudPaper class="bg-[--mud-palette-background-grey] h-screen w-full p-3 md:p-8">
<MudStack class="w-full p-8 h-screen bg-[--mud-palette-background-grey]">
<MudGrid>
<MudItem xs="12">
<MudStack Row="true" class="overflow-x-auto pb-3">
<MudStack Spacing="1" class="min-w-[340px]">
<MudStack Row="true" class="mb-5">
<MudText Typo="Typo.h4">دسته بندی ها</MudText>
@* <MudChip Color="Color.Info" Variant="Variant.Outlined">124 عدد</MudChip> *@
<MudSpacer/>
<MudButton Variant="Variant.Filled"
DisableElevation="true"
StartIcon="@Icons.Material.Outlined.Add"
Color="Color.Secondary"
OnClick="ViewModel.AddProductCategoryClicked"
class="my-auto">افزودن دسته بندی</MudButton>
<MudText Typo="Typo.h6" class="my-auto"><b>دسته های اصلی</b></MudText>
<MudSpacer />
<MudIconButton Icon="@Icons.Material.Filled.Add"
Size="@Size.Small"
Variant="@Variant.Outlined"
OnClick="()=>ViewModel.ChangeOriginalCategoryVisibility()"
Color="@Color.Secondary" />
</MudStack>
<MudPaper class="!max-h-[80vh] overflow-auto">
@if (ViewModel.OriginalCategoryVisibility)
{
<MudTextField class="-mt-2 mb-3" T="string" Label="نام دسته بندی" Variant="Variant.Text" Immediate="false" ValueChanged="name => ViewModel.AddFastProductCategory(name)"></MudTextField>
}
@foreach (var item in ViewModel.PageDto)
{
<MudCard class="@(item.IsSelected ? "cursor-pointer border-dashed border-2 border-blue-400 " : "cursor-pointer" )" @onclick="()=>ViewModel.SelectCategory(item)">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">@item.Name</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudStack Row="true" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="@Size.Small"
class="mt-1"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.EditProductCategoryClicked(item)"
Color="@Color.Info" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="@Size.Small"
class="mt-1"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.DeleteProductCategoryAsync(item.Id)"
Color="@Color.Error" />
</MudStack>
</CardHeaderActions>
</MudCardHeader>
</MudCard>
}
</MudStack>
@foreach (var item in ViewModel.SelectedCategories)
{
<MudStack Spacing="1" class="min-w-[340px]">
<MudStack Row="true" class="mb-5">
<MudText Typo="Typo.h6" class="my-auto"><b>@item.Name</b></MudText>
<MudSpacer />
<MudIconButton Icon="@Icons.Material.Filled.Add"
Size="@Size.Small"
Variant="@Variant.Outlined"
OnClick="()=>item.AddNewCatVisibility = !item.AddNewCatVisibility"
Color="@Color.Secondary" />
</MudStack>
@if (item.AddNewCatVisibility)
{
<MudTextField class="-mt-2 mb-3 flex-none" T="string" Label="نام دسته بندی" Variant="Variant.Text" Immediate="false" ValueChanged="name => ViewModel.AddFastProductCategory(name,item.Id)"></MudTextField>
}
@foreach (var child in item.Children)
{
<MudCard class="@(child.IsSelected ? "cursor-pointer border-dashed border-2 border-blue-400 " : "cursor-pointer" )" @onclick="()=>ViewModel.SelectCategory(child)">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">@child.Name</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudStack Row="true" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="@Size.Small"
class="mt-1"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.EditProductCategoryClicked(child)"
Color="@Color.Info" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="@Size.Small"
class="mt-1"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.DeleteProductCategoryAsync(child.Id)"
Color="@Color.Error" />
</MudStack>
</CardHeaderActions>
</MudCardHeader>
</MudCard>
}
</MudStack>
}
</MudStack>
</MudPaper>
@code
{
public CategoriesPageViewModel ViewModel { get; set; }
protected override async Task OnInitializedAsync()
{
ViewModel = new CategoriesPageViewModel(NavigationManager, Snackbar, UserUtility, RestWrapper, DialogService);
await ViewModel.InitializeAsync();
await base.OnInitializedAsync();
}
}
@* <MudPaper class="!max-h-[80vh] overflow-auto">
<MudDataGrid FixedFooter="true" FixedHeader="true" Striped="true"
T="ProductCategorySDto" Items="@ViewModel.PageDto" CurrentPage="@ViewModel.CurrentPage"
T="ProductCategorySDto" Items="@ViewModel.PageDto" CurrentPage="@ViewModel.CurrentPage"
RowsPerPage="15" Filterable="false" Loading="@ViewModel.IsProcessing"
SortMode="@SortMode.None" Groupable="false">
<ToolBarContent>
<MudTextField T="string" Placeholder="جست جو بر اساس نام" Adornment="Adornment.Start" Immediate="true"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" class="my-auto"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" class="my-auto"
@bind-Value="@ViewModel.Search"
OnAdornmentClick="@ViewModel.SearchAsync"></MudTextField>
</ToolBarContent>
<Columns>
<HierarchyColumn T="ProductCategorySDto" ButtonDisabledFunc="@(x => x.Description.IsNullOrEmpty())" />
<PropertyColumn Title="نام دسته" Resizable="false" Property="arg => arg.Name"/>
@* <TemplateColumn Title="توضیحاتــ" Resizable="false" T="ProductCategorySDto">
<TemplateColumn Title="توضیحاتــ" Resizable="false" T="ProductCategorySDto">
<CellTemplate>
<p class="line-clamp-1">@context.Item.Description</p>
</CellTemplate>
</TemplateColumn> *@
</TemplateColumn>
<TemplateColumn Resizable="false" Title="دسته اصلی" T="ProductCategorySDto">
<CellTemplate>
@if (@context.Item.IsMain)
@ -74,7 +173,7 @@
</CellTemplate>
</TemplateColumn>
</Columns>
<ChildRowContent>
<MudCard>
<MudCardHeader>
@ -91,24 +190,9 @@
<MudStack Row="true" class="w-full">
<MudPagination Rectangular="true" Variant="Variant.Filled" Count="@ViewModel.PageCount"
SelectedChanged="@ViewModel.ChangePageAsync" class="my-4 mx-auto"/>
SelectedChanged="@ViewModel.ChangePageAsync" class="mx-auto my-4"/>
</MudStack>
</PagerContent>
</MudDataGrid>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
@code
{
public CategoriesPageViewModel ViewModel { get; set; }
protected override async Task OnInitializedAsync()
{
ViewModel = new CategoriesPageViewModel(NavigationManager, Snackbar, UserUtility, RestWrapper, DialogService);
await ViewModel.InitializeAsync();
await base.OnInitializedAsync();
}
}
</MudPaper> *@

View File

@ -9,9 +9,7 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
private readonly IRestWrapper _restWrapper;
public string Search = string.Empty;
public int CurrentPage = 0;
public int PageCount = 1;
public CategoriesPageViewModel(NavigationManager navigationManager, ISnackbar snackbar, IUserUtility userUtility, IRestWrapper restWrapper, IDialogService dialogService) : base(userUtility)
{
_navigationManager = navigationManager;
@ -26,13 +24,10 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
try
{
IsProcessing = true;
CurrentPage = 0;
PageDto.Clear();
var dto = await _restWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController)
.ReadAll(CurrentPage);
dto.ForEach(d=>PageDto.Add(d));
if (PageDto.Count == 15)
PageCount = 2;
var dto = await _restWrapper.ProductCategoryRestApi.ReadAll(true);
dto.ForEach(d => PageDto.Add(d));
}
catch (ApiException ex)
{
@ -51,38 +46,6 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
await base.InitializeAsync();
}
public async Task ChangePageAsync(int page)
{
CurrentPage = page-1;
if (CurrentPage > PageCount-2)
{
try
{
IsProcessing = true;
var dto = await _restWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController)
.ReadAll(CurrentPage);
dto.ForEach(d => PageDto.Add(d));
if(PageDto.Count % 15==0)
PageCount = CurrentPage + 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}
public async Task AddProductCategoryClicked()
{
@ -99,7 +62,7 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
{
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
var parameters = new DialogParameters<ProductCategoryActionDialogBox>();
parameters.Add(x=>x.Category,category);
parameters.Add(x => x.Category, category);
var dialogResult = await _dialogService.ShowAsync<ProductCategoryActionDialogBox>($"ویرایش دسته {category.Name}", parameters, maxWidth);
var result = await dialogResult.Result;
if (!result.Canceled && result.Data is bool and true)
@ -150,13 +113,9 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
if (Search.IsNullOrEmpty())
throw new AppException("دسته بندی برای جست جو وارد نشده است");
IsProcessing = true;
CurrentPage = 0;
PageCount = 1;
PageDto.Clear();
var dto = await _restWrapper.ProductCategoryRestApi.ReadAll(Search);
dto.ForEach(d => PageDto.Add(d));
if (PageDto.Count == 15)
PageCount = 2;
}
catch (ApiException ex)
{
@ -173,4 +132,90 @@ public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<Produc
IsProcessing = false;
}
}
public List<ProductCategorySDto> SelectedCategories = new();
public void SelectCategory(ProductCategorySDto category)
{
var selected = SelectedCategories.FirstOrDefault(c => c.Id == category.Id);
if (selected != null)
{
UnSelectCategory(selected);
return;
}
var parent = FindParent(category.ParentId, PageDto.ToList());
if (parent != null)
category.Index = parent.Index + 1;
category.IsSelected = true;
var currentSelectedIndex = SelectedCategories.FirstOrDefault(c => c.Index == category.Index);
if (currentSelectedIndex != null)
UnSelectCategory(currentSelectedIndex);
SelectedCategories.Add(category);
}
public bool OriginalCategoryVisibility = false;
public bool ChangeOriginalCategoryVisibility() => OriginalCategoryVisibility = !OriginalCategoryVisibility;
public void AddFastProductCategory(string categoryName, Guid? parentId = null)
{
if(categoryName.IsNullOrEmpty())
return;
ProductCategorySDto category = new ProductCategorySDto { Name = categoryName, IsMain = true};
if (parentId != null)
{
category.IsMain = false;
category.ParentId = parentId.Value;
}
var command = category.Adapt<CreateProductCategoryCommand>() with{Files = new()};
Task.Run(async () =>
{
var token = await _userUtility.GetBearerTokenAsync();
if (token == null)
return;
var response = await _restWrapper.CrudApiRest<ProductCategory, Guid>(Address.ProductCategoryController)
.Create(command, token);
category.Id = response;
});
if (parentId == null)
{
category.IsMain = true;
PageDto.Add(category);
}
else
{
var parent = FindParent(parentId.Value, PageDto.ToList());
if (parent != null)
{
category.Index = parent.Index + 1;
parent.Children.Add(category);
}
}
categoryName = string.Empty;
}
private void UnSelectCategory(ProductCategorySDto category)
{
category.IsSelected = false;
foreach (var selected in SelectedCategories.Where(c => c.ParentId == category.Id).ToList())
UnSelectCategory(selected);
SelectedCategories.Remove(category);
}
private ProductCategorySDto? FindParent(Guid parentId, List<ProductCategorySDto> categories)
{
var parent = categories.FirstOrDefault(c => c.Id == parentId);
if (parent != null)
return parent;
foreach (var category in categories)
{
var founded = FindParent(parentId, category.Children);
if (founded != null)
return founded;
}
return null;
}
}

View File

@ -1,9 +1,9 @@
namespace Netina.AdminPanel.PWA.Services.RestServices;
public interface ICrudApiRest<T, in TKey> where T : class
public interface ICrudApiRest<T, TKey> where T : class
{
[Post("")]
Task Create<TCreateCommand>([Body] TCreateCommand payload, [Header("Authorization")] string authorization);
Task<TKey> Create<TCreateCommand>([Body] TCreateCommand payload, [Header("Authorization")] string authorization);
[Get("")]
Task<List<T>> ReadAll([Query] int page,[Header("Authorization")] string authorization);

View File

@ -8,6 +8,8 @@ public interface IProductCategoryRestApi
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] int page);
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] bool sortByMain);
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] int page,[Query]string categoryName);
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] string categoryName);

View File

@ -8,6 +8,14 @@ module.exports = {
"./node_modules/flowbite/**/*.js"
],
theme: {
screens: {
'xs': '380px',
'sm': '600px',
'md': '960px',
'lg': '1280px',
'xl': '1920px',
'2xl': '2560px',
},
fontSize: {
xs: '.75rem',
sm: '.875rem',

View File

@ -977,22 +977,22 @@ input:checked + .toggle-bg {
.container {
width: 100%;
}
@media (min-width: 640px) {
@media (min-width: 380px) {
.container {
max-width: 640px;
max-width: 380px;
}
}
@media (min-width: 768px) {
@media (min-width: 600px) {
.container {
max-width: 768px;
max-width: 600px;
}
}
@media (min-width: 1024px) {
@media (min-width: 960px) {
.container {
max-width: 1024px;
max-width: 960px;
}
}
@media (min-width: 1280px) {
@ -1001,10 +1001,16 @@ input:checked + .toggle-bg {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
@media (min-width: 1920px) {
.container {
max-width: 1536px;
max-width: 1920px;
}
}
@media (min-width: 2560px) {
.container {
max-width: 2560px;
}
}
.visible {
@ -1150,6 +1156,9 @@ input:checked + .toggle-bg {
.-mt-1 {
margin-top: -0.25rem;
}
.-mt-2 {
margin-top: -0.5rem;
}
.-mt-3 {
margin-top: -0.75rem;
}
@ -1353,6 +1362,9 @@ input:checked + .toggle-bg {
.w-screen {
width: 100vw;
}
.min-w-\[340px\] {
min-width: 340px;
}
.flex-1 {
flex: 1 1 0%;
}
@ -1466,6 +1478,9 @@ input:checked + .toggle-bg {
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.overflow-x-hidden {
overflow-x: hidden;
}
@ -1565,6 +1580,10 @@ input:checked + .toggle-bg {
--tw-border-opacity: 1;
border-color: rgb(167 139 250 / var(--tw-border-opacity));
}
.border-yellow-400 {
--tw-border-opacity: 1;
border-color: rgb(227 160 8 / var(--tw-border-opacity));
}
.bg-\[\#000000\] {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@ -1704,6 +1723,9 @@ input:checked + .toggle-bg {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.pt-2 {
padding-top: 0.5rem;
}
@ -2218,7 +2240,7 @@ code {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
@media (min-width: 640px) {
@media (min-width: 600px) {
.sm\:mr-3 {
margin-right: 0.75rem;
@ -2238,7 +2260,7 @@ code {
}
}
@media (min-width: 768px) {
@media (min-width: 960px) {
.md\:visible {
visibility: visible;
@ -2277,13 +2299,17 @@ code {
padding: 1.25rem;
}
.md\:p-8 {
padding: 2rem;
}
.md\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1024px) {
@media (min-width: 1280px) {
.lg\:h-60 {
height: 15rem;

View File

@ -1031,21 +1031,21 @@ input:checked + .toggle-bg {
width: 100%;
}
@media (min-width: 640px) {
@media (min-width: 380px) {
.container {
max-width: 640px;
max-width: 380px;
}
}
@media (min-width: 768px) {
@media (min-width: 600px) {
.container {
max-width: 768px;
max-width: 600px;
}
}
@media (min-width: 1024px) {
@media (min-width: 960px) {
.container {
max-width: 1024px;
max-width: 960px;
}
}
@ -1055,9 +1055,15 @@ input:checked + .toggle-bg {
}
}
@media (min-width: 1536px) {
@media (min-width: 1920px) {
.container {
max-width: 1536px;
max-width: 1920px;
}
}
@media (min-width: 2560px) {
.container {
max-width: 2560px;
}
}
@ -1247,6 +1253,10 @@ input:checked + .toggle-bg {
margin-top: -0.25rem;
}
.-mt-2 {
margin-top: -0.5rem;
}
.-mt-3 {
margin-top: -0.75rem;
}
@ -1451,6 +1461,14 @@ input:checked + .toggle-bg {
min-height: 33rem;
}
.min-h-full {
min-height: 100%;
}
.\!min-h-full {
min-height: 100% !important;
}
.w-1\/2 {
width: 50%;
}
@ -1516,6 +1534,10 @@ input:checked + .toggle-bg {
width: 100vw;
}
.min-w-\[340px\] {
min-width: 340px;
}
.flex-1 {
flex: 1 1 0%;
}
@ -1662,6 +1684,10 @@ input:checked + .toggle-bg {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.overflow-x-hidden {
overflow-x: hidden;
}
@ -1790,6 +1816,11 @@ input:checked + .toggle-bg {
border-color: rgb(167 139 250 / var(--tw-border-opacity));
}
.border-yellow-400 {
--tw-border-opacity: 1;
border-color: rgb(227 160 8 / var(--tw-border-opacity));
}
.bg-\[\#000000\] {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@ -1979,6 +2010,10 @@ input:checked + .toggle-bg {
padding-top: 2rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.text-center {
text-align: center;
}
@ -2557,7 +2592,7 @@ code {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
@media (min-width: 640px) {
@media (min-width: 600px) {
.sm\:mr-3 {
margin-right: 0.75rem;
}
@ -2576,7 +2611,7 @@ code {
}
}
@media (min-width: 768px) {
@media (min-width: 960px) {
.md\:visible {
visibility: visible;
}
@ -2614,13 +2649,17 @@ code {
padding: 1.25rem;
}
.md\:p-8 {
padding: 2rem;
}
.md\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1024px) {
@media (min-width: 1280px) {
.lg\:h-60 {
height: 15rem;
}