This commit is contained in:
King
2025-09-28 18:56:17 +03:30
parent 50e864bd61
commit 148363d468
75 changed files with 1817 additions and 0 deletions

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35027.167
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BackOffice", "BackOffice\BackOffice.csproj", "{CD6DF182-2FBE-4C17-8B2C-CC25F488A8C0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CD6DF182-2FBE-4C17-8B2C-CC25F488A8C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD6DF182-2FBE-4C17-8B2C-CC25F488A8C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD6DF182-2FBE-4C17-8B2C-CC25F488A8C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD6DF182-2FBE-4C17-8B2C-CC25F488A8C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {43173B65-5703-4FEB-B360-0D88A0F9CA16}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,30 @@
@using BackOffice.Shared
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@{
Task.Delay(0).ContinueWith(p =>
{
try
{
Navigation.NavigateTo(RouteConstance.Login);
}
catch
{
}
});
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<PageTitle>یافت نشد</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">آدرس مورد نظر یافت نشد</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Common\NewFolder\**" />
<Content Remove="Common\NewFolder\**" />
<EmbeddedResource Remove="Common\NewFolder\**" />
<None Remove="Common\NewFolder\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Foursat.BackOffice.BFF.Otp.Protobuf" Version="0.0.111" />
<PackageReference Include="FourSat.BackOffice.BFF.Package.Protobuf" Version="0.0.111" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Grpc.Core" Version="2.46.6" />
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.65.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.18" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.20" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.18" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="7.16.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
<PackageReference Include="Tizzani.MudBlazor.HtmlEditor" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<MudDateRangePicker @ref="_picker" Label="@Label" AutoClose="false" Culture="@GetPersianCulture()" TitleDateFormat="dddd, dd MMMM" @bind-DateRange="_dateRange">
<PickerActions>
<MudButton Class="mr-auto align-self-start" OnClick="OnClickClear">پاک کردن</MudButton>
<MudButton OnClick="@(() => _picker.CloseAsync(false))">لغو</MudButton>
<MudButton Color="Color.Primary" OnClick="OnClickOK">تایید</MudButton>
</PickerActions>
</MudDateRangePicker>

View File

@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Globalization;
using System.Reflection;
namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker
{
private DateRange _dateRange = new();
private MudDateRangePicker _picker;
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
[Parameter] public DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { get; set; }
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (DefaultStart.HasValue)
_dateRange.Start = DefaultStart.Value.Date;
if (DefaultEnd.HasValue)
_dateRange.End = DefaultEnd.Value.Date;
}
public CultureInfo GetPersianCulture()
{
var culture = new CultureInfo("fa-IR");
DateTimeFormatInfo formatInfo = culture.DateTimeFormat;
formatInfo.AbbreviatedDayNames = new[] { "ی", "د", "س", "چ", "پ", "ج", "ش" };
formatInfo.DayNames = new[] { "یکشنبه", "دوشنبه", "سه شنبه", "چهار شنبه", "پنجشنبه", "جمعه", "شنبه" };
var monthNames = new[]
{
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن",
"اسفند",
"",
};
formatInfo.AbbreviatedMonthNames =
formatInfo.MonthNames =
formatInfo.MonthGenitiveNames = formatInfo.AbbreviatedMonthGenitiveNames = monthNames;
formatInfo.AMDesignator = "ق.ظ";
formatInfo.PMDesignator = "ب.ظ";
formatInfo.ShortDatePattern = "yyyy/MM/dd";
formatInfo.LongDatePattern = "dddd, dd MMMM,yyyy";
formatInfo.FirstDayOfWeek = DayOfWeek.Saturday;
Calendar cal = new PersianCalendar();
FieldInfo fieldInfo = culture.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance);
fieldInfo?.SetValue(culture, cal);
FieldInfo info = formatInfo.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance);
info?.SetValue(formatInfo, cal);
culture.NumberFormat.NumberDecimalSeparator = "/";
culture.NumberFormat.DigitSubstitution = DigitShapes.NativeNational;
culture.NumberFormat.NumberNegativePattern = 0;
return culture;
}
private async Task OnClickOK()
{
_picker.CloseAsync();
await OnChanged.InvokeAsync(_picker.DateRange);
}
private async Task OnClickClear()
{
_picker.CloseAsync();
await OnChanged.InvokeAsync(_picker.DateRange);
}
}

View File

@@ -0,0 +1,12 @@
@using MudBlazor
@inherits MudImage
@if (string.IsNullOrWhiteSpace(src))
{
<MudSkeleton Class="@Class" Style="@Style" SkeletonType="SkeletonType.Rectangle" Height="@(Height != null ? $"{Height}px" : "100%")" Width="@(Width != null ? $"{Width}px" : "100%")" />
}
else
{
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@(Style + ";cursor:pointer;")" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace BackOffice.Common.BaseComponents
{
public partial class Image
{
private string src = string.Empty;
[Parameter] public EventCallback OnClick { get; set; }
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (!string.IsNullOrWhiteSpace(Src))
{
src = Src.Contains("data:") ? Src : "https://dl.afrino.co" + Src;
}
}
//protected override async Task OnAfterRenderAsync(bool firstRender)
//{
// await base.OnAfterRenderAsync(firstRender);
// if (firstRender)
// {
// if (!string.IsNullOrWhiteSpace(Src))
// {
// src = await FileManagement.GetFileBase64(Src);
// StateHasChanged();
// }
// }
//}
}
}

View File

@@ -0,0 +1,93 @@

using BackOffice.BFF.Package.Protobuf.Protos.Package;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
using Google.Protobuf.Reflection;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using MudBlazor.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static MudBlazor.Colors;
namespace Microsoft.Extensions.DependencyInjection;
public static class ConfigureServices
{
public static IServiceCollection AddCommonServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddBlazoredLocalStorageAsSingleton(config =>
{
config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
config.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
config.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
config.JsonSerializerOptions.WriteIndented = false;
});
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
services.AddSingleton<ITokenProvider, AppTokenProvider>();
services.AddSingleton<ITokenHandler, TokenHandler>();
services.AddMudServices();
services.AddGrpcServices(configuration);
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration) //
{
var baseUri = configuration["GwUrl"];
Console.WriteLine();
Console.WriteLine(baseUri);
var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
httpClient.Timeout = TimeSpan.FromMinutes(10); // TODO Check Timeout
var serviceProvider = services.BuildServiceProvider();
var channel = CreateAuthenticatedChannel(baseUri, httpClient, serviceProvider);
services.AddTransient(sp => new PackageContract.PackageContractClient(channel));
//services.AddTransient(sp => new OtpContract.OtpContractClient(channel));
return services;
}
private static CallInvoker CreateAuthenticatedChannel(string address, HttpClient httpClient, IServiceProvider serviceProvider)
{
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
// var accessToken = await provider.RequestAccessToken();
// accessToken.TryGetToken(out var token);
var token = await provider.GetTokenAsync();
if (!string.IsNullOrEmpty(token))
{
// Console.WriteLine($"Authorization Bearer {token.Value}");
metadata.Add("Authorization", $"Bearer {token}");
}
await Task.CompletedTask;
});
// SslCredentials is used here because this channel is using TLS.
// CallCredentials can't be used with ChannelCredentials.Insecure on non-TLS channels.
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
UnsafeUseInsecureChannelCallCredentials = true,
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
HttpClient = httpClient,
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
});
var invoker = channel.Intercept(new ErrorHandlerInterceptor());
return invoker;
}
}

View File

@@ -0,0 +1,14 @@
namespace BackOffice.Common.Models;
public class WorkHoursDto
{
public string Title { get; set; }
public string Name { get; set; }
public bool Closed { get; set; }
public List<Hours> Hours { get; set; }
}
public class Hours
{
public string OpenTime { get; set; }
public string CloseTime { get; set; }
}

View File

@@ -0,0 +1,54 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
namespace BackOffice.Common.Utilities;
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(ILocalStorageService localStorage)
{
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var savedToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(savedToken);
var AuthenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "jwt")));
return AuthenticationState;
}
catch (Exception ex)
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}

View File

@@ -0,0 +1,27 @@
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
namespace BackOffice.Common.Utilities;
public class AppTokenProvider : ITokenProvider
{
private readonly ILocalStorageService _localStorage;
private string _token;
public AppTokenProvider(ILocalStorageService localStorage)
{
_localStorage = localStorage;
}
public async Task<string> GetTokenAsync()
{
if (_token == null)
{
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
if (!string.IsNullOrEmpty(authorizationToken))
_token = authorizationToken.ToString().Replace("Bearer ", "");
}
return _token;
}
}

View File

@@ -0,0 +1,17 @@
using Google.Protobuf.Reflection;
using System.ComponentModel.DataAnnotations;
namespace BackOffice.Common.Utilities;
public class Enums
{
public enum PrivacyTypeEnum
{
[Display(Name = "شخصی")]
Individual,
[Display(Name = "گروهی")]
Group,
[Display(Name = "همه")]
All
}
}

View File

@@ -0,0 +1,35 @@
using Grpc.Core.Interceptors;
using Grpc.Core;
namespace BackOffice.Common.Utilities;
public class ErrorHandlerInterceptor : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleResponse(call.ResponseAsync),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
{
try
{
return await inner;
}
catch (Exception ex)
{
GlobalConstants.ConstSnackbar.Add(ex.Message, severity: MudBlazor.Severity.Error);
throw;
}
}
}

View File

@@ -0,0 +1,195 @@
using HtmlAgilityPack;
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
namespace BackOffice.Common.Utilities;
public static class Extensions
{
public static string ExtractUserFriendlyMessage(this string errorMessage)
{
// کلیدواژه‌ای که بعد از آن بخش مورد نظر شروع می‌شود
string keyword = "Exception:";
// بررسی وجود کلیدواژه در پیام خطا
int keywordIndex = errorMessage.IndexOf(keyword);
if (keywordIndex >= 0)
{
// استخراج بخش بعد از کلیدواژه
string userFriendlyMessage = errorMessage.Substring(keywordIndex + keyword.Length).Trim();
if (userFriendlyMessage.EndsWith(")"))
{
userFriendlyMessage = userFriendlyMessage.Substring(0, userFriendlyMessage.Length - 2);
}
return userFriendlyMessage;
}
// اگر کلیدواژه وجود نداشت، کل پیام خطا برگردانده شود
return errorMessage;
}
public static string ToDashString(this string? text) => string.IsNullOrWhiteSpace(text) ? "-" : text;
public static string ToThousands(this double? digit) => digit == null ? "0" : digit.Value.ToString("N0");
public static string ToThousands(this float? digit) => digit == null ? "0" : digit.Value.ToString("N0");
public static string ToThousands(this int? digit) => digit == null ? "0" : digit.Value.ToString("N0");
public static string ToThousands(this long? digit) => digit == null ? "0" : digit.Value.ToString("N0");
public static string ToThousands(this double digit) => digit.ToString("N0");
public static string ToThousands(this float digit) => digit.ToString("N0");
public static string ToThousands(this int digit) => digit.ToString("N0");
public static string ToThousands(this long digit) => digit.ToString("N0");
public static string ToCurrency(this string price) => price + " ریال";
public static DateTime UnixTimeStampToDateTime(this long unixTimeStamp)
{
// Unix timestamp is seconds past epoch
DateTime dateTime = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dateTime = dateTime.AddMilliseconds(unixTimeStamp).ToLocalTime();
return dateTime;
}
public static string PersianToEnglish(this string persianStr)
{
return persianStr.Replace("۰", "0")
.Replace("۱", "1")
.Replace("۲", "2")
.Replace("۳", "3")
.Replace("۴", "4")
.Replace("۵", "5")
.Replace("۶", "6")
.Replace("۷", "7")
.Replace("۸", "8")
.Replace("۹", "9");
}
public static string ArabicToPersian(this string arabicStr)
{
return arabicStr
.Replace("ک", "ك")
.Replace("ی", "ي");
}
public static string Truncate(this string value, int maxLength, bool isAppendDots = false)
{
if (string.IsNullOrEmpty(value)) return value;
return value.Length <= maxLength ? value : isAppendDots ? value.Substring(0, maxLength) + "..." : value.Substring(0, maxLength);
}
public static string DiffDateTime(this DateTime dateTime)
{
const int SECOND = 1;
const int MINUTE = 60 * SECOND;
const int HOUR = 60 * MINUTE;
const int DAY = 24 * HOUR;
const int MONTH = 30 * DAY;
var ts = new TimeSpan(DateTime.Now.Ticks - dateTime.Ticks);
double delta = Math.Abs(ts.TotalSeconds);
if (delta < 1 * MINUTE)
{
return ts.Seconds == 1 ? "لحظه ای قبل" : ts.Seconds + " ثانیه قبل";
}
if (delta < 2 * MINUTE)
{
return "یک دقیقه قبل";
}
if (delta < 45 * MINUTE)
{
return ts.Minutes + " دقیقه قبل";
}
if (delta < 90 * MINUTE)
{
return "یک ساعت قبل";
}
if (delta < 24 * HOUR)
{
return ts.Hours + " ساعت قبل";
}
if (delta < 48 * HOUR)
{
return "دیروز";
}
if (delta < 30 * DAY)
{
return ts.Days + " روز قبل";
}
if (delta < 12 * MONTH)
{
int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
return months <= 1 ? "یک ماه قبل" : months + " ماه قبل";
}
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
return years <= 1 ? "یک سال قبل" : years + " سال قبل";
}
public static bool GenericEquals<T>(this T value1, T value2)
{
if (value1 == null && value2 == null)
return true;
if (value1 != null && value2 == null || value1 == null && value2 != null)
return false;
foreach (var item in value1.GetType().GetProperties())
{
var val1 = item.GetValue(value1);
var val2 = value2.GetType().GetProperty(item.Name).GetValue(value2);
if (Convert.GetTypeCode(val1) == TypeCode.Object)
continue;
var strVal = Convert.ToString(val1);
var strVal2 = Convert.ToString(val2);
if (!strVal.Equals(strVal2))
{
return false;
}
}
return true;
}
public static string GetDisplayName(this Enum enumValue)
{
try
{
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<DisplayAttribute>()?.GetName();
}
catch (Exception ex)
{
return "";
}
}
public static void ChangeLoading(ref bool loading, ref bool toLoading)
{
loading = toLoading;
}
//public static bool IsValidURL(this string source)
//{
// Uri uri = null;
// if (!Uri.TryCreate(source, UriKind.Absolute, out uri) || null == uri)
// return false;
// else
// return true;
//}
public static bool IsValidUrl(this string webSiteUrl)
{
if (webSiteUrl.StartsWith("www."))
{
webSiteUrl = "http://" + webSiteUrl;
}
return Uri.TryCreate(webSiteUrl, UriKind.Absolute, out Uri uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp
|| uriResult.Scheme == Uri.UriSchemeHttps) && uriResult.Host.Replace("www.", "").Split('.').Count() > 1 && uriResult.HostNameType == UriHostNameType.Dns && uriResult.Host.Length > uriResult.Host.LastIndexOf(".") + 1 && 100 >= webSiteUrl.Length;
}
public static string HtmlToText(this string html)
{
if (string.IsNullOrWhiteSpace(html))
return string.Empty;
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
string text = doc.DocumentNode.InnerText;
return WebUtility.HtmlDecode(text).Trim(); // ← این خط مهمه
}
}

View File

@@ -0,0 +1,13 @@
using MudBlazor;
namespace BackOffice.Common.Utilities;
public static class GlobalConstants
{
public const string DateFormat = "MMM dd, yyyy";
public const string DateTimeFormat = "MMM dd, yyyy - HH:mm";
public static string JwtTokenKey = "AuthToken";
public const string SuccessMsg = "با موفقیت انجام شد";
public static ISnackbar ConstSnackbar;
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.Common.Utilities;
public interface ITokenProvider
{
Task<string> GetTokenAsync();
}

View File

@@ -0,0 +1,9 @@
namespace BackOffice.Common.Utilities;
public static class RouteConstance
{
public const string HomePage = "/";
public const string Login = "/Login/";
public const string VerifyCodePage = "/VerifyCodePage/";
public const string Package = "/PackagePage/";
}

View File

@@ -0,0 +1,52 @@
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
namespace BackOffice.Common.Utilities;
public interface ITokenHandler
{
bool IsDifferent(string oldToken, string newToken);
bool IsBlocked(string newToken);
IEnumerable<Claim> GetUserClaims(string token);
}
public class TokenHandler : ITokenHandler
{
public bool IsDifferent(string oldToken, string newToken)
{
oldToken = oldToken.Replace("\"", "");
newToken = newToken.Replace("\"", "");
var handler = new JwtSecurityTokenHandler();
var oldJwtSecurityToken = handler.ReadJwtToken(oldToken);
var newJwtSecurityToken = handler.ReadJwtToken(newToken);
if (newJwtSecurityToken.Claims.Count() != oldJwtSecurityToken.Claims.Count())
return true;
int differences = 0;
foreach (var claim in oldJwtSecurityToken.Claims)
{
if (claim.Type == "exp")
continue;
if (!newJwtSecurityToken.Claims.Any(x => x.Type == claim.Type && x.Value == claim.Value))
{
differences++;
}
}
return differences > 0;
}
public bool IsBlocked(string newToken)
{
newToken = newToken.Replace("\"", "");
var handler = new JwtSecurityTokenHandler();
var newJwtSecurityToken = handler.ReadJwtToken(newToken);
return newJwtSecurityToken.Claims.Any(x => x.Type == "IsBlocked" && x.Value == "True");
}
public IEnumerable<Claim> GetUserClaims(string token)
{
token = token.Replace("\"", "");
var handler = new JwtSecurityTokenHandler();
var newJwtSecurityToken = handler.ReadJwtToken(token);
return newJwtSecurityToken.Claims;
}
}

View File

@@ -0,0 +1,6 @@
namespace BackOffice.Common.Utilities;
public static class UrlUtility
{
public static string DownloadUrl { get; set; }
}

View File

@@ -0,0 +1,3 @@
@page "/"
@attribute [AllowAnonymous]
<h1>Hello, world!</h1>

View File

@@ -0,0 +1,21 @@
@using BackOffice.Shared;
@layout EmptyLayout
@attribute [Route(RouteConstance.Login)]
@attribute [AllowAnonymous]
<MudStack Style="width:100%;height:80vh;" AlignItems="AlignItems.Center" Justify="Justify.Center">
<MudPaper Elevation="4" Width="35%" Height="200px" Class="mx-auto my-auto pa-4">
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)">
<MudStack>
<MudTextField T="string" Variant="Variant.Outlined" Label="شماره موبایل" @bind-Value="@_request.Mobile" For="() => _request.Mobile" HelperText="لطفا شماره موبایل خود را وارد کنید"></MudTextField>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="_isLoading" OnClick="OnSubmitClick" ButtonType="ButtonType.Button" Style="cursor:pointer;">ارسال کد</MudButton>
</MudStack>
</MudForm>
</MudPaper>
</MudStack>

View File

@@ -0,0 +1,41 @@
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
using BackOffice.BFF.Otp.Protobuf.Validator;
using BackOffice.Common.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Login;
public partial class LoginPage
{
private bool _isLoading;
private SendOtpRequest _request = new();
private SendOtpRequestValidator _requestValidator = new();
private MudForm _form;
[Inject] public OtpContract.OtpContractClient OtpContract { get; set; }
private async Task OnSubmitClick()
{
Console.WriteLine(OtpContract == null);
await _form.Validate();
if (!_form.IsValid)
return;
_isLoading = true;
StateHasChanged();
_request.Mobile = _request.Mobile.PersianToEnglish();
try
{
await OtpContract.SendOtpAsync(_request);
Navigation.NavigateTo(RouteConstance.VerifyCodePage + _request.Mobile);
}
catch (Exception ex)
{
Snackbar.Add(message: ex.Message, severity: Severity.Error, null);
}
_isLoading = false;
StateHasChanged();
}
}

View File

@@ -0,0 +1,20 @@
@using BackOffice.Shared
@layout EmptyLayout
@attribute [Route(RouteConstance.VerifyCodePage + "{Mobile}")]
@attribute [AllowAnonymous]
<MudStack Style="width:100%;height:100vh;" AlignItems="AlignItems.Center" Justify="Justify.Center">
<MudPaper Elevation="4" Width="35%" Height="230px" Class="mx-auto my-auto pa-4">
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)">
<MudStack Spacing="5">
<MudTextField T="string" @bind-Value="@_request.Code" For="() => _request.Code" Variant="Variant.Outlined" Label="رمز پویا" HelperText="لطفا کد شش رقمی دریافتی را وارد کنید" InputType="InputType.Telephone" />
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="_isLoading" OnClick="OnSubmitClick">ورود به حساب کاربری</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Text" Disabled="@(_currentCount > 1 || _isLoading)" OnClick="OnResendOtpClick">@(_currentCount < 1 ? "ارسال مجدد رمز پویا" : $"ارسال مجدد رمز پویا {_currentCount}")</MudButton>
</MudStack>
</MudForm>
</MudPaper>
</MudStack>

View File

@@ -0,0 +1,97 @@
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
using BackOffice.BFF.Otp.Protobuf.Validator;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Login;
public partial class VerifyCodePage
{
[Parameter]
public string Mobile { get; set; }
private bool _isLoading;
private VerifyOtpCodeRequest _request = new();
private VerifyOtpCodeRequestValidator _requestValidator = new();
private MudForm _form;
private Timer _timer;
private int _currentCount = 120;
[Inject]
public OtpContract.OtpContractClient OtpContract { get; set; }
protected override void OnInitialized()
{
StartTimer();
}
private async Task OnSubmitClick()
{
await _form.Validate();
if (!_form.IsValid)
return;
_isLoading = true;
StateHasChanged();
_request.Mobile = Mobile.PersianToEnglish();
_request.Code = _request.Code.PersianToEnglish();
try
{
var token = await OtpContract.VerifyOtpCodeAsync(_request);
await LocalStorageService.SetItemAsync(GlobalConstants.JwtTokenKey, token.Token);
Navigation.NavigateTo(RouteConstance.HomePage, forceLoad: true);
}
catch (Exception ex)
{
Snackbar.Add(message: ex.Message, severity: Severity.Error, null);
}
_isLoading = false;
StateHasChanged();
}
private async Task OnResendOtpClick()
{
_isLoading = true;
StateHasChanged();
try
{
await OtpContract.SendOtpAsync(request: new()
{
Mobile = Mobile
});
StartTimer();
}
catch (Exception ex)
{
Snackbar.Add(message: ex.Message, severity: Severity.Error, null);
}
_isLoading = false;
StateHasChanged();
}
private void StartTimer()
{
_currentCount = 120;
_timer = new Timer(new TimerCallback(_ =>
{
if (_currentCount > 0)
{
_currentCount--;
InvokeAsync(() =>
{
StateHasChanged();
});
}
else
{
_timer.Dispose();
}
}), null, 1000, 1000);
}
}

View File

@@ -0,0 +1,55 @@
@using BackOffice.BFF.Package.Protobuf.Protos.Package
@using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
<MudDialog>
<DialogContent>
<MudStack>
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
<Image Src="@_srcImage" Width="200" Height="100" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageFileSelect">
<ActivatorContent>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
ButtonType="ButtonType.Button"
StartIcon="@Icons.Material.Filled.Image"
Style="cursor:pointer;">
انتخاب تصویر
</MudButton>
</ActivatorContent>
<SelectedTemplate>
@if (context != null)
{
<MudText Class="mt-2" Typo="Typo.subtitle2">
<MudTooltip Text="@context.Name" Inline="true">
@context.Name
</MudTooltip>
</MudText>
}
else
{
<MudText Class="mt-2" Typo="Typo.subtitle2">فایلی انتخاب نشده</MudText>
}
</SelectedTemplate>
</MudFileUpload>
</MudStack>
<MudItem xs="12">
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description">
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
</MudHtmlEditor>
</MudItem>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isLoading">لغو</MudButton>
<MudButton Color="Color.Primary" OnClick="CallCreateMethod" Disabled="_isLoading">ثبت </MudButton>
</DialogActions>
</MudDialog>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,75 @@
@using BackOffice.BFF.Package.Protobuf.Protos.Package
@using BackOffice.Common.BaseComponents
@using DataModel = BackOffice.BFF.Package.Protobuf.Protos.Package.GetAllPackageByFilterResponseModel
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
Hover="true" @ref="_gridData" Height="72vh">
<ColGroup>
<col />
<col />
<col />
<col style="width: 58px;" />
</ColGroup>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.Title" Title="عنوان" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center">
<Image Src="@context.Item.ImagePath" Width="25" Height="25" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Fill" />
@if (string.IsNullOrWhiteSpace(context.Item.Title))
{
<MudText Typo="Typo.inherit">-</MudText>
}
else
{
<MudTooltip Text="@(context.Item.Title)" Arrow="true" Style="@($"{(context.Item.Title.Length < 70 ? string.Empty : "width:600px;")}")">
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
@(context.Item.Title.Truncate(20, true))
</MudText>
</MudTooltip>
}
</MudStack>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Description" Title="توضیحات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
@if (string.IsNullOrWhiteSpace(context.Item.Description))
{
<MudText Typo="Typo.inherit">-</MudText>
}
else
{
<MudTooltip Text="@(context.Item.Description.HtmlToText())" Arrow="true" Style="@($"{(context.Item.Description.Length < 70 ? string.Empty : "width:600px;")}")">
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
@(context.Item.Description.HtmlToText().Truncate(20, true))
</MudText>
</MudTooltip>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" Style="cursor:pointer;" />
</MudTooltip>
<MudTooltip Text="آرشیو">
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" Style="cursor:pointer;" />
</MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="DataModel" PageSizeOptions=@(new int[] { 30, 60, 90 }) InfoFormat="سطر {first_item} تا {last_item} از {all_items}" RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>

View File

@@ -0,0 +1,74 @@
using BackOffice.BFF.Package.Protobuf.Protos.Package;
using Google.Protobuf.WellKnownTypes;
using HtmlAgilityPack;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using static Google.Rpc.Context.AttributeContext.Types;
using DataModel = BackOffice.BFF.Package.Protobuf.Protos.Package.GetAllPackageByFilterResponseModel;
namespace BackOffice.Pages.Package.Components;
public partial class PackageDataTable
{
[Inject] public PackageContract.PackageContractClient PackageContract { get; set; }
private bool _isLoading = true;
private MudDataGrid<DataModel> _gridData;
private GetAllPackageByFilterRequest _request = new() { Filter = new() };
private async Task<GridData<DataModel>> ServerReload(GridState<DataModel> state)
{
_request.Filter ??= new();
_request.PaginationState ??= new();
_request.PaginationState.PageNumber = state.Page + 1;
_request.PaginationState.PageSize = state.PageSize;
var result = await PackageContract.GetAllPackageByFilterAsync(_request);
if (result != null && result.Models != null && result.Models.Any())
{
return new GridData<DataModel>() { Items = result.Models.ToList(), TotalItems = (int)result.MetaData.TotalCount };
}
return new GridData<DataModel>();
}
public async Task Update(DataModel model)
{
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdatePackageRequest>() } };
var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
var result = await dialog.Result;
if (!result.Canceled)
{
ReLoadData();
//Reload Data
Snackbar.Add("عملیات با موفقیت انجام شد", Severity.Success);
}
}
private async Task OnDelete(DataModel model)
{
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
bool? result = await DialogService.ShowMessageBox(
"اخطار",
"آیا از حذف این مورد مطمئن هستید؟",
yesText: "حذف", cancelText: "لغو",
options: options);
if (result != null && result.Value)
{
await PackageContract.DeletePackageAsync(new()
{
Id = model.Id
});
ReLoadData();
}
StateHasChanged();
}
public async void ReLoadData()
{
if (_gridData != null)
await _gridData.ReloadServerData();
}
}

View File

@@ -0,0 +1,55 @@
@using BackOffice.BFF.Package.Protobuf.Protos.Package
@using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
<MudDialog>
<DialogContent>
<MudStack>
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
<Image Src="@_srcImage" Width="200" Height="100" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageFileSelect">
<ActivatorContent>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Image"
ButtonType="ButtonType.Button"
Style="cursor:pointer;">
انتخاب تصویر
</MudButton>
</ActivatorContent>
<SelectedTemplate>
@if (context != null)
{
<MudText Class="mt-2" Typo="Typo.subtitle2">
<MudTooltip Text="@context.Name" Inline="true">
@context.Name
</MudTooltip>
</MudText>
}
else
{
<MudText Class="mt-2" Typo="Typo.subtitle2">فایلی انتخاب نشده</MudText>
}
</SelectedTemplate>
</MudFileUpload>
</MudStack>
<MudItem xs="12">
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description">
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
</MudHtmlEditor>
</MudItem>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isLoading">لغو</MudButton>
<MudButton Color="Color.Primary" OnClick="CallUpdateMethod" Disabled="_isLoading">ثبت</MudButton>
</DialogActions>
</MudDialog>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
@attribute [Route(RouteConstance.Package)]
@using BackOffice.BFF.Package.Protobuf.Protos.Package
@using BackOffice.Pages.Package.Components
<MudStack>
<MudPaper Elevation="1" Class="pa-4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>مدیریت پکیج</MudText>
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" ButtonType="ButtonType.Button" OnClick="CreateNew" Style="cursor:pointer;">افزودن</MudButton>
</MudStack>
</MudStack>
</MudPaper>
<BackOffice.Pages.Package.Components.PackageDataTable @ref="_table" />
</MudStack>
@code {
private PackageDataTable _table;
public async Task CreateNew()
{
var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
var result = await dialog.Result;
if (!result.Canceled)
{
_table.ReLoadData();
Snackbar.Add("عملیات با موفقیت انجام شد", Severity.Success);
}
}
}

View File

@@ -0,0 +1,15 @@
using BackOffice;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddCommonServices(builder.Configuration);
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -0,0 +1,38 @@
{
"iisSettings": {
"iisExpress": {
"applicationUrl": "http://localhost:21136",
"sslPort": 44321
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5018",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7106;http://localhost:5018",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,36 @@
@inherits LayoutComponentBase
<MudRTLProvider RightToLeft="true">
<MudThemeProvider Theme="CustomTheme" />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudMainContent>
<MudStack>
@Body
</MudStack>
</MudMainContent>
</MudLayout>
</MudRTLProvider>
@code {
MudTheme CustomTheme = new MudTheme()
{
Typography = new Typography()
{
Default = new Default()
{
FontFamily = new[] { "IRANSans" }
}
},
LayoutProperties = new()
{
DrawerWidthRight = "250px"
},
};
}

View File

@@ -0,0 +1,36 @@
@using BackOffice.Shared
@using Microsoft.AspNetCore.Components.Authorization
@inherits LayoutComponentBase
@attribute [AllowAnonymous]
<MudRTLProvider RightToLeft="true">
<MudThemeProvider Theme="CustomTheme" />
<MudDialogProvider />
<MudSnackbarProvider />
<MudPopoverProvider />
<MudLayout>
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
مدیریت
<MudSpacer />
<AuthorizeView>
<Authorized>
<MudText Class="mx-3 py-0" Typo="Typo.overline">@(context.User.Claims?.FirstOrDefault(x => x.Type == "sub")?.Value)</MudText>
<MudBadge Color="Color.Warning" Visible="false" Overlap="true" Dot="true" Bordered="false">
<MudIconButton Icon="@Icons.Material.Outlined.Person4" Color="Color.Inherit" Style="border: 1px solid #5e4df9;" />
</MudBadge>
</Authorized>
</AuthorizeView>
</MudAppBar>
<MudDrawer @bind-Open="@_drawerOpen" Breakpoint="Breakpoint.Lg" Elevation="1" Variant="@DrawerVariant.Responsive">
<NavMenu />
</MudDrawer>
<MudMainContent>
<MudStack Class="pa-4 mt-5">
@Body
</MudStack>
</MudMainContent>
</MudLayout>
</MudRTLProvider>

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
namespace BackOffice.Shared;
public partial class MainLayout
{
bool _drawerOpen = true;
private string Details { get; set; }
void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}
MudTheme CustomTheme = new MudTheme()
{
Typography = new Typography()
{
Default = new Default()
{
FontFamily = new[] { "IRANSans" }
}
}
};
}

View File

@@ -0,0 +1,25 @@
@using BackOffice.BFF.Package.Protobuf.Protos.Package
@using Microsoft.AspNetCore.Components.Authorization
<MudNavMenu Bordered="true">
<MudNavLink Match="NavLinkMatch.Prefix" Href="/">داشبورد</MudNavLink>
<AuthorizeView Roles="Administrator">
<Authorized>
<MudNavLink Match="NavLinkMatch.Prefix" Href="@(RouteConstance.Package)">مدیریت پکیج</MudNavLink>
</Authorized>
</AuthorizeView>
<MudNavLink Match="NavLinkMatch.Prefix" OnClick="Signout">خروج از حساب</MudNavLink>
</MudNavMenu>
@code {
public async Task Signout()
{
await LocalStorageService.RemoveItemAsync("AuthToken");
Navigation.NavigateTo("/Login");
}
}

View File

@@ -0,0 +1,22 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Blazored.LocalStorage
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using BackOffice
@using BackOffice.Common.Utilities
@using MudBlazor
@using Mapster
@using DateTimeConverterCL
@attribute [Authorize(Roles = "Administrator, Admin, Author")]
@inject MudBlazor.IDialogService DialogService
@inject MudBlazor.ISnackbar Snackbar
@inject IJSRuntime jsRuntime
@inject NavigationManager Navigation
@inject ILocalStorageService LocalStorageService

View File

@@ -0,0 +1,9 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"GwUrl": "https://localhost:6468",
//"GwUrl": "https://localhost:6468",
"Authentication": {
//"Authority": "https://localhost:5001",
"Authority": "https://ids.afrino.co/",
"ClientId": "client_backoffice_spa"
}
}

View File

@@ -0,0 +1,82 @@
@font-face {
font-family: IRANSans;
font-style: normal;
font-weight: 500;
src: url('../fonts/eot/IRANSansWeb(FaNum)_Medium.eot');
src: url('../fonts/eot/IRANSansWeb(FaNum)_Medium.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
url('../fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
url('../fonts/woff/IRANSansWeb(FaNum)_Medium.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url('../fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf') format('truetype');
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
font-family: "Vazir";
direction: rtl;
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "بارگذاری ...");
}
.mud-input.mud-input-outlined {
background-color: var(--mud-palette-surface);
}
h1:focus {
outline: none;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مدیریت آفرینو</title>
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.css" rel="stylesheet" />
<!-- If you add any scoped CSS files, uncomment the following to load them
<link href="BackOffice.styles.css" rel="stylesheet" /> -->
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="/js/main.js"></script>
<script src="/js/quill.js"></script>
<script src="_content/Tizzani.MudBlazor.HtmlEditor/quill-blot-formatter.min.js"></script> <!-- optional; for image resize -->
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
function jsSaveAsFile(filename, byteBase64) {
var link = document.createElement('a');
link.download = filename;
link.href = "data:application/octet-stream;base64," + byteBase64;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
{
"name": "Afrino",
"short_name": "Afrino",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"prefer_related_applications": false,
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
}
]
}

View File

@@ -0,0 +1,4 @@
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });

View File

@@ -0,0 +1,47 @@
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url/*, { integrity: asset.hash, cache: 'no-cache' }*/));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
const shouldServeIndexHtml = event.request.mode === 'navigate';
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}