commit 0304b03201ab0b7f0af0cc682f01433c8e3b0d8a Author: Gitea Actions Date: Sat Aug 1 11:51:13 2026 +0000 Publish from private repository diff --git a/MyOffice.Core/Error.cs b/MyOffice.Core/Error.cs new file mode 100644 index 0000000..48cf996 --- /dev/null +++ b/MyOffice.Core/Error.cs @@ -0,0 +1,23 @@ +namespace MyOffice.Core; + +public class Error +{ + private Exception? _exception; + + public Error(string message) + { + Message = message; + } + + public Error(Exception exception): this(exception.GetType().Name) + { + _exception = exception; + } + + public Error(string message, Exception exception): this(message) + { + _exception = exception; + } + + public string Message { get; private set;} +} \ No newline at end of file diff --git a/MyOffice.Core/Exec.cs b/MyOffice.Core/Exec.cs new file mode 100644 index 0000000..3f929c3 --- /dev/null +++ b/MyOffice.Core/Exec.cs @@ -0,0 +1,106 @@ +namespace MyOffice.Core; + +public enum GeneralExecStatus +{ + success, + failure, + not_found, + forbidden +} + +public class Exec : Exec + where TResult : class +{ + public Exec(TResult result, GeneralExecStatus status) : base(result, status) + { + } + + public Exec(GeneralExecStatus status) : base(status) + { + } + + public Exec() : base(GeneralExecStatus.success) + { + } + + public new Exec Set(GeneralExecStatus status) + { + Status = status; + + return this; + } + + public new Exec Set(TResult? result) + { + Result = result; + Status = result == null ? GeneralExecStatus.failure : GeneralExecStatus.success; + + return this; + } +} + +public class Exec + where TResult : class + where TStatus : Enum +{ + public static Exec Start(TResult result, TStatus status) + { + return new Exec(result, status); + } + + public static Exec Start(TStatus status) + { + return new Exec(status); + } + + public static Exec StartSuccess() + { + return new Exec(GeneralExecStatus.success); + } + + public static Exec StartFailure() + { + return new Exec(GeneralExecStatus.failure); + } + + public static Exec StartNotFound() + { + return new Exec(GeneralExecStatus.not_found); + } + + public Exec(TResult result, TStatus status) + { + Result = result; + Status = status; + } + + public Exec(TStatus status) + { + Status = status; + } + + public TResult? Result { get; internal set; } + public TStatus Status { get; internal set; } + + public Exec Set(TResult result, TStatus status) + { + Result = result; + Status = status; + + return this; + } + + public Exec Set(TStatus status) + { + Status = status; + + return this; + } + + public Exec Set(TResult result) + { + Result = result; + + return this; + } +} \ No newline at end of file diff --git a/MyOffice.Core/Extensions/BoolExtensions.cs b/MyOffice.Core/Extensions/BoolExtensions.cs new file mode 100644 index 0000000..8639085 --- /dev/null +++ b/MyOffice.Core/Extensions/BoolExtensions.cs @@ -0,0 +1,14 @@ +namespace MyOffice.Core.Extensions; + +public static class BoolExtensions +{ + public static string ToLowerCase(this bool value) + { + return value.ToString().ToLower(); + } + + public static string? ToLowerCase(this bool? value) + { + return value == null ? null : value.ToString()?.ToLower(); + } +} \ No newline at end of file diff --git a/MyOffice.Core/Extensions/ClaimsExtensions.cs b/MyOffice.Core/Extensions/ClaimsExtensions.cs new file mode 100644 index 0000000..af77327 --- /dev/null +++ b/MyOffice.Core/Extensions/ClaimsExtensions.cs @@ -0,0 +1,31 @@ +namespace MyOffice.Core.Extensions; + +using System.Security.Claims; + +public static class ClaimsExtensions +{ + public static string? GetValue(this IEnumerable claims, string type) + { + return claims.FirstOrDefault(x => x.Type.Equals(type, StringComparison.OrdinalIgnoreCase))?.Value; + } + + public static string? GetValue(this IEnumerable claims, string[] types) + { + return claims.FirstOrDefault(x => types.Any(z => x.Type.Equals(z, StringComparison.OrdinalIgnoreCase)))?.Value; + } + + public static string? GetEmail(this IEnumerable claims) + { + return GetValue(claims, new[] { ClaimTypes.Email, "email" }); + } + + public static string? GetNameIdentifier(this IEnumerable claims) + { + return GetValue(claims, ClaimTypes.NameIdentifier); + } + + public static string? GetSID(this IEnumerable claims) + { + return GetValue(claims, "sid"); + } +} \ No newline at end of file diff --git a/MyOffice.Core/Extensions/DateTimeExtensions.cs b/MyOffice.Core/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..8b22231 --- /dev/null +++ b/MyOffice.Core/Extensions/DateTimeExtensions.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Core.Extensions; + +public static class DateTimeExtensions +{ + public static DateTime StartOfDay(this DateTime dateTime) + { + return dateTime.Date; + } + + public static DateTime EndOfDay(this DateTime dateTime) + { + return dateTime.AddDays(1).AddTicks(-1); + } + + public static DateTime ToUtc(this DateTime dateTime) + { + return new DateTime(dateTime.Ticks, DateTimeKind.Utc); + } +} \ No newline at end of file diff --git a/MyOffice.Core/Extensions/GuidExtensions.cs b/MyOffice.Core/Extensions/GuidExtensions.cs new file mode 100644 index 0000000..6a95e4a --- /dev/null +++ b/MyOffice.Core/Extensions/GuidExtensions.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Core.Extensions; + +public static class GuidExtensions +{ + public static bool EqualsString(this Guid guid, string str) + { + return guid.ToString().EqualsIgnoreCase(str); + } + + public static string ToShort(this Guid guid) + { + return guid.ToString("N"); + } + + public static string? ToShort(this Guid? guid) + { + return guid?.ToString("N"); + } +} \ No newline at end of file diff --git a/MyOffice.Core/Extensions/StringExtensions.cs b/MyOffice.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..eca1473 --- /dev/null +++ b/MyOffice.Core/Extensions/StringExtensions.cs @@ -0,0 +1,65 @@ +namespace MyOffice.Core.Extensions; + +using Microsoft.Extensions.Logging; + +public static class StringExtensions +{ + public static bool IsMissing(this string? str) + { + return str == null || string.IsNullOrEmpty(str) || string.IsNullOrEmpty(str.Trim()); + } + + public static bool IsPresent(this string? str) + { + return !IsMissing(str); + } + + public static string? NullIfEmpty(this string? str) + { + return IsMissing(str) ? null : str; + } + + public static bool EqualsIgnoreCase(this string? str, string? value) + { + return str != null && str.Equals(value, StringComparison.OrdinalIgnoreCase); + } + + public static string? SafeTrim(this string? str) + { + return str == null ? str : str!.Trim(); + } + + public static string? MaxOf(this string? str, int maxLength) + { + if (str == null) + { + return null; + } + + if (str.Length < maxLength) + { + return str; + } + + return str.Substring(0, maxLength); + } + + public static bool AsBool(this string? str, bool defaultValue = false) + { + return true.ToString().Equals(str, StringComparison.OrdinalIgnoreCase) || defaultValue; + } + + public static Guid AsGuid(this string str) + { + return Guid.Parse(str); + } + + public static Guid? AsGuidNull(this string? str) + { + if (Guid.TryParse(str, out var guid)) + { + return guid; + } + return null; + } +} \ No newline at end of file diff --git a/MyOffice.Core/Helpers/JsonHelper.cs b/MyOffice.Core/Helpers/JsonHelper.cs new file mode 100644 index 0000000..100f7bc --- /dev/null +++ b/MyOffice.Core/Helpers/JsonHelper.cs @@ -0,0 +1,31 @@ +namespace MyOffice.Core.Helpers +{ + using System.Text.Json; + using System.Text.Json.Serialization; + + public static class JsonHelper + { + public static string ToJson(this object entity, JsonSerializerOptions? options = null) + { + options ??= new JsonSerializerOptions + { + MaxDepth = 0, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + WriteIndented = true, + }; + return JsonSerializer.Serialize(entity, options); + } + } + + /*public class CustomIgnoreReferenceHandler : ReferenceHandler + { + //public CustomIgnoreReferenceHandler() => HandlingStrategy = ReferenceHandlingStrategy.IgnoreCycles; + public CustomIgnoreReferenceHandler() + { + new HandlingStrategy { } = true; + } + + public override ReferenceResolver CreateResolver() => new IgnoreReferenceResolver(); + }*/ +} diff --git a/MyOffice.Core/Helpers/RandomizationHelper.cs b/MyOffice.Core/Helpers/RandomizationHelper.cs new file mode 100644 index 0000000..f240a4f --- /dev/null +++ b/MyOffice.Core/Helpers/RandomizationHelper.cs @@ -0,0 +1,18 @@ +namespace MyOffice.Core.Helpers; + +using MyOffice.Core.Extensions; + +public class RandomizationHelper +{ + public static string Generate(int length) + { + var result = ""; + + while(result.Length < length) + { + result += Guid.NewGuid().ToShort(); + } + + return result.Substring(0, length); + } +} diff --git a/MyOffice.Core/Identity/IExternalProviderValidator.cs b/MyOffice.Core/Identity/IExternalProviderValidator.cs new file mode 100644 index 0000000..d74d303 --- /dev/null +++ b/MyOffice.Core/Identity/IExternalProviderValidator.cs @@ -0,0 +1,47 @@ +namespace MyOffice.Core.Identity; + +using System; +using System.Diagnostics.Contracts; + +public interface IExternalProviderValidator +{ + string Provider { get; } + public bool IsConfigured { get; } + + Task ValidateAsync(string token); +} + +public class ExternalProviderValidatorResult +{ + public static ExternalProviderValidatorResult Failed() + { + return new ExternalProviderValidatorResult(); + } + + public static ExternalProviderValidatorResult Success(string email, bool emailVerified, string externalId, + string? fullName) + { + return new ExternalProviderValidatorResult(email, emailVerified, externalId, fullName); + } + + public ExternalProviderValidatorResult() + { + IsSuccessed = false; + } + + public ExternalProviderValidatorResult(string email, bool emailVerified, string externalId, string? fullName) : + this() + { + Email = email; + EmailVerified = emailVerified; + IsSuccessed = true; + ExternalId = externalId; + FullName = fullName; + } + + public bool IsSuccessed { get; } + public string? Email { get; } + public bool EmailVerified { get; } + public string? ExternalId { get; } + public string? FullName { get; } +} \ No newline at end of file diff --git a/MyOffice.Core/Interfaces/IDataModel.cs b/MyOffice.Core/Interfaces/IDataModel.cs new file mode 100644 index 0000000..586eae7 --- /dev/null +++ b/MyOffice.Core/Interfaces/IDataModel.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Core; + +public interface IDataModel +{ + +} diff --git a/MyOffice.Core/Interfaces/IDataModelDto.cs b/MyOffice.Core/Interfaces/IDataModelDto.cs new file mode 100644 index 0000000..073a84b --- /dev/null +++ b/MyOffice.Core/Interfaces/IDataModelDto.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Core; + +public interface IDataModelDto where TSource : IDataModel +{ + +} diff --git a/MyOffice.Core/MyOffice.Core.csproj b/MyOffice.Core/MyOffice.Core.csproj new file mode 100644 index 0000000..13dc371 --- /dev/null +++ b/MyOffice.Core/MyOffice.Core.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/MyOffice.Data.Models/Accounts/Account.cs b/MyOffice.Data.Models/Accounts/Account.cs new file mode 100644 index 0000000..9048af0 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/Account.cs @@ -0,0 +1,51 @@ +namespace MyOffice.Data.Models.Accounts; + +using Currencies; +using MyOffice.Core; +using MyOffice.Data.Models.Users; + +public class Account: IDataModel +{ + public Guid Id { get; set; } + public string CurrencyGlobalId { get; set; } = null!; + public CurrencyGlobal? CurrencyGlobal { get; set; } + public Guid? OwnerId { get; set; } + public User? Owner { get; set; } + public string Name { get; set; } = null!; + public IEnumerable? AccessRights { get; set; } + public IEnumerable? Motions { get; set; } + public IEnumerable? Categories { get; set; } + public IEnumerable? Invites { get; set; } +} + +public class AccountDetailed: IDataModel +{ + public Account Account { get; set; } = null!; + public decimal TotalPlus { get; set; } + public decimal TotalMinus { get; set; } + public decimal Rest => TotalPlus - TotalMinus; +} + +public class AccountSimple +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public AccountAccessTypeEnum Type { get; set; } + public string? CurrencyName { get; set; } + public string? CurrencyShortName { get; set; } + public decimal? CurrencyRate { get; set; } + public int? CurrencyQuantity { get; set; } + public decimal? TotalPlus { get; set; } + public decimal? TotalMinus { get; set; } + public decimal? Balance { get; set; } +} + +public class MotionTotalSimple +{ + public Guid? Id { get; set; } + public string Name { get; set; } = null!; + public string CurrencyId { get; set; } = null!; + public string CurrencyName { get; set; } = null!; + public decimal CurrencyRate { get; set; } + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Accounts/AccountAccess.cs b/MyOffice.Data.Models/Accounts/AccountAccess.cs new file mode 100644 index 0000000..0d1aeb3 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/AccountAccess.cs @@ -0,0 +1,28 @@ +namespace MyOffice.Data.Models.Accounts; + +using MyOffice.Data.Models.Users; + +public enum AccountAccessTypeEnum +{ + balance, + credit, + external, + other, +} + +public class AccountAccess +{ + public int Id { get; set; } + public Guid AccountId { get; set; } + public Account? Account { get; set; } + public Guid UserId { get; set; } + public User? User { get; set; } + public Guid OwnerId { get; set; } + public User? Owner { get; set; } + + public bool IsAllowRead { get; set; } + public bool IsAllowWrite { get; set; } + public bool IsAllowManage { get; set; } + public AccountAccessTypeEnum Type { get; set; } + public string? Name { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Accounts/AccountAccessInvite.cs b/MyOffice.Data.Models/Accounts/AccountAccessInvite.cs new file mode 100644 index 0000000..0827695 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/AccountAccessInvite.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Models.Accounts; + +using Users; + +public class AccountAccessInvite +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public User? User { get; set; } + public Guid AccountId { get; set; } + public Account? Account { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime? AcceptedOn { get; set; } + public DateTime? RejectedOn { get; set; } + public string Email { get; set; } = null!; + public bool IsAllowWrite { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Accounts/AccountAccountCategory.cs b/MyOffice.Data.Models/Accounts/AccountAccountCategory.cs new file mode 100644 index 0000000..3ede78a --- /dev/null +++ b/MyOffice.Data.Models/Accounts/AccountAccountCategory.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Data.Models.Accounts; + +public class AccountAccountCategory +{ + public int Id { get; set; } + public Guid AccountId { get; set; } + public Account Account { get; set; } = null!; + public Guid CategoryId { get; set; } + public AccountCategory Category { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Accounts/AccountCategory.cs b/MyOffice.Data.Models/Accounts/AccountCategory.cs new file mode 100644 index 0000000..dc445b9 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/AccountCategory.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Data.Models.Accounts; + +using MyOffice.Data.Models.Users; + +public class AccountCategory +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public User? User { get; set; } + public string Name { get; set; } = null!; + public IEnumerable? Accounts { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs b/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs new file mode 100644 index 0000000..e5d2504 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Data.Models.Accounts.Domain; + +using Currencies; + +public class AccountWithRate +{ + public Account Account { get; set; } = null!; + public Currency? Currency { get; set; } + public CurrencyRate? Rate { get; set; } +} diff --git a/MyOffice.Data.Models/Accounts/Motion.cs b/MyOffice.Data.Models/Accounts/Motion.cs new file mode 100644 index 0000000..1439e8d --- /dev/null +++ b/MyOffice.Data.Models/Accounts/Motion.cs @@ -0,0 +1,22 @@ +namespace MyOffice.Data.Models.Accounts; + +using Items; + +/// +/// Account motion +/// DateTime: 2023-01-01, Account: 'Debit card', Item.ItemGlobal: 'Primary salary', AmountPlus: 5000 +/// +public class Motion +{ + public Guid Id { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime DateTime { get; set; } + public Guid AccountId { get; set; } + public Account Account { get; set; } = null!; + public int ItemId { get; set; } + public Item Item { get; set; } = null!; + public string? Description { get; set; } + public decimal AmountPlus { get; set; } + public decimal AmountMinus { get; set; } + public DateTime? DeletedOn { get; set; } +} diff --git a/MyOffice.Data.Models/Currencies/Currency.cs b/MyOffice.Data.Models/Currencies/Currency.cs new file mode 100644 index 0000000..c8cd254 --- /dev/null +++ b/MyOffice.Data.Models/Currencies/Currency.cs @@ -0,0 +1,21 @@ +namespace MyOffice.Data.Models.Currencies; + +using MyOffice.Data.Models.Users; + +public class Currency +{ + public Guid Id { get; set; } + public string CurrencyGlobalId { get; set; } = null!; + public CurrencyGlobal? CurrencyGlobal { get; set; } + public Guid UserId { get; set; } + public User? User { get; set; } + + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public IEnumerable? Rates { get; set; } + + public int? CurrentRateId { get; set; } + public CurrencyRate? CurrentRate { get; set; } + + public bool IsPrimary { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Currencies/CurrencyGlobal.cs b/MyOffice.Data.Models/Currencies/CurrencyGlobal.cs new file mode 100644 index 0000000..78ea67e --- /dev/null +++ b/MyOffice.Data.Models/Currencies/CurrencyGlobal.cs @@ -0,0 +1,26 @@ +namespace MyOffice.Data.Models.Currencies; + +using MyOffice.Data.Models.Accounts; + +public enum CurrencyGlobalIdEnum +{ + UAH, + USD, + EUR, + GBP, + RUB, + BTC, + ETH, + TON, + OTHER, +} + +public class CurrencyGlobal +{ + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + public string Symbol { get; set; } = null!; + public int DefaultQuantity { get; set; } + public IEnumerable? Currencies { get; set; } + public IEnumerable? Accounts { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Currencies/CurrencyRate.cs b/MyOffice.Data.Models/Currencies/CurrencyRate.cs new file mode 100644 index 0000000..8d20bbe --- /dev/null +++ b/MyOffice.Data.Models/Currencies/CurrencyRate.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Data.Models.Currencies; + +public class CurrencyRate +{ + public int Id { get; set; } + public Guid CurrencyId { get; set; } + public Currency? Currency { get; set; } + public DateTime DateTime { get; set; } + public int Quantity { get; set; } + public decimal Rate { get; set; } + public IEnumerable? Currencies { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Items/Item.cs b/MyOffice.Data.Models/Items/Item.cs new file mode 100644 index 0000000..17cb931 --- /dev/null +++ b/MyOffice.Data.Models/Items/Item.cs @@ -0,0 +1,20 @@ +namespace MyOffice.Data.Models.Items; + +using MyOffice.Data.Models.Accounts; + +/// +/// User 'Item' linked to global item +/// ItemGlobal: 'Primary salary', ItemCategory: 'Incomes' +/// +public class Item +{ + public int Id { get; set; } + /// + /// UnCategorized category is CategoryId = UserId + /// + public Guid CategoryId { get; set; } + public ItemCategory? Category { get; set; } + public Guid ItemGlobalId { get; set; } + public ItemGlobal ItemGlobal { get; set; } = null!; + public List? Motions { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Items/ItemCategory.cs b/MyOffice.Data.Models/Items/ItemCategory.cs new file mode 100644 index 0000000..f368199 --- /dev/null +++ b/MyOffice.Data.Models/Items/ItemCategory.cs @@ -0,0 +1,13 @@ +namespace MyOffice.Data.Models.Items; + +using MyOffice.Data.Models.Users; + +public class ItemCategory +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public User User { get; set; } = null!; + public string Name { get; set; } = null!; + public List Items { get; set; } = null!; + public bool IsInternal { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Items/ItemGlobal.cs b/MyOffice.Data.Models/Items/ItemGlobal.cs new file mode 100644 index 0000000..420d19b --- /dev/null +++ b/MyOffice.Data.Models/Items/ItemGlobal.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Data.Models.Items; + +public class ItemGlobal +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public IEnumerable Items { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Data.Models/MyOffice.Data.Models.csproj b/MyOffice.Data.Models/MyOffice.Data.Models.csproj new file mode 100644 index 0000000..34cd07d --- /dev/null +++ b/MyOffice.Data.Models/MyOffice.Data.Models.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/MyOffice.Data.Models/Notifications/EmailTemplate.cs b/MyOffice.Data.Models/Notifications/EmailTemplate.cs new file mode 100644 index 0000000..4b17ef2 --- /dev/null +++ b/MyOffice.Data.Models/Notifications/EmailTemplate.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Models.Notifications; + +public enum EmailTemplateEnum +{ + PasswordRestore, +} + +public class EmailTemplate +{ + public string Id { get; set; } + public DateTime CreatedOn { get; set; } + public string Description { get; set; } + public string Subject { get; set; } + public string Sender { get; set; } + public string SenderName { get; set; } + public string Template { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Users/User.cs b/MyOffice.Data.Models/Users/User.cs new file mode 100644 index 0000000..2246452 --- /dev/null +++ b/MyOffice.Data.Models/Users/User.cs @@ -0,0 +1,35 @@ +namespace MyOffice.Data.Models.Users; + +using Accounts; +using Currencies; +using Items; + +public class User +{ + public User() + { + CurrencyId = "USD"; + } + + public Guid Id { get; set; } + public string UserName { get; set; } = null!; + public string Email { get; set; } = null!; + public string PasswordHash { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? FullName { get; set; } + public string? Phone { get; set; } + public bool IsEmailConfirmed { get; set; } + public IEnumerable? UserClaims { get; set; } + + public string CurrencyId { get; set; } + public CurrencyGlobal? Currency { get; set; } + public IEnumerable? Currencies { get; set; } + public IEnumerable? AccountAccess { get; set; } + public IEnumerable? AccountMotions { get; set; } + public IEnumerable? AccountCategories { get; set; } + public IEnumerable? ItemCategories { get; set; } + public IEnumerable? Accounts { get; set; } + public IEnumerable? AccountAccessOwners { get; set; } + public IEnumerable? AccountAccessInvites { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Users/UserExternal.cs b/MyOffice.Data.Models/Users/UserExternal.cs new file mode 100644 index 0000000..b86f5df --- /dev/null +++ b/MyOffice.Data.Models/Users/UserExternal.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Data.Models.Users; + +public class UserExternal +{ + public int Id { get; set; } + public DateTime CreatedOn { get; set; } + public Guid UserId { get; set; } + public User User { get; set; } = null!; + public string ExternalId { get; set; } = null!; + public string Email { get; set; } = null!; + public string Provider { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Verifications/VerificationCode.cs b/MyOffice.Data.Models/Verifications/VerificationCode.cs new file mode 100644 index 0000000..272f823 --- /dev/null +++ b/MyOffice.Data.Models/Verifications/VerificationCode.cs @@ -0,0 +1,18 @@ +namespace MyOffice.Data.Models.Verifications; + +using MyOffice.Data.Models.Users; + +public class VerificationCode +{ + public int Id { get; set; } + public Guid UserId { get; set; } + public User? User { get; set; } + public string Code { get; set; } = null!; + public string DestinationType { get; set; } = null!; + public string Destination { get; set; } = null!; + public DateTime CreatedOn { get; set; } + public DateTime ExpiresOn { get; set; } + public DateTime? VerifiedOn { get; set; } + public string Template { get; set; } = null!; + public string? Metadata { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Models/Verifications/VerificationCodeTemplateEnum.cs b/MyOffice.Data.Models/Verifications/VerificationCodeTemplateEnum.cs new file mode 100644 index 0000000..96096a0 --- /dev/null +++ b/MyOffice.Data.Models/Verifications/VerificationCodeTemplateEnum.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Data.Models.Verifications; + +public enum VerificationCodeTemplateEnum +{ + password_restore, + email_confirm, +} diff --git a/MyOffice.Data.Models/Verifications/VerificationCodeTypeEnum.cs b/MyOffice.Data.Models/Verifications/VerificationCodeTypeEnum.cs new file mode 100644 index 0000000..642a403 --- /dev/null +++ b/MyOffice.Data.Models/Verifications/VerificationCodeTypeEnum.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Data.Models.Verifications; + +public enum VerificationCodeTypeEnum +{ + email, + phone, +} diff --git a/MyOffice.Data.Repositories/Account/AccountAccessInviteRepository.cs b/MyOffice.Data.Repositories/Account/AccountAccessInviteRepository.cs new file mode 100644 index 0000000..967d7cd --- /dev/null +++ b/MyOffice.Data.Repositories/Account/AccountAccessInviteRepository.cs @@ -0,0 +1,81 @@ +namespace MyOffice.Data.Repositories.Account; + +using Microsoft.EntityFrameworkCore; +using Models.Accounts; +using MyOffice.Data.Repositories; +using MyOffice.DbContext; + +public class AccountAccessInviteRepository : AppRepository, IAccountAccessInviteRepository +{ + public AccountAccessInviteRepository(AppDbContext context) : base(context) + { + } + + public AccountAccessInvite? Get(Guid userId, string email) + { + return _context.AccountAccessInvites + .FirstOrDefault(x => x.UserId == userId + && x.Email == email + && !x.AcceptedOn.HasValue + && !x.RejectedOn.HasValue + ); + } + + public async Task GetAsync(Guid userId, string email, CancellationToken cancellationToken = default) + { + return await _context.AccountAccessInvites + .FirstOrDefaultAsync(x => x.UserId == userId + && x.Email == email + && !x.AcceptedOn.HasValue + && !x.RejectedOn.HasValue, + cancellationToken); + } + + public bool Add(AccountAccessInvite invite) + { + return AddBase(invite) > 0; + } + + public async Task AddAsync(AccountAccessInvite invite, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(invite, cancellationToken) > 0; + } + + public bool Update(AccountAccessInvite invite) + { + return UpdateBase(invite) > 0; + } + + public async Task UpdateAsync(AccountAccessInvite invite, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(invite, cancellationToken) > 0; + } + + public IEnumerable GetActive(string email) + { + return _context + .AccountAccessInvites + .Include(x => x.Account) + .Where(x => x.Email == email && !x.AcceptedOn.HasValue && !x.RejectedOn.HasValue); + + } + + public async Task> GetActiveAsync(string email, CancellationToken cancellationToken = default) + { + return await _context + .AccountAccessInvites + .Include(x => x.Account) + .Where(x => x.Email == email && !x.AcceptedOn.HasValue && !x.RejectedOn.HasValue) + .ToListAsync(cancellationToken); + } + + public AccountAccessInvite? Get(Guid id) + { + return _context.AccountAccessInvites.FirstOrDefault(x => x.Id == id); + } + + public async Task GetAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.AccountAccessInvites.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/AccountAccessRepository.cs b/MyOffice.Data.Repositories/Account/AccountAccessRepository.cs new file mode 100644 index 0000000..be14bbe --- /dev/null +++ b/MyOffice.Data.Repositories/Account/AccountAccessRepository.cs @@ -0,0 +1,42 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; +using MyOffice.Data.Repositories; +using MyOffice.DbContext; + +public class AccountAccessRepository : AppRepository, IAccountAccessRepository +{ + public AccountAccessRepository(AppDbContext context) : base(context) + { + } + + public bool Add(AccountAccess accountAccess) + { + return AddBase(accountAccess) > 0; + } + + public async Task AddAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(accountAccess, cancellationToken) > 0; + } + + public bool Update(AccountAccess accountAccess) + { + return UpdateBase(accountAccess) > 0; + } + + public async Task UpdateAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(accountAccess, cancellationToken) > 0; + } + + public bool Delete(AccountAccess accountAccess) + { + return RemoveBase(accountAccess) > 0; + } + + public async Task DeleteAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(accountAccess, cancellationToken) > 0; + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/AccountAccountCategoryRepository.cs b/MyOffice.Data.Repositories/Account/AccountAccountCategoryRepository.cs new file mode 100644 index 0000000..9fdbf54 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/AccountAccountCategoryRepository.cs @@ -0,0 +1,55 @@ +namespace MyOffice.Data.Repositories.Account; + +using Microsoft.EntityFrameworkCore; +using Models.Accounts; +using MyOffice.DbContext; + +public class AccountAccountCategoryRepository : AppRepository, IAccountAccountCategoryRepository +{ + public AccountAccountCategoryRepository(AppDbContext context) : base(context) + { + } + + public List Get(Guid userId, Guid accountId, Guid categoryId) + { + return _context.AccountAccountCategories + .Include(x => x.Account) + .Include(x => x.Category) + .Where(x => x.AccountId == accountId && x.CategoryId == categoryId && x.Category.UserId == userId) + .ToList(); + } + + public async Task> GetAsync( + Guid userId, + Guid accountId, + Guid categoryId, + CancellationToken cancellationToken = default + ) + { + return await _context.AccountAccountCategories + .Include(x => x.Account) + .Include(x => x.Category) + .Where(x => x.AccountId == accountId && x.CategoryId == categoryId && x.Category.UserId == userId) + .ToListAsync(cancellationToken); + } + + public bool Remove(AccountAccountCategory accountAccountCategory) + { + return RemoveBase(accountAccountCategory) > 0; + } + + public async Task RemoveAsync(AccountAccountCategory accountAccountCategory, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(accountAccountCategory, cancellationToken) > 0; + } + + public bool Add(AccountAccountCategory accountAccountCategory) + { + return AddBase(accountAccountCategory) > 0; + } + + public async Task AddAsync(AccountAccountCategory accountAccountCategory, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(accountAccountCategory, cancellationToken) > 0; + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/AccountCategoryRepository.cs b/MyOffice.Data.Repositories/Account/AccountCategoryRepository.cs new file mode 100644 index 0000000..f1ec7e1 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/AccountCategoryRepository.cs @@ -0,0 +1,73 @@ +namespace MyOffice.Data.Repositories.Account; + +using Microsoft.EntityFrameworkCore; +using Models.Accounts; +using MyOffice.Data.Repositories; +using MyOffice.DbContext; + +public class AccountCategoryRepository : AppRepository, IAccountCategoryRepository +{ + public AccountCategoryRepository(AppDbContext context) : base(context) + { + } + + public List GetAll(Guid userId) + { + return _context.AccountCategories + .Include(x => x.Accounts) + .Where(x => x.UserId == userId) + .ToList(); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.AccountCategories + .Include(x => x.Accounts) + .Where(x => x.UserId == userId) + .ToListAsync(cancellationToken); + } + + public bool Add(AccountCategory accountCategory) + { + return AddBase(accountCategory) > 0; + } + + public async Task AddAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(accountCategory, cancellationToken) > 0; + } + + public AccountCategory? Get(Guid userId, Guid id) + { + return _context.AccountCategories + .Include(x => x.Accounts) + .FirstOrDefault(x => x.Id == id && x.UserId == userId); + } + + public async Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default) + { + return await _context.AccountCategories + .Include(x => x.Accounts) + .FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId, cancellationToken); + } + + public bool Update(AccountCategory accountCategory) + { + return UpdateBase(accountCategory) > 0; + } + + public async Task UpdateAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(accountCategory, cancellationToken) > 0; + } + + public bool Remove(AccountCategory accountCategory) + { + return RemoveBase(accountCategory) > 0; + } + + public async Task RemoveAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(accountCategory, cancellationToken) > 0; + } +} diff --git a/MyOffice.Data.Repositories/Account/AccountRepository.cs b/MyOffice.Data.Repositories/Account/AccountRepository.cs new file mode 100644 index 0000000..f09eaf4 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/AccountRepository.cs @@ -0,0 +1,641 @@ +namespace MyOffice.Data.Repositories.Account; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Models.Accounts; +using MyOffice.Data.Models.Accounts.Domain; +using MyOffice.Data.Models.Currencies; +using MyOffice.Data.Repositories; +using MyOffice.DbContext; + +public class AccountRepository : AppRepository, IAccountRepository +{ + private readonly ILogger _logger; + + public AccountRepository( + AppDbContext context, + ILogger logger + ) : base(context) + { + _logger = logger; + } + + public List GetAll(Guid userId) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + // categories created with current user + .Include(x => x.Categories!.Where(c => c.Category.UserId == userId))! + .ThenInclude(x => x.Category) + // access rights created with current user + .Include(x => x.AccessRights!.Where(a => a.OwnerId == userId))! + .ThenInclude(x => x.User) + // require only one motions to check if can to delete account + .Include(x => x.Motions!.Take(1))! + // only accounts with access to current user + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .ToList(); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.Accounts! + .Include(x => x.CurrencyGlobal) + // categories created with current user + .Include(x => x.Categories!.Where(c => c.Category.UserId == userId))! + .ThenInclude(x => x.Category) + // access rights created with current user + .Include(x => x.AccessRights!.Where(a => a.OwnerId == userId))! + .ThenInclude(x => x.User) + // require only one motions to check if can to delete account + .Include(x => x.Motions!.Take(1))! + // only accounts with access to current user + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .ToListAsync(cancellationToken); + } + + public List GetByCategory(Guid userId, Guid categoryId) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Include(x => x.Motions)! + .Where(x => x.Categories!.Any(c => c.CategoryId == categoryId) && x.AccessRights!.Any(a => a.UserId == userId)) + .ToList(); + } + + public async Task> GetByCategoryAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default) + { + return await _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Include(x => x.Motions)! + .Where(x => x.Categories!.Any(c => c.CategoryId == categoryId) && x.AccessRights!.Any(a => a.UserId == userId)) + .ToListAsync(cancellationToken); + } + + public List GetByCategoryDetailed(Guid userId, Guid categoryId) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Where(x => x.Categories!.Any(c => c.CategoryId == categoryId) && x.AccessRights!.Any(a => a.UserId == userId)) + .Select(x => new AccountDetailed + { + Account = x, + TotalPlus = x.Motions!.Sum(m => m.AmountPlus), + TotalMinus = x.Motions!.Sum(m => m.AmountMinus), + }) + .ToList(); + } + + public Task> GetByCategoryDetailedAsync( + Guid userId, + Guid categoryId, + CancellationToken cancellationToken = default + ) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Where(x => x.Categories!.Any(c => c.CategoryId == categoryId) && x.AccessRights!.Any(a => a.UserId == userId)) + .Select(x => new AccountDetailed + { + Account = x, + TotalPlus = x.Motions!.Sum(m => m.AmountPlus), + TotalMinus = x.Motions!.Sum(m => m.AmountMinus), + }) + .ToListAsync(cancellationToken); + } + + public AccountDetailed? GetByIdDetailed(Guid userId, Guid id) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Where(x => x.Id == id && x.AccessRights!.Any(a => a.UserId == userId)) + .Select(x => new AccountDetailed + { + Account = x, + TotalPlus = x.Motions!.Sum(m => m.AmountPlus), + TotalMinus = x.Motions!.Sum(m => m.AmountMinus), + }) + .FirstOrDefault(); + } + + public Task GetByIdDetailedAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default + ) + { + return _context.Accounts! + .Include(x => x.CurrencyGlobal) + .Include(x => x.Categories)! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .Where(x => x.Id == id && x.AccessRights!.Any(a => a.UserId == userId)) + .Select(x => new AccountDetailed + { + Account = x, + TotalPlus = x.Motions!.Sum(m => m.AmountPlus), + TotalMinus = x.Motions!.Sum(m => m.AmountMinus), + }) + .FirstOrDefaultAsync(cancellationToken); + } + + public bool Add(Account account) + { + return AddBase(account) > 0; + } + + public async Task AddAsync(Account account, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(account, cancellationToken) > 0; + } + + public bool Delete(Account account) + { + return RemoveBase(account) > 0; + } + + public async Task DeleteAsync(Account account, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(account, cancellationToken) > 0; + } + + public Account? Get(Guid userId, Guid id) + { + return _context.Accounts! + .Include(x => x.Categories!.Where(c => c.Category.UserId == userId))! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + //.Include(x => x.Motions)! + .FirstOrDefault(x => x.Id == id && x.AccessRights!.Any(r => r.UserId == userId)); + } + + public Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default) + { + return _context.Accounts! + .Include(x => x.Categories!.Where(c => c.Category.UserId == userId))! + .ThenInclude(x => x.Category) + .Include(x => x.AccessRights)! + .ThenInclude(x => x.User) + .FirstOrDefaultAsync( + x => x.Id == id && x.AccessRights!.Any(r => r.UserId == userId), + cancellationToken); + } + + public bool Update(Account account) + { + return UpdateBase(account) > 0; + } + + public async Task UpdateAsync(Account account, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(account, cancellationToken) > 0; + } + + public bool Remove(Account account) + { + return RemoveBase(account) > 0; + } + + public List FindAccounts(Guid userId, string term) + { + return _context.Accounts + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .Where(x => x.Name.ToLower().Contains(term.ToLower())) + .ToList(); + } + + public async Task> FindAccountsAsync(Guid userId, string term, CancellationToken cancellationToken = default) + { + return await _context.Accounts + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .Where(x => x.Name.ToLower().Contains(term.ToLower())) + .ToListAsync(cancellationToken); + } + + public List GetAccountsWithRate(Guid userId, DateTime date) + { + var accounts = _context.Accounts + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .ToList(); + + var currencyIds = accounts + .Select(x => x.CurrencyGlobalId) + .ToList(); + + var rates = _context.CurrencyRates + .Include(x => x.Currency) + .Where(x => x.Currency!.UserId == userId) + .Where(x => x.DateTime <= date) + .Where(x => currencyIds.Contains(x.Currency!.CurrencyGlobalId)) + .GroupBy(x => x.Currency!.CurrencyGlobalId) + .Select(x => x.OrderByDescending(r => r.DateTime).First()) + .ToList(); + + return accounts + .Select(account => + { + var rate = rates.FirstOrDefault(rate => rate.Currency!.CurrencyGlobalId == account.CurrencyGlobalId); + + var result = new AccountWithRate + { + Account = account, + Currency = rate?.Currency, + Rate = rate, + }; + + return result; + }) + .ToList(); + } + + public List GetRestAtDate(Guid userId, DateTime date) + { + return _context.AccountAccesses! + .Where(x => x.UserId == userId) + .Include(x => x.Account!) + .ThenInclude(x => x.CurrencyGlobal!) + .ThenInclude(x => x.Currencies!) + .ThenInclude(x => x.CurrentRate!) + .Include(x => x.Account) + .ThenInclude(x => x!.Motions) + .Select(x => new + { + Id = x.AccountId, + Name = x.Account!.Name, + Type = x.Type, + Currency = x.Account!.CurrencyGlobal!.Currencies!.FirstOrDefault(c => c.UserId == userId), + CurrentRate = x.Account!.CurrencyGlobal!.Currencies!.FirstOrDefault(c => c.UserId == userId)!.CurrentRate, + Plus = x.Account.Motions! + .Where(x => !x.DeletedOn.HasValue) + .Sum(m => m.AmountPlus), + Minus = x.Account.Motions! + .Where(x => !x.DeletedOn.HasValue) + .Sum(m => m.AmountMinus), + }) + .ToList() + .Select(x => new AccountSimple + { + Id = x.Id, + Name = x.Name, + Type = x.Type, + CurrencyName = x.Currency?.Name, + CurrencyShortName = x.Currency?.ShortName, + CurrencyRate = x.CurrentRate?.Rate, + CurrencyQuantity = x.CurrentRate?.Quantity, + TotalMinus = x.Minus, + TotalPlus = x.Plus, + Balance = x.Plus - x.Minus, + }) + .ToList(); + } + + public List GetIncomeByCategories(Guid userId, DateTime from, DateTime to) + { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new + { + Id = x.Item.CategoryId, + Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .ToList() + .Select(x => new MotionTotalSimple + { + Id = x.Key.IsUserCategory ? x.Key.Id : userId, + Name = x.Key.Name, + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountPlus), + }) + .ToList(); + } + + public List GetIncomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.Category!.UserId == userId) + .Where(x => x.Item.CategoryId == categoryId) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.Category!.UserId == userId) + .GroupBy(x => new + { + Id = x.Item.ItemGlobalId, + Name = x.Item.ItemGlobal!.Name, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + Name = x.Key.Name, + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountPlus), + }) + .ToList(); + } + + + public List GetOutcomeByCategories(Guid userId, DateTime from, DateTime to) + { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new + { + Id = x.Item.CategoryId, + Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + Id = x.Key.IsUserCategory ? x.Key.Id : userId, + Name = x.Key.IsUserCategory ? x.Key.Name : "UnCategorized", + //Name = x.Key.Name, + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountMinus), + }).ToList(); + } + + + public List GetOutcomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) + { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.CategoryId == categoryId) + .GroupBy(x => new + { + Id = x.Item.ItemGlobalId, + Name = x.Item.ItemGlobal!.Name, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountMinus), + }) + .ToList(); + } + + public async Task> GetAccountsWithRateAsync( + Guid userId, + DateTime date, + CancellationToken cancellationToken = default + ) + { + var accounts = await _context.Accounts + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .ToListAsync(cancellationToken); + + var currencyIds = accounts + .Select(x => x.CurrencyGlobalId) + .ToList(); + + var rates = await _context.CurrencyRates + .Include(x => x.Currency) + .Where(x => x.Currency!.UserId == userId) + .Where(x => x.DateTime <= date) + .Where(x => currencyIds.Contains(x.Currency!.CurrencyGlobalId)) + .GroupBy(x => x.Currency!.CurrencyGlobalId) + .Select(x => x.OrderByDescending(r => r.DateTime).First()) + .ToListAsync(cancellationToken); + + return accounts + .Select(account => + { + var rate = rates.FirstOrDefault(r => r.Currency!.CurrencyGlobalId == account.CurrencyGlobalId); + return new AccountWithRate + { + Account = account, + Currency = rate?.Currency, + Rate = rate, + }; + }) + .ToList(); + } + + public async Task> GetRestAtDateAsync( + Guid userId, + DateTime date, + CancellationToken cancellationToken = default + ) + { + var rows = await _context.AccountAccesses! + .Where(x => x.UserId == userId) + .Include(x => x.Account!) + .ThenInclude(x => x.CurrencyGlobal!) + .ThenInclude(x => x.Currencies!) + .ThenInclude(x => x.CurrentRate!) + .Include(x => x.Account) + .ThenInclude(x => x!.Motions) + .Select(x => new + { + Id = x.AccountId, + Name = x.Account!.Name, + Type = x.Type, + Currency = x.Account!.CurrencyGlobal!.Currencies!.FirstOrDefault(c => c.UserId == userId), + CurrentRate = x.Account!.CurrencyGlobal!.Currencies!.FirstOrDefault(c => c.UserId == userId)!.CurrentRate, + Plus = x.Account.Motions! + .Where(m => !m.DeletedOn.HasValue) + .Sum(m => m.AmountPlus), + Minus = x.Account.Motions! + .Where(m => !m.DeletedOn.HasValue) + .Sum(m => m.AmountMinus), + }) + .ToListAsync(cancellationToken); + + return rows + .Select(x => new AccountSimple + { + Id = x.Id, + Name = x.Name, + Type = x.Type, + CurrencyName = x.Currency?.Name, + CurrencyShortName = x.Currency?.ShortName, + CurrencyRate = x.CurrentRate?.Rate, + CurrencyQuantity = x.CurrentRate?.Quantity, + TotalMinus = x.Minus, + TotalPlus = x.Plus, + Balance = x.Plus - x.Minus, + }) + .ToList(); + } + + public async Task> GetIncomeByCategoriesAsync( + Guid userId, + DateTime from, + DateTime to, + CancellationToken cancellationToken = default + ) + { + var accounts = await GetAccountsWithRateAsync(userId, to, cancellationToken); + var accountIds = accounts.Select(x => x.Account.Id).ToList(); + + var groups = await _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new + { + Id = x.Item.CategoryId, + Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .ToListAsync(cancellationToken); + + return groups + .Select(x => new MotionTotalSimple + { + Id = x.Key.IsUserCategory ? x.Key.Id : userId, + Name = x.Key.Name, + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountPlus), + }) + .ToList(); + } + + public Task> GetIncomeByCategoryAsync( + Guid userId, + Guid categoryId, + DateTime from, + DateTime to, + CancellationToken cancellationToken = default + ) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.Category!.UserId == userId) + .Where(x => x.Item.CategoryId == categoryId) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.Category!.UserId == userId) + .GroupBy(x => new + { + Id = x.Item.ItemGlobalId, + Name = x.Item.ItemGlobal!.Name, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + Name = x.Key.Name, + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountPlus), + }) + .ToListAsync(cancellationToken); + } + + public async Task> GetOutcomeByCategoriesAsync( + Guid userId, + DateTime from, + DateTime to, + CancellationToken cancellationToken = default + ) + { + var accounts = await GetAccountsWithRateAsync(userId, to, cancellationToken); + var accountIds = accounts.Select(x => x.Account.Id).ToList(); + + return await _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new + { + Id = x.Item.CategoryId, + Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + Id = x.Key.IsUserCategory ? x.Key.Id : userId, + Name = x.Key.IsUserCategory ? x.Key.Name : "UnCategorized", + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Amount = x.Sum(a => a.AmountMinus), + }) + .ToListAsync(cancellationToken); + } + + public async Task> GetOutcomeByCategoryAsync( + Guid userId, + Guid categoryId, + DateTime from, + DateTime to, + CancellationToken cancellationToken = default + ) + { + var accounts = await GetAccountsWithRateAsync(userId, to, cancellationToken); + var accountIds = accounts.Select(x => x.Account.Id).ToList(); + + return await _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => accountIds.Contains(x.AccountId)) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.CategoryId == categoryId) + .GroupBy(x => new + { + Id = x.Item.ItemGlobalId, + Name = x.Item.ItemGlobal!.Name, + CurrencyId = x.Account.CurrencyGlobalId, + CurrencyName = x.Account.CurrencyGlobal!.Name, + }) + .Select(x => new MotionTotalSimple + { + CurrencyId = x.Key.CurrencyId, + CurrencyName = x.Key.CurrencyName, + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountMinus), + }) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IAccountAccessInviteRepository.cs b/MyOffice.Data.Repositories/Account/IAccountAccessInviteRepository.cs new file mode 100644 index 0000000..ac0a50c --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IAccountAccessInviteRepository.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IAccountAccessInviteRepository +{ + AccountAccessInvite? Get(Guid userId, string email); + Task GetAsync(Guid userId, string email, CancellationToken cancellationToken = default); + IEnumerable GetActive(string email); + Task> GetActiveAsync(string email, CancellationToken cancellationToken = default); + AccountAccessInvite? Get(Guid id); + Task GetAsync(Guid id, CancellationToken cancellationToken = default); + bool Add(AccountAccessInvite invite); + Task AddAsync(AccountAccessInvite invite, CancellationToken cancellationToken = default); + bool Update(AccountAccessInvite invite); + Task UpdateAsync(AccountAccessInvite invite, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IAccountAccessRepository.cs b/MyOffice.Data.Repositories/Account/IAccountAccessRepository.cs new file mode 100644 index 0000000..47e7c95 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IAccountAccessRepository.cs @@ -0,0 +1,13 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IAccountAccessRepository +{ + bool Add(AccountAccess accountAccess); + Task AddAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default); + bool Update(AccountAccess accountAccess); + Task UpdateAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default); + bool Delete(AccountAccess accountAccess); + Task DeleteAsync(AccountAccess accountAccess, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IAccountAccountCategoryRepository.cs b/MyOffice.Data.Repositories/Account/IAccountAccountCategoryRepository.cs new file mode 100644 index 0000000..1f2d59c --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IAccountAccountCategoryRepository.cs @@ -0,0 +1,13 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IAccountAccountCategoryRepository +{ + List Get(Guid userId, Guid accountId, Guid categoryId); + Task> GetAsync(Guid userId, Guid accountId, Guid categoryId, CancellationToken cancellationToken = default); + bool Remove(AccountAccountCategory accountAccountCategory); + Task RemoveAsync(AccountAccountCategory accountAccountCategory, CancellationToken cancellationToken = default); + bool Add(AccountAccountCategory accountAccountCategory); + Task AddAsync(AccountAccountCategory accountAccountCategory, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IAccountCategoryRepository.cs b/MyOffice.Data.Repositories/Account/IAccountCategoryRepository.cs new file mode 100644 index 0000000..24903f2 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IAccountCategoryRepository.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IAccountCategoryRepository +{ + List GetAll(Guid userId); + Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default); + bool Add(AccountCategory accountCategory); + Task AddAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default); + AccountCategory? Get(Guid userId, Guid id); + Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + bool Update(AccountCategory accountCategory); + Task UpdateAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default); + bool Remove(AccountCategory accountCategory); + Task RemoveAsync(AccountCategory accountCategory, CancellationToken cancellationToken = default); +} diff --git a/MyOffice.Data.Repositories/Account/IAccountyRepository.cs b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs new file mode 100644 index 0000000..5d7345d --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs @@ -0,0 +1,37 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IAccountRepository +{ + List GetAll(Guid userId); + Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default); + List GetByCategory(Guid userId, Guid categoryId); + Task> GetByCategoryAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default); + List GetByCategoryDetailed(Guid userId, Guid categoryId); + Task> GetByCategoryDetailedAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default); + AccountDetailed? GetByIdDetailed(Guid userId, Guid id); + Task GetByIdDetailedAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + bool Add(Account account); + Task AddAsync(Account account, CancellationToken cancellationToken = default); + bool Delete(Account account); + Task DeleteAsync(Account account, CancellationToken cancellationToken = default); + Account? Get(Guid userId, Guid id); + Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + bool Update(Account account); + Task UpdateAsync(Account account, CancellationToken cancellationToken = default); + bool Remove(Account account); + List FindAccounts(Guid userId, string term); + Task> FindAccountsAsync(Guid userId, string term, CancellationToken cancellationToken = default); + + List GetRestAtDate(Guid userId, DateTime date); + Task> GetRestAtDateAsync(Guid userId, DateTime date, CancellationToken cancellationToken = default); + List GetIncomeByCategories(Guid userId, DateTime from, DateTime to); + Task> GetIncomeByCategoriesAsync(Guid userId, DateTime from, DateTime to, CancellationToken cancellationToken = default); + List GetIncomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to); + Task> GetIncomeByCategoryAsync(Guid userId, Guid categoryId, DateTime from, DateTime to, CancellationToken cancellationToken = default); + List GetOutcomeByCategories(Guid userId, DateTime from, DateTime to); + Task> GetOutcomeByCategoriesAsync(Guid userId, DateTime from, DateTime to, CancellationToken cancellationToken = default); + List GetOutcomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to); + Task> GetOutcomeByCategoryAsync(Guid userId, Guid categoryId, DateTime from, DateTime to, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IMotionRepository.cs b/MyOffice.Data.Repositories/Account/IMotionRepository.cs new file mode 100644 index 0000000..2cc664d --- /dev/null +++ b/MyOffice.Data.Repositories/Account/IMotionRepository.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Data.Repositories.Account; + +using Models.Accounts; + +public interface IMotionRepository +{ + Task AddAsync(Motion motion, CancellationToken cancellationToken = default); + Task> GetByAccountAsync(Guid accountId, DateTime dateFrom, DateTime dateTo, CancellationToken cancellationToken = default); + Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + Task UpdateAsync(Motion motion, CancellationToken cancellationToken = default); + Task RemoveAsync(Motion motion, CancellationToken cancellationToken = default); +} diff --git a/MyOffice.Data.Repositories/Account/MotionRepository.cs b/MyOffice.Data.Repositories/Account/MotionRepository.cs new file mode 100644 index 0000000..3d2ceb5 --- /dev/null +++ b/MyOffice.Data.Repositories/Account/MotionRepository.cs @@ -0,0 +1,55 @@ +namespace MyOffice.Data.Repositories.Account; + +using Microsoft.EntityFrameworkCore; +using Models.Accounts; +using MyOffice.DbContext; + +public class MotionRepository : AppRepository, IMotionRepository +{ + public MotionRepository(AppDbContext context) : base(context) + { + } + + public async Task AddAsync(Motion motion, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(motion, cancellationToken) > 0; + } + + public async Task> GetByAccountAsync( + Guid accountId, + DateTime dateFrom, + DateTime dateTo, + CancellationToken cancellationToken = default + ) + { + return await _context + .Motions + .Include(x => x.Item) + .ThenInclude(x => x.ItemGlobal) + .Where(x => x.AccountId == accountId && x.DateTime >= dateFrom && x.DateTime <= dateTo) + .OrderByDescending(x => x.DateTime) + .ThenByDescending(x => x.CreatedOn) + .ToListAsync(cancellationToken); + } + + public async Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default) + { + return await _context + .Motions + .Include(x => x.Item) + .ThenInclude(x => x.ItemGlobal) + .FirstOrDefaultAsync( + x => x.Account!.AccessRights!.Any(a => a.UserId == userId) && x.Id == id, + cancellationToken); + } + + public async Task UpdateAsync(Motion motion, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(motion, cancellationToken) > 0; + } + + public async Task RemoveAsync(Motion motion, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(motion, cancellationToken) > 0; + } +} diff --git a/MyOffice.Data.Repositories/AppRepository.cs b/MyOffice.Data.Repositories/AppRepository.cs new file mode 100644 index 0000000..84e7481 --- /dev/null +++ b/MyOffice.Data.Repositories/AppRepository.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Data.Repositories; + +using DbContext; + +public class AppRepository : RepositoryBase where TEntity : class +{ + public AppRepository(AppDbContext context) : base(context) + { + } +} diff --git a/MyOffice.Data.Repositories/Currency/CurrencyGlobalRepository.cs b/MyOffice.Data.Repositories/Currency/CurrencyGlobalRepository.cs new file mode 100644 index 0000000..1eda5bf --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/CurrencyGlobalRepository.cs @@ -0,0 +1,22 @@ +namespace MyOffice.Data.Repositories.Currency; + +using Microsoft.EntityFrameworkCore; +using Models.Currencies; +using MyOffice.DbContext; + +public class CurrencyGlobalRepository : AppRepository, ICurrencyGlobalRepository +{ + public CurrencyGlobalRepository(AppDbContext context) : base(context) + { + } + + public List GetAll() + { + return _context.CurrencyGlobals.ToList(); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _context.CurrencyGlobals.ToListAsync(cancellationToken); + } +} diff --git a/MyOffice.Data.Repositories/Currency/CurrencyRateRepository.cs b/MyOffice.Data.Repositories/Currency/CurrencyRateRepository.cs new file mode 100644 index 0000000..72b43be --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/CurrencyRateRepository.cs @@ -0,0 +1,115 @@ +namespace MyOffice.Data.Repositories.Currency; + +using System.Xml.Schema; +using Microsoft.EntityFrameworkCore; +using Models.Currencies; +using MyOffice.DbContext; + +public class CurrencyRateRepository : AppRepository, ICurrencyRateRepository +{ + public CurrencyRateRepository(AppDbContext context) : base(context) + { + } + + public List GetLastRates(Guid currencyId, DateTime? before = null, int count = 1) + { + before = before ?? DateTime.UtcNow; + + return _context.CurrencyRates + .Where(x => x.CurrencyId == currencyId) + .Where(x => x.DateTime <= before) + .OrderByDescending(x => x.DateTime) + .ThenByDescending(x => x.Id) + .Take(count) + .ToList(); + } + + public async Task> GetLastRatesAsync( + Guid currencyId, + DateTime? before = null, + int count = 1, + CancellationToken cancellationToken = default) + { + before ??= DateTime.UtcNow; + + return await _context.CurrencyRates + .Where(x => x.CurrencyId == currencyId) + .Where(x => x.DateTime <= before) + .OrderByDescending(x => x.DateTime) + .ThenByDescending(x => x.Id) + .Take(count) + .ToListAsync(cancellationToken); + } + + public Dictionary GetLastRates(Guid userId, List currencyIds, DateTime? before = null) + { + return _context.CurrencyRates + .Include(x => x.Currency) + .Include(x => x.Currency!.CurrencyGlobal) + .Where(x => x.DateTime <= before) + .Where(x => x.Currency!.UserId == userId) + .Where(x => currencyIds.Contains(x.Currency!.CurrencyGlobalId)) + .GroupBy(x => new + { + x.Currency!.CurrencyGlobalId + }, (key, g) => new + { + key.CurrencyGlobalId, + rate = g.OrderByDescending(x => x.DateTime).First() + }) + .ToDictionary(x => x.CurrencyGlobalId, x => x.rate); + } + + public async Task> GetLastRatesAsync( + Guid userId, + List currencyIds, + DateTime? before = null, + CancellationToken cancellationToken = default + ) + { + var rows = await _context.CurrencyRates + .Include(x => x.Currency) + .Include(x => x.Currency!.CurrencyGlobal) + .Where(x => x.DateTime <= before) + .Where(x => x.Currency!.UserId == userId) + .Where(x => currencyIds.Contains(x.Currency!.CurrencyGlobalId)) + .GroupBy(x => new + { + x.Currency!.CurrencyGlobalId + }, (key, g) => new + { + key.CurrencyGlobalId, + rate = g.OrderByDescending(x => x.DateTime).First() + }) + .ToListAsync(cancellationToken); + + return rows.ToDictionary(x => x.CurrencyGlobalId, x => x.rate); + } + + public bool AddRate(CurrencyRate currencyRate) + { + return this.AddBase(currencyRate) > 0; + } + + public async Task AddRateAsync(CurrencyRate currencyRate, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(currencyRate, cancellationToken) > 0; + } + + public List GetAtDate(Guid currencyId, DateTime date) + { + return _context.CurrencyRates + .Where(x => x.CurrencyId == currencyId && x.DateTime == date) + .ToList(); + } + + public async Task> GetAtDateAsync( + Guid currencyId, + DateTime date, + CancellationToken cancellationToken = default) + { + return await _context.CurrencyRates + .Where(x => x.CurrencyId == currencyId && x.DateTime == date) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Currency/CurrencyRepository.cs b/MyOffice.Data.Repositories/Currency/CurrencyRepository.cs new file mode 100644 index 0000000..77d12bb --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/CurrencyRepository.cs @@ -0,0 +1,102 @@ +namespace MyOffice.Data.Repositories.Currency; + +using Microsoft.EntityFrameworkCore; +using Models.Currencies; +using MyOffice.Data.Repositories; +using MyOffice.DbContext; + +public class CurrencyRepository : AppRepository, ICurrencyRepository +{ + public CurrencyRepository(AppDbContext context) : base(context) + { + } + + public List GetAll(Guid userId) + { + return _context.Currencies + .Include(x => x.CurrencyGlobal) + .Where(x => x.UserId == userId) + .ToList(); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.Currencies + .Include(x => x.CurrencyGlobal) + .Where(x => x.UserId == userId) + .ToListAsync(cancellationToken); + } + + public Currency? Get(Guid userId, Guid id) + { + return _context + .Currencies + .Include(x => x.CurrencyGlobal) + .FirstOrDefault(x => x.Id == id && x.UserId == userId); + } + + public async Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default) + { + return await _context + .Currencies + .Include(x => x.CurrencyGlobal) + .FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId, cancellationToken); + } + + public Currency? GetByGlobalCurrency(Guid userId, string globalCurrencyId) + { + return _context.Currencies.FirstOrDefault(x => x.UserId == userId && x.CurrencyGlobalId == globalCurrencyId); + } + + public async Task GetByGlobalCurrencyAsync( + Guid userId, + string globalCurrencyId, + CancellationToken cancellationToken = default) + { + return await _context.Currencies.FirstOrDefaultAsync( + x => x.UserId == userId && x.CurrencyGlobalId == globalCurrencyId, + cancellationToken); + } + + public bool Add(Currency currency) + { + return AddBase(currency) > 0; + } + + public async Task AddAsync(Currency currency, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(currency, cancellationToken) > 0; + } + + public bool Update(Currency currency) + { + return UpdateBase(currency) > 0; + } + + public async Task UpdateAsync(Currency currency, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(currency, cancellationToken) > 0; + } + + public bool Remove(Currency currency) + { + return RemoveBase(currency) > 0; + } + + public async Task RemoveAsync(Currency currency, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(currency, cancellationToken) > 0; + } + + public List GetPrimaries(Guid userId) + { + return _context.Currencies.Where(x => x.UserId == userId && x.IsPrimary).ToList(); + } + + public async Task> GetPrimariesAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.Currencies + .Where(x => x.UserId == userId && x.IsPrimary) + .ToListAsync(cancellationToken); + } +} diff --git a/MyOffice.Data.Repositories/Currency/ICurrencyGlobalRepository.cs b/MyOffice.Data.Repositories/Currency/ICurrencyGlobalRepository.cs new file mode 100644 index 0000000..e2b75ce --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/ICurrencyGlobalRepository.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Data.Repositories.Currency; + +using Models.Currencies; + +public interface ICurrencyGlobalRepository +{ + List GetAll(); + Task> GetAllAsync(CancellationToken cancellationToken = default); +} diff --git a/MyOffice.Data.Repositories/Currency/ICurrencyRateRepository.cs b/MyOffice.Data.Repositories/Currency/ICurrencyRateRepository.cs new file mode 100644 index 0000000..d54bb0e --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/ICurrencyRateRepository.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Data.Repositories.Currency; + +using Models.Currencies; + +public interface ICurrencyRateRepository +{ + List GetLastRates(Guid currencyId, DateTime? before = null, int count = 1); + Task> GetLastRatesAsync(Guid currencyId, DateTime? before = null, int count = 1, CancellationToken cancellationToken = default); + Dictionary GetLastRates(Guid userId, List currencyIds, DateTime? before = null); + Task> GetLastRatesAsync( + Guid userId, + List currencyIds, + DateTime? before = null, + CancellationToken cancellationToken = default); + bool AddRate(CurrencyRate currencyRate); + Task AddRateAsync(CurrencyRate currencyRate, CancellationToken cancellationToken = default); + List GetAtDate(Guid currencyId, DateTime date); + Task> GetAtDateAsync(Guid currencyId, DateTime date, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Currency/ICurrencyRepository.cs b/MyOffice.Data.Repositories/Currency/ICurrencyRepository.cs new file mode 100644 index 0000000..ebb3ce1 --- /dev/null +++ b/MyOffice.Data.Repositories/Currency/ICurrencyRepository.cs @@ -0,0 +1,21 @@ +namespace MyOffice.Data.Repositories.Currency; + +using Models.Currencies; + +public interface ICurrencyRepository +{ + List GetAll(Guid userId); + Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default); + Currency? Get(Guid userId, Guid id); + Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + Currency? GetByGlobalCurrency(Guid userId, string globalCurrencyId); + Task GetByGlobalCurrencyAsync(Guid userId, string globalCurrencyId, CancellationToken cancellationToken = default); + bool Add(Currency currency); + Task AddAsync(Currency currency, CancellationToken cancellationToken = default); + bool Update(Currency currency); + Task UpdateAsync(Currency currency, CancellationToken cancellationToken = default); + bool Remove(Currency currency); + Task RemoveAsync(Currency currency, CancellationToken cancellationToken = default); + List GetPrimaries(Guid userId); + Task> GetPrimariesAsync(Guid userId, CancellationToken cancellationToken = default); +} diff --git a/MyOffice.Data.Repositories/Item/IItemCategoryRepository.cs b/MyOffice.Data.Repositories/Item/IItemCategoryRepository.cs new file mode 100644 index 0000000..ced8cea --- /dev/null +++ b/MyOffice.Data.Repositories/Item/IItemCategoryRepository.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Repositories.Item; + +using MyOffice.Data.Models.Items; + +public interface IItemCategoryRepository +{ + List GetAll(Guid userId); + Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default); + bool Add(ItemCategory accountCategory); + Task AddAsync(ItemCategory accountCategory, CancellationToken cancellationToken = default); + ItemCategory? Get(Guid userId, Guid id); + Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default); + bool Update(ItemCategory accountCategory); + Task UpdateAsync(ItemCategory accountCategory, CancellationToken cancellationToken = default); + bool Remove(ItemCategory accountCategory); + Task RemoveAsync(ItemCategory accountCategory, CancellationToken cancellationToken = default); +} diff --git a/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs b/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs new file mode 100644 index 0000000..1dcd4a8 --- /dev/null +++ b/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs @@ -0,0 +1,14 @@ +namespace MyOffice.Data.Repositories.Item; + +using MyOffice.Data.Models.Items; + +public interface IItemGlobalRepository +{ + ItemGlobal? Get(Guid id); + ItemGlobal? GetByName(string name); + Task GetByNameAsync(string name, CancellationToken cancellationToken = default); + bool Add(ItemGlobal itemGlobal); + Task AddAsync(ItemGlobal itemGlobal, CancellationToken cancellationToken = default); + List GetAvailableToUser(Guid userId); + Task> GetAvailableToUserAsync(Guid userId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Item/IItemRepository.cs b/MyOffice.Data.Repositories/Item/IItemRepository.cs new file mode 100644 index 0000000..2f98f3a --- /dev/null +++ b/MyOffice.Data.Repositories/Item/IItemRepository.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Data.Repositories.Item; + +using MyOffice.Data.Models.Items; + +public interface IItemRepository +{ + List GetAll(Guid userId); + Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default); + List GetByCategory(Guid userId, Guid categoryId); + Task> GetByCategoryAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default); + Item? GetByGlobal(Guid userId, Guid globalItemId); + Task GetByGlobalAsync(Guid userId, Guid globalItemId, CancellationToken cancellationToken = default); + bool Add(Item item); + Task AddAsync(Item item, CancellationToken cancellationToken = default); + bool Update(Item item); + Task UpdateAsync(Item item, CancellationToken cancellationToken = default); + List Find(Guid userId, string term, int limit); + Task> FindAsync(Guid userId, string term, int limit, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Item/ItemCategoryRepository.cs b/MyOffice.Data.Repositories/Item/ItemCategoryRepository.cs new file mode 100644 index 0000000..74bae1d --- /dev/null +++ b/MyOffice.Data.Repositories/Item/ItemCategoryRepository.cs @@ -0,0 +1,72 @@ +namespace MyOffice.Data.Repositories.Item; + +using Microsoft.EntityFrameworkCore; +using MyOffice.Data.Models.Items; +using MyOffice.DbContext; + +public class ItemCategoryRepository : AppRepository, IItemCategoryRepository +{ + public ItemCategoryRepository(AppDbContext context) : base(context) + { + } + + public List GetAll(Guid userId) + { + return _context.ItemCategories + .Include(x => x.Items) + .Where(x => x.UserId == userId) + .ToList(); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.ItemCategories + .Include(x => x.Items) + .Where(x => x.UserId == userId) + .ToListAsync(cancellationToken); + } + + public bool Add(ItemCategory itemCategory) + { + return AddBase(itemCategory) > 0; + } + + public async Task AddAsync(ItemCategory itemCategory, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(itemCategory, cancellationToken) > 0; + } + + public ItemCategory? Get(Guid userId, Guid id) + { + return _context.ItemCategories + .Include(x => x.Items) + .FirstOrDefault(x => x.Id == id && x.UserId == userId); + } + + public async Task GetAsync(Guid userId, Guid id, CancellationToken cancellationToken = default) + { + return await _context.ItemCategories + .Include(x => x.Items) + .FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId, cancellationToken); + } + + public bool Update(ItemCategory itemCategory) + { + return UpdateBase(itemCategory) > 0; + } + + public async Task UpdateAsync(ItemCategory itemCategory, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(itemCategory, cancellationToken) > 0; + } + + public bool Remove(ItemCategory itemCategory) + { + return RemoveBase(itemCategory) > 0; + } + + public async Task RemoveAsync(ItemCategory itemCategory, CancellationToken cancellationToken = default) + { + return await RemoveBaseAsync(itemCategory, cancellationToken) > 0; + } +} diff --git a/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs b/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs new file mode 100644 index 0000000..1f57d46 --- /dev/null +++ b/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs @@ -0,0 +1,69 @@ +namespace MyOffice.Data.Repositories.Item; + +using Microsoft.EntityFrameworkCore; +using MyOffice.Data.Models.Items; +using MyOffice.DbContext; + +public class ItemGlobalRepository : AppRepository, IItemGlobalRepository +{ + public ItemGlobalRepository(AppDbContext context) : base(context) + { + } + + public ItemGlobal? Get(Guid id) + { + return _context.ItemGlobals.FirstOrDefault(x => x.Id == id); + } + + public ItemGlobal? GetByName(string name) + { + return _context.ItemGlobals.FirstOrDefault(x => x.Name == name); + } + + public Task GetByNameAsync(string name, CancellationToken cancellationToken = default) + { + return _context.ItemGlobals.FirstOrDefaultAsync(x => x.Name == name, cancellationToken); + } + + public bool Add(ItemGlobal itemGlobal) + { + return AddBase(itemGlobal) > 0; + } + + public async Task AddAsync(ItemGlobal itemGlobal, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(itemGlobal, cancellationToken) > 0; + } + + public List GetAvailableToUser(Guid userId) + { + return _context.Motions + .Include(x => x.Item) + .ThenInclude(x => x.ItemGlobal) + .ThenInclude(x => x.Items) + .ThenInclude(x => x.Category) + .Where(x => x.Account.AccessRights!.Any(a => a.UserId == userId)) + .Where(x => x.Item.ItemGlobal.Items.All(g => g.Category!.UserId != userId)) + .Select(x => x.Item.ItemGlobal) + .GroupBy(x => x.Id) + .Select(x => x.First()) + .ToList(); + } + + public async Task> GetAvailableToUserAsync( + Guid userId, + CancellationToken cancellationToken = default) + { + return await _context.Motions + .Include(x => x.Item) + .ThenInclude(x => x.ItemGlobal) + .ThenInclude(x => x.Items) + .ThenInclude(x => x.Category) + .Where(x => x.Account.AccessRights!.Any(a => a.UserId == userId)) + .Where(x => x.Item.ItemGlobal.Items.All(g => g.Category!.UserId != userId)) + .Select(x => x.Item.ItemGlobal) + .GroupBy(x => x.Id) + .Select(x => x.First()) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Item/ItemRepository.cs b/MyOffice.Data.Repositories/Item/ItemRepository.cs new file mode 100644 index 0000000..ed0bb82 --- /dev/null +++ b/MyOffice.Data.Repositories/Item/ItemRepository.cs @@ -0,0 +1,121 @@ +namespace MyOffice.Data.Repositories.Item; + +using Microsoft.EntityFrameworkCore; +using MyOffice.Data.Models.Items; +using MyOffice.DbContext; + +public class ItemRepository : AppRepository, IItemRepository +{ + public ItemRepository(AppDbContext context) : base(context) + { + } + + public List GetAll(Guid userId) + { + return _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId) + .ToList(); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId) + .ToListAsync(cancellationToken); + } + + public List GetByCategory(Guid userId, Guid categoryId) + { + return _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId && x.CategoryId == categoryId) + .ToList(); + } + + public async Task> GetByCategoryAsync( + Guid userId, + Guid categoryId, + CancellationToken cancellationToken = default) + { + return await _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId && x.CategoryId == categoryId) + .ToListAsync(cancellationToken); + } + + public Item? GetByGlobal(Guid userId, Guid globalItemId) + { + return _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .FirstOrDefault(x => x.Category!.UserId == userId && x.ItemGlobalId == globalItemId); + } + + public Task GetByGlobalAsync(Guid userId, Guid globalItemId, CancellationToken cancellationToken = default) + { + return _context.Items + .Include(x => x.Motions) + .Include(x => x.Category) + .Include(x => x.ItemGlobal) + .FirstOrDefaultAsync( + x => x.Category!.UserId == userId && x.ItemGlobalId == globalItemId, + cancellationToken); + } + + public bool Update(Item item) + { + return base.UpdateBase(item) > 0; + } + + public async Task UpdateAsync(Item item, CancellationToken cancellationToken = default) + { + return await UpdateBaseAsync(item, cancellationToken) > 0; + } + + public bool Add(Item item) + { + return base.AddBase(item) > 0; + } + + public async Task AddAsync(Item item, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(item, cancellationToken) > 0; + } + + public List Find(Guid userId, string term, int limit) + { + return _context + .Items + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId && x.ItemGlobal.Name.ToLower().Contains(term.ToLower())) + .OrderByDescending(x => x!.Motions!.Count()) + .Take(limit) + .ToList(); + } + + public async Task> FindAsync( + Guid userId, + string term, + int limit, + CancellationToken cancellationToken = default) + { + return await _context + .Items + .Include(x => x.ItemGlobal) + .Where(x => x.Category!.UserId == userId && x.ItemGlobal.Name.ToLower().Contains(term.ToLower())) + .OrderByDescending(x => x!.Motions!.Count()) + .Take(limit) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/MyOffice.Data.Repositories.csproj b/MyOffice.Data.Repositories/MyOffice.Data.Repositories.csproj new file mode 100644 index 0000000..0d17979 --- /dev/null +++ b/MyOffice.Data.Repositories/MyOffice.Data.Repositories.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/MyOffice.Data.Repositories/Notifications/EmailTemplateRepository.cs b/MyOffice.Data.Repositories/Notifications/EmailTemplateRepository.cs new file mode 100644 index 0000000..40addda --- /dev/null +++ b/MyOffice.Data.Repositories/Notifications/EmailTemplateRepository.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Data.Repositories.Item; + +using MyOffice.Data.Models.Notifications; +using MyOffice.DbContext; + +public class EmailTemplateRepository : AppRepository, IEmailTemplateRepository +{ + public EmailTemplateRepository(AppDbContext context) : base(context) + { + } + + public EmailTemplate? Get(EmailTemplateEnum emailTemplate) + { + var emailTemplateStr = emailTemplate.ToString(); + return _context.EmailTemplates.FirstOrDefault(x => x.Id == emailTemplateStr); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Notifications/IEmailTemplateRepository.cs b/MyOffice.Data.Repositories/Notifications/IEmailTemplateRepository.cs new file mode 100644 index 0000000..65849f2 --- /dev/null +++ b/MyOffice.Data.Repositories/Notifications/IEmailTemplateRepository.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Data.Repositories.Item; + +using MyOffice.Data.Models.Notifications; + +public interface IEmailTemplateRepository +{ + EmailTemplate? Get(EmailTemplateEnum emailTemplate); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/RepositoryBase.cs b/MyOffice.Data.Repositories/RepositoryBase.cs new file mode 100644 index 0000000..c265ef0 --- /dev/null +++ b/MyOffice.Data.Repositories/RepositoryBase.cs @@ -0,0 +1,108 @@ +namespace MyOffice.Data.Repositories; + +using Microsoft.EntityFrameworkCore; +using DbContext; + +public class RepositoryBase where TEntity : class +{ + protected readonly AppDbContext _context; + + protected RepositoryBase( + AppDbContext context + ) + { + _context = context; + } + + protected async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + try + { + return await _context.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException updateException) + { + throw updateException; + } + } + + protected int SaveChanges() + { + try + { + return _context.SaveChanges(); + } + catch (DbUpdateException updateException) + { + throw updateException; + } + } + + + protected async Task GetBaseAsync(Guid id) + { + return await _context.Set().FindAsync(id); + } + + protected TEntity? GetBase(Guid id) + { + return _context.Set().Find(id); + } + + protected int AddBase(TEntity entity) + { + _context.Entry(entity).State = EntityState.Added; + + var result = SaveChanges(); + + _context.Entry(entity).State = EntityState.Detached; + return result; + } + + protected async Task AddBaseAsync(TEntity entity, CancellationToken cancellationToken = default) + { + _context.Entry(entity).State = EntityState.Added; + var result = await SaveChangesAsync(cancellationToken); + _context.Entry(entity).State = EntityState.Detached; + return result; + } + + protected int RemoveBase(TEntity entity) + { + _context.Set().Remove(entity); + + _context.Entry(entity).State = EntityState.Deleted; + + var result = SaveChanges(); + + _context.Entry(entity).State = EntityState.Detached; + return result; + } + + protected async Task RemoveBaseAsync(TEntity entity, CancellationToken cancellationToken = default) + { + _context.Set().Remove(entity); + _context.Entry(entity).State = EntityState.Deleted; + var result = await SaveChangesAsync(cancellationToken); + _context.Entry(entity).State = EntityState.Detached; + return result; + } + + protected int UpdateBase(TEntity entity) + { + _context.Entry(entity).State = EntityState.Modified; + + var result = SaveChanges(); + + _context.Entry(entity).State = EntityState.Detached; + return result; + } + + protected async Task UpdateBaseAsync(TEntity entity, CancellationToken cancellationToken = default) + { + _context.Entry(entity).State = EntityState.Modified; + var result = await SaveChangesAsync(cancellationToken); + _context.Entry(entity).State = EntityState.Detached; + return result; + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Users/IUserExternalRepository.cs b/MyOffice.Data.Repositories/Users/IUserExternalRepository.cs new file mode 100644 index 0000000..738f029 --- /dev/null +++ b/MyOffice.Data.Repositories/Users/IUserExternalRepository.cs @@ -0,0 +1,20 @@ +namespace MyOffice.Data.Repositories.Users; + +using Models.Users; + +public interface IUserExternalRepository +{ + int AddUserExternal(UserExternal external); + Task AddUserExternalAsync(UserExternal external, CancellationToken cancellationToken = default); + + Task> GetUserExternalsByUserIdAsync(Guid userId); + + Task GetByUserIdAsync(Guid userId, string provider); + UserExternal? GetByUserId(Guid userId, string provider); + + UserExternal? GetByExternalId(string externalId, string provider); + Task GetByExternalIdAsync(string externalId, string provider); + + void RemoveUserExternal(UserExternal userExternal); + Task RemoveUserExternalAsync(UserExternal userExternal, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Users/IUserRepository.cs b/MyOffice.Data.Repositories/Users/IUserRepository.cs new file mode 100644 index 0000000..9d47ac1 --- /dev/null +++ b/MyOffice.Data.Repositories/Users/IUserRepository.cs @@ -0,0 +1,15 @@ +namespace MyOffice.Data.Repositories.Users; + +using MyOffice.Data.Models.Users; + +public interface IUserRepository +{ + Task GetUserAsync(Guid id); + User? GetUser(Guid id); + + Task GetByUserUserNameAsync(string userName); + User? GetByUserUserName(string userName); + + int AddUser(User user); + int UpdateUser(User user); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Users/UserExternalRepository.cs b/MyOffice.Data.Repositories/Users/UserExternalRepository.cs new file mode 100644 index 0000000..5fd1ad8 --- /dev/null +++ b/MyOffice.Data.Repositories/Users/UserExternalRepository.cs @@ -0,0 +1,108 @@ +namespace MyOffice.Data.Repositories.Users; + +using MyOffice.Data.Models.Users; +using Repositories; +using Microsoft.EntityFrameworkCore; +using MyOffice.DbContext; + +public class UserExternalRepository : AppRepository, IUserExternalRepository +{ + public UserExternalRepository(AppDbContext context) : base(context) + { + } + + public int AddUserExternal(UserExternal userExternal) + { + return AddBase(userExternal); + } + + public async Task AddUserExternalAsync(UserExternal userExternal, CancellationToken cancellationToken = default) + { + return await AddBaseAsync(userExternal, cancellationToken); + } + + public async Task AddUserExternalsAsync(IEnumerable claims) + { + _context.AddRange(claims); + return await SaveChangesAsync(); + } + + + public async Task> GetUserExternalsByUserNameAsync(string userName) + { + return await _context + .UserClaims + .Where(x => x.User.UserName == userName) + .Include(x => x.User) + .ToListAsync(); + } + + public async Task> GetUserExternalsByUserIdAsync(Guid userId) + { + return await _context + .UserClaims + .Where(x => x.UserId == userId) + .Include(x => x.User) + .ToListAsync(); + } + + public List GetUserExternalsByUser(Guid userId) + { + return _context + .UserClaims + .Where(x => x.UserId == userId) + .Include(x => x.User) + .ToList(); + } + + public List GetUserExternalsByUserName(string userName) + { + return _context + .UserClaims + .Where(x => x.User.UserName == userName) + .Include(x => x.User) + .ToList(); + } + + public async Task GetByUserIdAsync(Guid userId, string provider) + { + return await _context + .UserClaims + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.UserId == userId && x.Provider == provider); + } + + public UserExternal? GetByUserId(Guid userId, string provider) + { + return _context + .UserClaims + .Include(x => x.User) + .FirstOrDefault(x => x.UserId == userId && x.Provider == provider); + } + + public UserExternal? GetByExternalId(string externalId, string provider) + { + return _context + .UserClaims + .Include(x => x.User) + .FirstOrDefault(x => x.ExternalId == externalId && x.Provider == provider); + } + + public Task GetByExternalIdAsync(string externalId, string provider) + { + return _context + .UserClaims + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.ExternalId == externalId && x.Provider == provider); + } + + public void RemoveUserExternal(UserExternal userExternal) + { + RemoveBase(userExternal); + } + + public async Task RemoveUserExternalAsync(UserExternal userExternal, CancellationToken cancellationToken = default) + { + await RemoveBaseAsync(userExternal, cancellationToken); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Users/UserRepository.cs b/MyOffice.Data.Repositories/Users/UserRepository.cs new file mode 100644 index 0000000..76aae6b --- /dev/null +++ b/MyOffice.Data.Repositories/Users/UserRepository.cs @@ -0,0 +1,43 @@ +namespace MyOffice.Data.Repositories.Users; + +using MyOffice.Data.Models.Users; +using Repositories; +using Microsoft.EntityFrameworkCore; +using MyOffice.DbContext; + +public class UserRepository : AppRepository, IUserRepository +{ + public UserRepository(AppDbContext context) : base(context) + { + } + + public async Task GetUserAsync(Guid id) + { + return await GetBaseAsync(id); + } + + public User? GetUser(Guid id) + { + return GetBase(id); + } + + public async Task GetByUserUserNameAsync(string userName) + { + return await _context.Users.FirstOrDefaultAsync(x => x.UserName == userName); + } + + public User? GetByUserUserName(string userName) + { + return _context.Users.FirstOrDefault(x => x.UserName == userName); + } + + public int AddUser(User user) + { + return AddBase(user); + } + + public int UpdateUser(User user) + { + return UpdateBase(user); + } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Verifications/IVerificationCodeRepository.cs b/MyOffice.Data.Repositories/Verifications/IVerificationCodeRepository.cs new file mode 100644 index 0000000..4c07131 --- /dev/null +++ b/MyOffice.Data.Repositories/Verifications/IVerificationCodeRepository.cs @@ -0,0 +1,11 @@ +using MyOffice.Data.Models.Verifications; + +namespace MyOffice.Data.Repositories.Item; + + +public interface IVerificationCodeRepository +{ + VerificationCode? GetByCode(string code); + bool Add(VerificationCode entity); + bool Update(VerificationCode entity); +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Verifications/VerificationCodeRepository.cs b/MyOffice.Data.Repositories/Verifications/VerificationCodeRepository.cs new file mode 100644 index 0000000..796a8cd --- /dev/null +++ b/MyOffice.Data.Repositories/Verifications/VerificationCodeRepository.cs @@ -0,0 +1,27 @@ +namespace MyOffice.Data.Repositories.Item; + +using Microsoft.EntityFrameworkCore; +using MyOffice.Data.Models.Verifications; +using MyOffice.DbContext; + +public class VerificationCodeRepository : AppRepository, IVerificationCodeRepository +{ + public VerificationCodeRepository(AppDbContext context) : base(context) + { + } + + public bool Add(VerificationCode entity) + { + return AddBase(entity) > 0; + } + + public bool Update(VerificationCode entity) + { + return UpdateBase(entity) > 0; + } + + public VerificationCode? GetByCode(string code) + { + return _context.Verifications.FirstOrDefault(x => x.Code == code); + } +} \ No newline at end of file diff --git a/MyOffice.DbContext/AppDbContext.cs b/MyOffice.DbContext/AppDbContext.cs new file mode 100644 index 0000000..eb14445 --- /dev/null +++ b/MyOffice.DbContext/AppDbContext.cs @@ -0,0 +1,335 @@ +namespace MyOffice.DbContext; + +using Data.Models.Accounts; +using Data.Models.Currencies; +using Data.Models.Items; +using Microsoft.EntityFrameworkCore; +using Data.Models.Users; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.Data.Models.Verifications; +using MyOffice.Data.Models.Notifications; + +public enum AppDbContextProvidersEnum +{ + sqlite, + mssql, + npgsql +} + +public class AppDbContext : DbContext +{ + private readonly AppDbContextProvidersEnum _provider; + + private readonly Dictionary _noCaseCollation = new() + { + { AppDbContextProvidersEnum.sqlite, "NOCASE" }, + { AppDbContextProvidersEnum.npgsql, "my_ci_collation" } + }; + + public AppDbContext(AppDbContextProvidersEnum provider, DbContextOptions options) : base(options) + { + _provider = provider; + ConfigureChangeTracker(); + } + + public AppDbContext(DbContextOptions options, ConnectionConfiguration connection) : base(options) + { + _provider = Enum.Parse(connection.Provider, ignoreCase: true); + ConfigureChangeTracker(); + } + + private void ConfigureChangeTracker() + { + // Match historical repository behavior (manual EntityState updates). + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + ChangeTracker.AutoDetectChangesEnabled = false; + } + + public DbSet Users { get; set; } = null!; + public DbSet UserClaims { get; set; } = null!; + + public DbSet CurrencyGlobals { get; set; } = null!; + public DbSet Currencies { get; set; } = null!; + public DbSet CurrencyRates { get; set; } = null!; + public DbSet AccountCategories { get; set; } = null!; + public DbSet Accounts { get; set; } = null!; + public DbSet AccountAccountCategories { get; set; } = null!; + public DbSet AccountAccesses { get; set; } = null!; + public DbSet AccountAccessInvites { get; set; } = null!; + + public DbSet ItemCategories { get; set; } = null!; + public DbSet ItemGlobals { get; set; } = null!; + public DbSet Items { get; set; } = null!; + public DbSet Motions { get; set; } = null!; + + public DbSet Verifications { get; set; } = null!; + public DbSet EmailTemplates { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var noCaseCollation = _noCaseCollation[_provider]; + + if (_provider == AppDbContextProvidersEnum.npgsql) + { + modelBuilder.HasCollation("my_ci_collation", "en-u-ks-primary", "icu", false); + } + + /* NOCASE PROPERTIES */ + modelBuilder.Entity() + .Property(x => x.UserName) + .UseCollation(noCaseCollation); + + modelBuilder.Entity() + .Property(x => x.Email) + .UseCollation(noCaseCollation); + + modelBuilder.Entity() + .Property(x => x.Provider) + .UseCollation(noCaseCollation); + + modelBuilder.Entity() + .Property(x => x.Email) + .UseCollation(noCaseCollation); + + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.UserClaims) + .HasForeignKey(x => x.UserId); + + UserCreating(modelBuilder); + CurrencyCreating(modelBuilder); + AccountCreating(modelBuilder); + MotionsCreating(modelBuilder); + VerificationsCreating(modelBuilder); + EmailTemplateCreating(modelBuilder); + + modelBuilder.UseOpenIddict(); + + base.OnModelCreating(modelBuilder); + } + + private void VerificationsCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany() + .HasForeignKey(x => x.UserId); + } + + private void EmailTemplateCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(x => x.Id); + } + + private void UserCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(x => x.Currency) + .WithMany() + .HasForeignKey(x => x.CurrencyId); + } + + private void CurrencyCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.CurrencyGlobal) + .WithMany(x => x.Currencies) + .HasForeignKey(x => x.CurrencyGlobalId); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.Currencies) + .HasForeignKey(x => x.UserId); + + modelBuilder.Entity() + .HasOne(x => x.Currency) + .WithMany(x => x.Rates) + .HasForeignKey(x => x.CurrencyId); + + modelBuilder.Entity() + .HasOne(x => x.CurrentRate) + .WithMany(x => x.Currencies) + .HasForeignKey(x => x.CurrentRateId); + } + + private void AccountCreating(ModelBuilder modelBuilder) + { + #region Account + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.CurrencyGlobal) + .WithMany(x => x.Accounts) + .HasForeignKey(x => x.CurrencyGlobalId); + + modelBuilder.Entity() + .HasOne(x => x.Owner) + .WithMany(x => x.Accounts) + .HasForeignKey(x => x.OwnerId); + + #endregion Account + + #region AccountAccess + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.Account) + .WithMany(x => x.AccessRights) + .HasForeignKey(x => x.AccountId); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.AccountAccess) + .HasForeignKey(x => x.UserId); + + modelBuilder.Entity() + .HasOne(x => x.Owner) + .WithMany(x => x.AccountAccessOwners) + .HasForeignKey(x => x.OwnerId); + + modelBuilder + .Entity() + .Property(d => d.Type) + .HasConversion(new EnumToStringConverter()); + + modelBuilder.Entity() + .HasIndex(p => new { p.AccountId, p.UserId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(p => new { p.AccountId, p.OwnerId }) + .IsUnique(); + + #endregion AccountAccess + + #region AccountAccessInvite + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.AccountAccessInvites) + .HasForeignKey(x => x.UserId); + + modelBuilder.Entity() + .HasOne(x => x.Account) + .WithMany(x => x.Invites) + .HasForeignKey(x => x.AccountId); + + #endregion AccountAccessInvite + + #region Motion + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.Account) + .WithMany(x => x.Motions) + .HasForeignKey(x => x.AccountId); + + #endregion Motion + + #region AccountCategory + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.AccountCategories) + .HasForeignKey(x => x.UserId); + + #endregion AccountCategory + + #region AccountAccountCategory + + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.Account) + .WithMany(x => x.Categories) + .HasForeignKey(x => x.AccountId); + + modelBuilder.Entity() + .HasOne(x => x.Category) + .WithMany(x => x.Accounts) + .HasForeignKey(x => x.CategoryId); + + modelBuilder.Entity() + .HasIndex(p => new { p.AccountId, p.CategoryId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(x => x.Category) + .WithMany(x => x.Accounts) + .HasForeignKey(x => x.CategoryId); + + #endregion AccountAccountCategory + } + + private void MotionsCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(x => x.Id); + modelBuilder.Entity() + .HasKey(x => x.Id); + modelBuilder.Entity() + .HasKey(x => x.Id); + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity() + .HasOne(x => x.User) + .WithMany(x => x.ItemCategories) + .HasForeignKey(x => x.UserId); + + modelBuilder.Entity() + .HasOne(x => x.Category) + .WithMany(x => x.Items) + .HasForeignKey(x => x.CategoryId); + + modelBuilder.Entity() + .HasOne(x => x.ItemGlobal) + .WithMany(x => x.Items) + .HasForeignKey(x => x.ItemGlobalId); + + modelBuilder.Entity() + .HasOne(x => x.Item) + .WithMany(x => x.Motions) + .HasForeignKey(x => x.ItemId); + + modelBuilder.Entity() + .HasOne(x => x.Account) + .WithMany(x => x.Motions) + .HasForeignKey(x => x.AccountId); + + modelBuilder.Entity() + .Property(x => x.AmountMinus) + .HasPrecision(18, 6); + + modelBuilder.Entity() + .Property(x => x.AmountPlus) + .HasPrecision(18, 6); + } +} \ No newline at end of file diff --git a/MyOffice.DbContext/AppDbContextFactory.cs b/MyOffice.DbContext/AppDbContextFactory.cs new file mode 100644 index 0000000..3362587 --- /dev/null +++ b/MyOffice.DbContext/AppDbContextFactory.cs @@ -0,0 +1,37 @@ +namespace MyOffice.DbContext; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +public class AppDbContextFactory : IDesignTimeDbContextFactory +{ + public static AppDbContextFactory Instance = new(); + + public AppDbContext CreateDbContext() + { + return CreateDbContext(null); + } + + public AppDbContext CreateDbContext(string[]? args) + { + var connection = RepositoryInitializer.ConnectionString; + if (connection is null) + { + connection = new ConnectionConfiguration( + args?[0] ?? "npgsql", + args?[1] ?? string.Empty); + } + + var builder = new DbContextOptionsBuilder(); + DbContextServiceCollectionExtensions.ConfigureDbContextOptions(builder, connection); + + if (!Enum.TryParse(connection?.Provider ?? args?[0] ?? "", ignoreCase: true, out var providerEnum)) + throw new InvalidOperationException($"No such provider: [{connection?.Provider}]"); + + var db = new AppDbContext(providerEnum, builder.Options); + db.ChangeTracker.AutoDetectChangesEnabled = false; + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + return db; + } +} \ No newline at end of file diff --git a/MyOffice.DbContext/DatabaseBootstrapper.cs b/MyOffice.DbContext/DatabaseBootstrapper.cs new file mode 100644 index 0000000..9e27209 --- /dev/null +++ b/MyOffice.DbContext/DatabaseBootstrapper.cs @@ -0,0 +1,67 @@ +namespace MyOffice.DbContext; + +using Data.Models.Currencies; +using Microsoft.EntityFrameworkCore; + +/// +/// Migrate and seed using a DI-scoped (not the design-time factory). +/// +public static class DatabaseBootstrapper +{ + public static async Task InitializeAsync(AppDbContext db, CancellationToken cancellationToken = default) + { + await db.Database.MigrateAsync(cancellationToken); + await PrefillAsync(db, cancellationToken); + await DemoDataSeeder.SeedIfEmptyAsync(db, cancellationToken); + await UpdateCurrentRatesAsync(db, cancellationToken); + } + + private static async Task PrefillAsync(AppDbContext dbContext, CancellationToken cancellationToken) + { + var predefinedCurrencyGlobals = new List + { + new() { Id = CurrencyGlobalIdEnum.UAH.ToString(), DefaultQuantity = 1, Symbol = "₴", Name = "Ukrainian hryvnias" }, + new() { Id = CurrencyGlobalIdEnum.USD.ToString(), DefaultQuantity = 1, Symbol = "$", Name = "US Dollar" }, + new() { Id = CurrencyGlobalIdEnum.EUR.ToString(), DefaultQuantity = 1, Symbol = "€", Name = "Euros" }, + new() { Id = CurrencyGlobalIdEnum.GBP.ToString(), DefaultQuantity = 1, Symbol = "£", Name = "British pounds sterling" }, + new() { Id = CurrencyGlobalIdEnum.RUB.ToString(), DefaultQuantity = 10, Symbol = "₽", Name = "Russia Ruble" }, + new() { Id = CurrencyGlobalIdEnum.BTC.ToString(), DefaultQuantity = 10, Symbol = "btc", Name = "Bitcoin" }, + new() { Id = CurrencyGlobalIdEnum.ETH.ToString(), DefaultQuantity = 10, Symbol = "eth", Name = "Ethereum" }, + new() { Id = CurrencyGlobalIdEnum.TON.ToString(), DefaultQuantity = 10, Symbol = "ton", Name = "TON" }, + new() { Id = CurrencyGlobalIdEnum.OTHER.ToString(), DefaultQuantity = 1, Symbol = "", Name = "Other" }, + }; + + var currencyGlobals = await dbContext.CurrencyGlobals.ToListAsync(cancellationToken); + foreach (var predefined in predefinedCurrencyGlobals) + { + if (currencyGlobals.All(x => x.Id != predefined.Id)) + { + dbContext.CurrencyGlobals.Add(predefined); + } + } + + await dbContext.SaveChangesAsync(cancellationToken); + } + + private static async Task UpdateCurrentRatesAsync(AppDbContext dbContext, CancellationToken cancellationToken) + { + var lastRates = await dbContext.CurrencyRates + .Include(x => x.Currency) + .GroupBy(x => new { x.CurrencyId }, + (key, g) => g.Where(x => x.DateTime <= DateTime.UtcNow).OrderByDescending(x => x.DateTime).First()) + .ToListAsync(cancellationToken); + + var currencies = await dbContext.Currencies.ToListAsync(cancellationToken); + foreach (var currency in currencies) + { + var rate = lastRates.FirstOrDefault(x => x.CurrencyId == currency.Id); + if (rate != null) + { + currency.CurrentRateId = rate.Id; + dbContext.Attach(currency).State = EntityState.Modified; + } + } + + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/MyOffice.DbContext/DbContextServiceCollectionExtensions.cs b/MyOffice.DbContext/DbContextServiceCollectionExtensions.cs new file mode 100644 index 0000000..42e111c --- /dev/null +++ b/MyOffice.DbContext/DbContextServiceCollectionExtensions.cs @@ -0,0 +1,50 @@ +namespace MyOffice.DbContext; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +public static class DbContextServiceCollectionExtensions +{ + public static IServiceCollection AddAppDbContext( + this IServiceCollection services, + ConnectionConfiguration connection + ) + { + services.AddSingleton(connection); + + services.AddDbContext((_, options) => + { + ConfigureDbContextOptions(options, connection); + }); + + return services; + } + + public static void ConfigureDbContextOptions( + DbContextOptionsBuilder options, + ConnectionConfiguration connection + ) + { + if (!Enum.TryParse(connection.Provider, ignoreCase: true, out var provider)) + throw new InvalidOperationException($"No such provider: [{connection.Provider}]"); + + switch (provider) + { + case AppDbContextProvidersEnum.npgsql: + options.UseNpgsql(connection.ConnectionString, x => + x.MigrationsAssembly("MyOffice.Migrations.Postgres")); + break; + case AppDbContextProvidersEnum.sqlite: + options.UseSqlite(connection.ConnectionString, x => + x.MigrationsAssembly("MyOffice.Migrations.Sqlite")); + break; + default: + throw new NotSupportedException($"No such provider: [{connection.Provider}]"); + } + +#if DEBUG + options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); +#endif + } +} diff --git a/MyOffice.DbContext/DemoDataSeeder.cs b/MyOffice.DbContext/DemoDataSeeder.cs new file mode 100644 index 0000000..bfc9b06 --- /dev/null +++ b/MyOffice.DbContext/DemoDataSeeder.cs @@ -0,0 +1,292 @@ +namespace MyOffice.DbContext; + +using Data.Models.Accounts; +using Data.Models.Currencies; +using Data.Models.Items; +using Data.Models.Users; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +/// +/// Fills an empty database with three demo users (UAH/USD/EUR) and sample catalog data. +/// +public static class DemoDataSeeder +{ + private static readonly PasswordHasher PasswordHasher = new(); + + private static readonly string[] CurrencyCodes = ["UAH", "USD", "EUR"]; + + private static readonly (string Code, string Email, string Password, string FirstName)[] Users = + [ + ("UAH", "user_UAH@user_UAH.userUAH", "user_UAH", "User UAH"), + ("USD", "user_USD@user_USD.userUSD", "user_USD", "User USD"), + ("EUR", "user_EUR@user_EUR.userEUR", "user_EUR", "User EUR"), + ]; + + /// 1 USD = 42 UAH, 1 EUR = 50 UAH. + private const decimal UsdInUah = 42m; + private const decimal EurInUah = 50m; + + public static async Task SeedIfEmptyAsync(AppDbContext db, CancellationToken cancellationToken = default) + { + if (await db.Users.AnyAsync(cancellationToken)) + { + return; + } + + // AppDbContext defaults to NoTracking; seeding needs identity values and graph inserts. + var previousTracking = db.ChangeTracker.QueryTrackingBehavior; + var previousDetect = db.ChangeTracker.AutoDetectChangesEnabled; + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; + db.ChangeTracker.AutoDetectChangesEnabled = true; + + try + { + var yearStart = new DateTime(DateTime.UtcNow.Year, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var rng = new Random(2026); + + foreach (var (code, email, password, firstName) in Users) + { + await SeedUserAsync(db, code, email, password, firstName, yearStart, rng, cancellationToken); + } + + await db.SaveChangesAsync(cancellationToken); + } + finally + { + db.ChangeTracker.QueryTrackingBehavior = previousTracking; + db.ChangeTracker.AutoDetectChangesEnabled = previousDetect; + db.ChangeTracker.Clear(); + } + } + + private static async Task SeedUserAsync( + AppDbContext db, + string homeCurrency, + string email, + string password, + string firstName, + DateTime rateDate, + Random rng, + CancellationToken cancellationToken) + { + var ua = homeCurrency == "UAH"; + var userId = Guid.NewGuid(); + + var user = new User + { + Id = userId, + UserName = email, + Email = email, + PasswordHash = PasswordHasher.HashPassword(new object(), password), + FirstName = firstName, + FullName = firstName, + IsEmailConfirmed = true, + CurrencyId = homeCurrency, + }; + db.Users.Add(user); + + var currencies = new Dictionary(StringComparer.Ordinal); + foreach (var code in CurrencyCodes) + { + var currency = new Currency + { + Id = Guid.NewGuid(), + UserId = userId, + CurrencyGlobalId = code, + Name = CurrencyDisplayName(code, ua), + ShortName = code, + IsPrimary = code == homeCurrency, + }; + db.Currencies.Add(currency); + db.CurrencyRates.Add(new CurrencyRate + { + CurrencyId = currency.Id, + DateTime = rateDate, + Quantity = 1, + Rate = RateInHome(code, homeCurrency), + }); + currencies[code] = currency; + } + + // UnCategorized convention: category Id == user Id + db.ItemCategories.Add(new ItemCategory + { + Id = userId, + UserId = userId, + Name = ua ? "Без категорії" : "UnCategorized", + Items = [], + IsInternal = true, + }); + + var accountCategoryDefs = ua + ? new (string Key, string Name)[] { ("CASH", "Готівка"), ("BANK", "Банк"), ("DEPOSIT", "Депозит") } + : [("CASH", "CASH"), ("BANK", "BANK"), ("DEPOSIT", "DEPOSIT")]; + + var accounts = new List(); + foreach (var (key, name) in accountCategoryDefs) + { + var category = new AccountCategory + { + Id = Guid.NewGuid(), + UserId = userId, + Name = name, + }; + db.AccountCategories.Add(category); + + foreach (var code in CurrencyCodes) + { + var account = new Account + { + Id = Guid.NewGuid(), + OwnerId = userId, + CurrencyGlobalId = code, + Name = $"{name} {code}", + }; + db.Accounts.Add(account); + db.AccountAccountCategories.Add(new AccountAccountCategory + { + AccountId = account.Id, + CategoryId = category.Id, + }); + db.AccountAccesses.Add(new AccountAccess + { + AccountId = account.Id, + UserId = userId, + OwnerId = userId, + IsAllowRead = true, + IsAllowWrite = true, + IsAllowManage = true, + Type = AccountAccessTypeEnum.balance, + }); + accounts.Add(account); + } + } + + var incomeCategory = new ItemCategory + { + Id = Guid.NewGuid(), + UserId = userId, + Name = ua ? "Доходи" : "INCOME", + Items = [], + }; + var outcomeCategory = new ItemCategory + { + Id = Guid.NewGuid(), + UserId = userId, + Name = ua ? "Витрати" : "OUTCOME", + Items = [], + }; + db.ItemCategories.AddRange(incomeCategory, outcomeCategory); + + var incomeNames = ua + ? new[] { "Готівка", "Офіс" } + : ["CASH", "OFFICE"]; + var outcomeNames = ua + ? new[] + { + "Товари", "Бензин", "Steam", "Продукти", "Кафе", + "Оренда", "Комунальні", "Інтернет", "Зв'язок", "Розваги", + } + : [ + "Goods", "Petrol", "Steam", "Groceries", "Cafe", + "Rent", "Utilities", "Internet", "Mobile", "Entertainment", + ]; + + var incomeItems = await AddItemsAsync(db, incomeCategory.Id, incomeNames, cancellationToken); + var outcomeItems = await AddItemsAsync(db, outcomeCategory.Id, outcomeNames, cancellationToken); + + await db.SaveChangesAsync(cancellationToken); + + SeedMotions(db, accounts, incomeItems, outcomeItems, rng, count: 100); + } + + private static async Task> AddItemsAsync( + AppDbContext db, + Guid categoryId, + IEnumerable names, + CancellationToken cancellationToken) + { + var items = new List(); + foreach (var name in names) + { + var global = db.ItemGlobals.Local.FirstOrDefault(x => x.Name == name) + ?? await db.ItemGlobals.FirstOrDefaultAsync(x => x.Name == name, cancellationToken); + if (global == null) + { + global = new ItemGlobal { Id = Guid.NewGuid(), Name = name }; + db.ItemGlobals.Add(global); + } + + var item = new Item + { + CategoryId = categoryId, + ItemGlobalId = global.Id, + }; + db.Items.Add(item); + items.Add(item); + } + + return items; + } + + private static void SeedMotions( + AppDbContext db, + List accounts, + List incomeItems, + List outcomeItems, + Random rng, + int count) + { + var now = DateTime.UtcNow; + var from = now.AddMonths(-3); + + for (var i = 0; i < count; i++) + { + var isIncome = rng.Next(2) == 0; + var item = isIncome + ? incomeItems[rng.Next(incomeItems.Count)] + : outcomeItems[rng.Next(outcomeItems.Count)]; + var account = accounts[rng.Next(accounts.Count)]; + var days = (now - from).TotalDays; + var when = from.AddDays(rng.NextDouble() * days); + var amount = Math.Round((decimal)(rng.NextDouble() * (isIncome ? 4500 : 700) + (isIncome ? 200 : 20)), 2); + + db.Motions.Add(new Motion + { + Id = Guid.NewGuid(), + AccountId = account.Id, + ItemId = item.Id, + DateTime = when, + CreatedOn = when, + Description = isIncome ? "Demo income" : "Demo expense", + AmountPlus = isIncome ? amount : 0, + AmountMinus = isIncome ? 0 : amount, + }); + } + } + + private static decimal RateInHome(string currencyCode, string homeCurrency) + { + static decimal InUah(string code) => code switch + { + "UAH" => 1m, + "USD" => UsdInUah, + "EUR" => EurInUah, + _ => 1m, + }; + + return InUah(currencyCode) / InUah(homeCurrency); + } + + private static string CurrencyDisplayName(string code, bool ua) => (code, ua) switch + { + ("UAH", true) => "Гривня", + ("USD", true) => "Долар США", + ("EUR", true) => "Євро", + ("UAH", _) => "Ukrainian hryvnia", + ("USD", _) => "US Dollar", + ("EUR", _) => "Euro", + _ => code, + }; +} diff --git a/MyOffice.DbContext/MyOffice.DbContext.csproj b/MyOffice.DbContext/MyOffice.DbContext.csproj new file mode 100644 index 0000000..b7225cb --- /dev/null +++ b/MyOffice.DbContext/MyOffice.DbContext.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/MyOffice.DbContext/RepositoryInitializer.cs b/MyOffice.DbContext/RepositoryInitializer.cs new file mode 100644 index 0000000..868d5a9 --- /dev/null +++ b/MyOffice.DbContext/RepositoryInitializer.cs @@ -0,0 +1,23 @@ +namespace MyOffice.DbContext; + +using Microsoft.Extensions.DependencyInjection; + +public record ConnectionConfiguration(string Provider, string ConnectionString); + +public static class RepositoryInitializer +{ + /// + /// Registers DbContext DI. Migrate/seed runs via DatabaseInitializerHostedService. + /// remains for design-time . + /// + public static void Initialize( + IServiceCollection services, + ConnectionConfiguration connectionString + ) + { + ConnectionString = connectionString; + services.AddAppDbContext(connectionString); + } + + public static ConnectionConfiguration ConnectionString { get; internal set; } = null!; +} diff --git a/MyOffice.Migration.Postgres/AppDbContextFactoryPostgres.cs b/MyOffice.Migration.Postgres/AppDbContextFactoryPostgres.cs new file mode 100644 index 0000000..93d1748 --- /dev/null +++ b/MyOffice.Migration.Postgres/AppDbContextFactoryPostgres.cs @@ -0,0 +1,27 @@ +namespace MyOffice.Migrations.Postgres; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using MyOffice.DbContext; +using MyOffice.Shared; + +public class AppDbContextFactoryPostgres : IDesignTimeDbContextFactory +{ + public AppDbContext CreateDbContext(string[]? args) + { + var builder = new DbContextOptionsBuilder(); + + var sharedConfiguration = SharedConfiguration.Build(); + var connectionString = + args?.FirstOrDefault() + ?? sharedConfiguration.GetConnectionString("npgsql"); + + builder.UseNpgsql(connectionString, b => b.MigrationsAssembly("MyOffice.Migrations.Postgres")); + + var db = new AppDbContext(AppDbContextProvidersEnum.npgsql, builder.Options); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + return db; + } +} \ No newline at end of file diff --git a/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.Designer.cs new file mode 100644 index 0000000..2c41f26 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.Designer.cs @@ -0,0 +1,608 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230621190024_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Motions.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.Item", b => + { + b.HasOne("MyOffice.Data.Models.Motions.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Motions.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Motions.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.cs b/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.cs new file mode 100644 index 0000000..47a3398 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230621190024_Init.cs @@ -0,0 +1,445 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False"); + + migrationBuilder.CreateTable( + name: "CurrencyGlobals", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Symbol = table.Column(type: "text", nullable: false), + DefaultQuantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurrencyGlobals", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ItemGlobals", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemGlobals", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Accounts", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CurrencyGlobalId = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Accounts", x => x.Id); + table.ForeignKey( + name: "FK_Accounts_CurrencyGlobals_CurrencyGlobalId", + column: x => x.CurrencyGlobalId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserName = table.Column(type: "text", nullable: false, collation: "my_ci_collation"), + Email = table.Column(type: "text", nullable: false, collation: "my_ci_collation"), + PasswordHash = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: true), + LastName = table.Column(type: "text", nullable: true), + FullName = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true), + IsEmailConfirmed = table.Column(type: "boolean", nullable: false), + CurrencyId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_CurrencyGlobals_CurrencyId", + column: x => x.CurrencyId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountAccesses", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + IsAllowRead = table.Column(type: "boolean", nullable: false), + IsAllowWrite = table.Column(type: "boolean", nullable: false), + IsAllowManage = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccesses", x => x.Id); + table.ForeignKey( + name: "FK_AccountAccesses_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccesses_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountCategories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountCategories", x => x.Id); + table.ForeignKey( + name: "FK_AccountCategories_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Currencies", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CurrencyGlobalId = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + ShortName = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Currencies", x => x.Id); + table.ForeignKey( + name: "FK_Currencies_CurrencyGlobals_CurrencyGlobalId", + column: x => x.CurrencyGlobalId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Currencies_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ItemCategories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemCategories", x => x.Id); + table.ForeignKey( + name: "FK_ItemCategories_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false, collation: "my_ci_collation"), + Provider = table.Column(type: "text", nullable: false, collation: "my_ci_collation") + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountAccountCategories", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccountId = table.Column(type: "uuid", nullable: false), + CategoryId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccountCategories", x => x.Id); + table.ForeignKey( + name: "FK_AccountAccountCategories_AccountCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "AccountCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccountCategories_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CurrencyRates", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CurrencyId = table.Column(type: "uuid", nullable: false), + DateTime = table.Column(type: "timestamp with time zone", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + Rate = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurrencyRates", x => x.Id); + table.ForeignKey( + name: "FK_CurrencyRates_Currencies_CurrencyId", + column: x => x.CurrencyId, + principalTable: "Currencies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CategoryId = table.Column(type: "uuid", nullable: false), + ItemGlobalId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Items", x => x.Id); + table.ForeignKey( + name: "FK_Items_ItemCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "ItemCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Items_ItemGlobals_ItemGlobalId", + column: x => x.ItemGlobalId, + principalTable: "ItemGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Motions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + DateTime = table.Column(type: "timestamp with time zone", nullable: false), + AccountId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "integer", nullable: false), + Description = table.Column(type: "text", nullable: true), + AmountPlus = table.Column(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false), + AmountMinus = table.Column(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false), + UserId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Motions", x => x.Id); + table.ForeignKey( + name: "FK_Motions_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Motions_Items_ItemId", + column: x => x.ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Motions_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId", + table: "AccountAccesses", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_UserId", + table: "AccountAccesses", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_AccountId", + table: "AccountAccountCategories", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_CategoryId", + table: "AccountAccountCategories", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountCategories_UserId", + table: "AccountCategories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_CurrencyGlobalId", + table: "Accounts", + column: "CurrencyGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrencyGlobalId", + table: "Currencies", + column: "CurrencyGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_UserId", + table: "Currencies", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_CurrencyRates_CurrencyId", + table: "CurrencyRates", + column: "CurrencyId"); + + migrationBuilder.CreateIndex( + name: "IX_ItemCategories_UserId", + table: "ItemCategories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Items_CategoryId", + table: "Items", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Items_ItemGlobalId", + table: "Items", + column: "ItemGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_AccountId", + table: "Motions", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_ItemId", + table: "Motions", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_UserId", + table: "Motions", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_CurrencyId", + table: "Users", + column: "CurrencyId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountAccesses"); + + migrationBuilder.DropTable( + name: "AccountAccountCategories"); + + migrationBuilder.DropTable( + name: "CurrencyRates"); + + migrationBuilder.DropTable( + name: "Motions"); + + migrationBuilder.DropTable( + name: "UserClaims"); + + migrationBuilder.DropTable( + name: "AccountCategories"); + + migrationBuilder.DropTable( + name: "Currencies"); + + migrationBuilder.DropTable( + name: "Accounts"); + + migrationBuilder.DropTable( + name: "Items"); + + migrationBuilder.DropTable( + name: "ItemCategories"); + + migrationBuilder.DropTable( + name: "ItemGlobals"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "CurrencyGlobals"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.Designer.cs new file mode 100644 index 0000000..529060a --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.Designer.cs @@ -0,0 +1,611 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230627044606_IsInternal")] + partial class IsInternal + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.cs b/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.cs new file mode 100644 index 0000000..65fe3c4 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230627044606_IsInternal.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class IsInternal : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsInternal", + table: "ItemCategories", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsInternal", + table: "ItemCategories"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.Designer.cs new file mode 100644 index 0000000..5a7e7a5 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.Designer.cs @@ -0,0 +1,617 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230720190103_PrimaryCurrencyDeletedMotion")] + partial class PrimaryCurrencyDeletedMotion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.cs b/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.cs new file mode 100644 index 0000000..8dbcb2d --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230720190103_PrimaryCurrencyDeletedMotion.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class PrimaryCurrencyDeletedMotion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedOn", + table: "Motions", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsPrimary", + table: "Currencies", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletedOn", + table: "Motions"); + + migrationBuilder.DropColumn( + name: "IsPrimary", + table: "Currencies"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.Designer.cs new file mode 100644 index 0000000..70a3adf --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.Designer.cs @@ -0,0 +1,631 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230721175243_CurrentRate")] + partial class CurrentRate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("CurrentRateId1") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId1"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany() + .HasForeignKey("CurrentRateId1"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.cs b/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.cs new file mode 100644 index 0000000..c7aef90 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721175243_CurrentRate.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class CurrentRate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CurrentRateId", + table: "Currencies", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "CurrentRateId1", + table: "Currencies", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrentRateId1", + table: "Currencies", + column: "CurrentRateId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId1", + table: "Currencies", + column: "CurrentRateId1", + principalTable: "CurrencyRates", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId1", + table: "Currencies"); + + migrationBuilder.DropIndex( + name: "IX_Currencies_CurrentRateId1", + table: "Currencies"); + + migrationBuilder.DropColumn( + name: "CurrentRateId", + table: "Currencies"); + + migrationBuilder.DropColumn( + name: "CurrentRateId1", + table: "Currencies"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.Designer.cs new file mode 100644 index 0000000..71d3381 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.Designer.cs @@ -0,0 +1,617 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230721180304_CurrentRateRemove")] + partial class CurrentRateRemove + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.cs b/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.cs new file mode 100644 index 0000000..30b99ec --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721180304_CurrentRateRemove.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class CurrentRateRemove : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId1", + table: "Currencies"); + + migrationBuilder.DropIndex( + name: "IX_Currencies_CurrentRateId1", + table: "Currencies"); + + migrationBuilder.DropColumn( + name: "CurrentRateId", + table: "Currencies"); + + migrationBuilder.DropColumn( + name: "CurrentRateId1", + table: "Currencies"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CurrentRateId", + table: "Currencies", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "CurrentRateId1", + table: "Currencies", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrentRateId1", + table: "Currencies", + column: "CurrentRateId1"); + + migrationBuilder.AddForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId1", + table: "Currencies", + column: "CurrentRateId1", + principalTable: "CurrencyRates", + principalColumn: "Id"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.Designer.cs new file mode 100644 index 0000000..aa4d8f7 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.Designer.cs @@ -0,0 +1,633 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230721180851_CurrentRateV2")] + partial class CurrentRateV2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.cs b/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.cs new file mode 100644 index 0000000..e7bb75c --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721180851_CurrentRateV2.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class CurrentRateV2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CurrentRateId", + table: "Currencies", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrentRateId", + table: "Currencies", + column: "CurrentRateId"); + + migrationBuilder.AddForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId", + table: "Currencies", + column: "CurrentRateId", + principalTable: "CurrencyRates", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId", + table: "Currencies"); + + migrationBuilder.DropIndex( + name: "IX_Currencies_CurrentRateId", + table: "Currencies"); + + migrationBuilder.DropColumn( + name: "CurrentRateId", + table: "Currencies"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.Designer.cs new file mode 100644 index 0000000..af69953 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.Designer.cs @@ -0,0 +1,636 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230721200424_AccountType")] + partial class AccountType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.cs b/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.cs new file mode 100644 index 0000000..c29a649 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230721200424_AccountType.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Type", + table: "Accounts", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "Accounts"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.Designer.cs new file mode 100644 index 0000000..2c92520 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.Designer.cs @@ -0,0 +1,637 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230722051335_AccountType2")] + partial class AccountType2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.cs b/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.cs new file mode 100644 index 0000000..09ff120 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230722051335_AccountType2.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountType2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Type", + table: "AccountAccesses", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "AccountAccesses"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.Designer.cs new file mode 100644 index 0000000..d81cbe5 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.Designer.cs @@ -0,0 +1,650 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230722052053_AccountOwner")] + partial class AccountOwner + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.cs b/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.cs new file mode 100644 index 0000000..277a90e --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230722052053_AccountOwner.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountOwner : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OwnerId", + table: "Accounts", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_OwnerId", + table: "Accounts", + column: "OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_Accounts_Users_OwnerId", + table: "Accounts", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Accounts_Users_OwnerId", + table: "Accounts"); + + migrationBuilder.DropIndex( + name: "IX_Accounts_OwnerId", + table: "Accounts"); + + migrationBuilder.DropColumn( + name: "OwnerId", + table: "Accounts"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.Designer.cs new file mode 100644 index 0000000..b92d8a3 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.Designer.cs @@ -0,0 +1,652 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230725054028_Collation")] + partial class Collation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.cs b/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.cs new file mode 100644 index 0000000..7b07646 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725054028_Collation.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class Collation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "ItemGlobals", + type: "text", + nullable: false, + collation: "my_ci_collation", + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Accounts", + type: "text", + nullable: false, + collation: "my_ci_collation", + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "ItemGlobals", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldCollation: "my_ci_collation"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Accounts", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldCollation: "my_ci_collation"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.Designer.cs new file mode 100644 index 0000000..5bbd3dd --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.Designer.cs @@ -0,0 +1,651 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230725061203_CollationR")] + partial class CollationR + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.cs b/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.cs new file mode 100644 index 0000000..ce688f3 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725061203_CollationR.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class CollationR : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Accounts", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldCollation: "my_ci_collation"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Accounts", + type: "text", + nullable: false, + collation: "my_ci_collation", + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.Designer.cs new file mode 100644 index 0000000..f45afe7 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.Designer.cs @@ -0,0 +1,650 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230725061910_CollationR2")] + partial class CollationR2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.cs b/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.cs new file mode 100644 index 0000000..d544478 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230725061910_CollationR2.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class CollationR2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "ItemGlobals", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldCollation: "my_ci_collation"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "ItemGlobals", + type: "text", + nullable: false, + collation: "my_ci_collation", + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.Designer.cs new file mode 100644 index 0000000..4e0b046 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.Designer.cs @@ -0,0 +1,663 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230726160638_AccessOwner")] + partial class AccessOwner + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.cs b/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.cs new file mode 100644 index 0000000..5b12a91 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230726160638_AccessOwner.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccessOwner : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OwnerId", + table: "AccountAccesses", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_OwnerId", + table: "AccountAccesses", + column: "OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses"); + + migrationBuilder.DropIndex( + name: "IX_AccountAccesses_OwnerId", + table: "AccountAccesses"); + + migrationBuilder.DropColumn( + name: "OwnerId", + table: "AccountAccesses"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.Designer.cs new file mode 100644 index 0000000..0615901 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.Designer.cs @@ -0,0 +1,665 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230726161226_AccessOwner2")] + partial class AccessOwner2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.cs b/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.cs new file mode 100644 index 0000000..457656b --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230726161226_AccessOwner2.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccessOwner2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "AccountAccesses", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "AccountAccesses", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + table: "AccountAccesses", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.Designer.cs new file mode 100644 index 0000000..a5d9362 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.Designer.cs @@ -0,0 +1,695 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230728172249_AccountAccessInvites")] + partial class AccountAccessInvites + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.cs b/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.cs new file mode 100644 index 0000000..46bed99 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728172249_AccountAccessInvites.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountAccessInvites : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Name", + table: "AccountAccesses", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "AccountAccessInvites", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + AcceptedOn = table.Column(type: "timestamp with time zone", nullable: true), + RejectedOn = table.Column(type: "timestamp with time zone", nullable: true), + Email = table.Column(type: "text", nullable: false), + IsAllowWrite = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccessInvites", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountAccessInvites"); + + migrationBuilder.DropColumn( + name: "Name", + table: "AccountAccesses"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.Designer.cs new file mode 100644 index 0000000..247cc3b --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.Designer.cs @@ -0,0 +1,713 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230728181916_AccountAccessInvites2")] + partial class AccountAccessInvites2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.cs b/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.cs new file mode 100644 index 0000000..30b2752 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728181916_AccountAccessInvites2.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountAccessInvites2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserId", + table: "AccountAccessInvites", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccessInvites_UserId", + table: "AccountAccessInvites", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_AccountAccessInvites_Users_UserId", + table: "AccountAccessInvites", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AccountAccessInvites_Users_UserId", + table: "AccountAccessInvites"); + + migrationBuilder.DropIndex( + name: "IX_AccountAccessInvites_UserId", + table: "AccountAccessInvites"); + + migrationBuilder.DropColumn( + name: "UserId", + table: "AccountAccessInvites"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.Designer.cs new file mode 100644 index 0000000..4574f4d --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.Designer.cs @@ -0,0 +1,728 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230728192017_AccountAccessInvites3")] + partial class AccountAccessInvites3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.cs b/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.cs new file mode 100644 index 0000000..7f22a0c --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230728192017_AccountAccessInvites3.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountAccessInvites3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccountId", + table: "AccountAccessInvites", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccessInvites_AccountId", + table: "AccountAccessInvites", + column: "AccountId"); + + migrationBuilder.AddForeignKey( + name: "FK_AccountAccessInvites_Accounts_AccountId", + table: "AccountAccessInvites", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AccountAccessInvites_Accounts_AccountId", + table: "AccountAccessInvites"); + + migrationBuilder.DropIndex( + name: "IX_AccountAccessInvites_AccountId", + table: "AccountAccessInvites"); + + migrationBuilder.DropColumn( + name: "AccountId", + table: "AccountAccessInvites"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.Designer.cs new file mode 100644 index 0000000..2015f55 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20230804145453_AccountAccesUnique")] + partial class AccountAccesUnique + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CategoryId"); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.cs b/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.cs new file mode 100644 index 0000000..7b3f55d --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20230804145453_AccountAccesUnique.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class AccountAccesUnique : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AccountAccesses_AccountId", + table: "AccountAccesses"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId_UserId", + table: "AccountAccesses", + columns: new[] { "AccountId", "UserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AccountAccesses_AccountId_UserId", + table: "AccountAccesses"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId", + table: "AccountAccesses", + column: "AccountId"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.Designer.cs new file mode 100644 index 0000000..445508f --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.Designer.cs @@ -0,0 +1,730 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20231217163807_acc_cat_unique")] + partial class acccatunique + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.cs b/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.cs new file mode 100644 index 0000000..fe91a77 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20231217163807_acc_cat_unique.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class acccatunique : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AccountAccountCategories_AccountId", + table: "AccountAccountCategories"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_AccountId_CategoryId", + table: "AccountAccountCategories", + columns: new[] { "AccountId", "CategoryId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AccountAccountCategories_AccountId_CategoryId", + table: "AccountAccountCategories"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_AccountId", + table: "AccountAccountCategories", + column: "AccountId"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.Designer.cs new file mode 100644 index 0000000..86bbaf1 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.Designer.cs @@ -0,0 +1,733 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20231217185452_acc_access_unique")] + partial class accaccessunique + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.cs b/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.cs new file mode 100644 index 0000000..ef3eb44 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20231217185452_acc_access_unique.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class accaccessunique : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId_OwnerId", + table: "AccountAccesses", + columns: new[] { "AccountId", "OwnerId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AccountAccesses_AccountId_OwnerId", + table: "AccountAccesses"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.Designer.cs new file mode 100644 index 0000000..57a8887 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.Designer.cs @@ -0,0 +1,790 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20240107073407_VerificationCode")] + partial class VerificationCode + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("text"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.cs b/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.cs new file mode 100644 index 0000000..0dfbe47 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20240107073407_VerificationCode.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class VerificationCode : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Verifications", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "text", nullable: false), + DestinationType = table.Column(type: "text", nullable: false), + Destination = table.Column(type: "text", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresOn = table.Column(type: "timestamp with time zone", nullable: false), + VerifiedOn = table.Column(type: "timestamp with time zone", nullable: true), + Template = table.Column(type: "text", nullable: false), + Metadata = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Verifications", x => x.Id); + table.ForeignKey( + name: "FK_Verifications_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Verifications_UserId", + table: "Verifications", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Verifications"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.Designer.cs new file mode 100644 index 0000000..afd56e5 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.Designer.cs @@ -0,0 +1,790 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20240107075033_VerificationCode2")] + partial class VerificationCode2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("text"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.cs b/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.cs new file mode 100644 index 0000000..05f4374 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20240107075033_VerificationCode2.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class VerificationCode2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.Designer.cs b/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.Designer.cs new file mode 100644 index 0000000..91858ec --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.Designer.cs @@ -0,0 +1,1067 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260717093113_OpenIddict")] + partial class OpenIddict + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Notifications.EmailTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("text"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.cs b/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.cs new file mode 100644 index 0000000..684ef30 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/20260717093113_OpenIddict.cs @@ -0,0 +1,186 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + /// + public partial class OpenIddict : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EmailTemplates", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "text", nullable: false), + Subject = table.Column(type: "text", nullable: false), + Sender = table.Column(type: "text", nullable: false), + SenderName = table.Column(type: "text", nullable: false), + Template = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EmailTemplates", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ClientId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "text", nullable: true), + ClientType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + JsonWebKeySet = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text", nullable: true), + PostLogoutRedirectUris = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedirectUris = table.Column(type: "text", nullable: true), + Requirements = table.Column(type: "text", nullable: true), + Settings = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "text", nullable: true), + Descriptions = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Properties = table.Column(type: "text", nullable: true), + Resources = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + Properties = table.Column(type: "text", nullable: true), + Scopes = table.Column(type: "text", nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + AuthorizationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + Payload = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedemptionDate = table.Column(type: "timestamp with time zone", nullable: true), + ReferenceId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(150)", maxLength: 150, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EmailTemplates"); + + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + } + } +} diff --git a/MyOffice.Migration.Postgres/Migrations/AppDbContextModelSnapshot.cs b/MyOffice.Migration.Postgres/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..7ffe0d0 --- /dev/null +++ b/MyOffice.Migration.Postgres/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,1064 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MyOffice.Migrations.Postgres.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:my_ci_collation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("IsAllowManage") + .HasColumnType("boolean"); + + b.Property("IsAllowRead") + .HasColumnType("boolean"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAllowWrite") + .HasColumnType("boolean"); + + b.Property("RejectedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("numeric(18,6)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentRateId") + .HasColumnType("integer"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DefaultQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("ItemGlobalId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsInternal") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Notifications.EmailTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text") + .UseCollation("my_ci_collation"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("text"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Postgres/MyOffice.Migrations.Postgres.csproj b/MyOffice.Migration.Postgres/MyOffice.Migrations.Postgres.csproj new file mode 100644 index 0000000..23915d8 --- /dev/null +++ b/MyOffice.Migration.Postgres/MyOffice.Migrations.Postgres.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/MyOffice.Migration.Sqlite/AppDbContextFactorySqlite.cs b/MyOffice.Migration.Sqlite/AppDbContextFactorySqlite.cs new file mode 100644 index 0000000..47d8c45 --- /dev/null +++ b/MyOffice.Migration.Sqlite/AppDbContextFactorySqlite.cs @@ -0,0 +1,27 @@ +namespace MyOffice.Migrations.Sqlite; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using MyOffice.DbContext; +using MyOffice.Shared; + +public class AppDbContextFactorySqlite : IDesignTimeDbContextFactory +{ + public AppDbContext CreateDbContext(string[]? args) + { + var builder = new DbContextOptionsBuilder(); + + var sharedConfiguration = SharedConfiguration.Build(); + var connectionString = + args?.FirstOrDefault() + ?? sharedConfiguration.GetConnectionString("sqlite"); + + builder.UseSqlite(connectionString, b => b.MigrationsAssembly("MyOffice.Migrations.Sqlite")); + + var db = new AppDbContext(AppDbContextProvidersEnum.sqlite, builder.Options); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + return db; + } +} \ No newline at end of file diff --git a/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.Designer.cs b/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.Designer.cs new file mode 100644 index 0000000..2074f6a --- /dev/null +++ b/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.Designer.cs @@ -0,0 +1,1049 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; + +#nullable disable + +namespace MyOffice.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260717093158_OpenIddict")] + partial class OpenIddict + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("IsAllowManage") + .HasColumnType("INTEGER"); + + b.Property("IsAllowRead") + .HasColumnType("INTEGER"); + + b.Property("IsAllowWrite") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AcceptedOn") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsAllowWrite") + .HasColumnType("INTEGER"); + + b.Property("RejectedOn") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("DeletedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CurrentRateId") + .HasColumnType("INTEGER"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DefaultQuantity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CurrencyId") + .HasColumnType("TEXT"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("ItemGlobalId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsInternal") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Notifications.EmailTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("FullName") + .HasColumnType("TEXT"); + + b.Property("IsEmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Phone") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("JsonWebKeySet") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedirectUris") + .HasColumnType("TEXT"); + + b.Property("Requirements") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Scopes") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Descriptions") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Resources") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("AuthorizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Payload") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedemptionDate") + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.cs b/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.cs new file mode 100644 index 0000000..a014ddc --- /dev/null +++ b/MyOffice.Migration.Sqlite/Migrations/20260717093158_OpenIddict.cs @@ -0,0 +1,747 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyOffice.Migrations.Sqlite.Migrations +{ + /// + public partial class OpenIddict : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CurrencyGlobals", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Symbol = table.Column(type: "TEXT", nullable: false), + DefaultQuantity = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurrencyGlobals", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "EmailTemplates", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + Subject = table.Column(type: "TEXT", nullable: false), + Sender = table.Column(type: "TEXT", nullable: false), + SenderName = table.Column(type: "TEXT", nullable: false), + Template = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EmailTemplates", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ItemGlobals", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemGlobals", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ApplicationType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ClientId = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "TEXT", nullable: true), + ClientType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + DisplayNames = table.Column(type: "TEXT", nullable: true), + JsonWebKeySet = table.Column(type: "TEXT", nullable: true), + Permissions = table.Column(type: "TEXT", nullable: true), + PostLogoutRedirectUris = table.Column(type: "TEXT", nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + RedirectUris = table.Column(type: "TEXT", nullable: true), + Requirements = table.Column(type: "TEXT", nullable: true), + Settings = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + Descriptions = table.Column(type: "TEXT", nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + DisplayNames = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + Resources = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", nullable: false, collation: "NOCASE"), + Email = table.Column(type: "TEXT", nullable: false, collation: "NOCASE"), + PasswordHash = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + FullName = table.Column(type: "TEXT", nullable: true), + Phone = table.Column(type: "TEXT", nullable: true), + IsEmailConfirmed = table.Column(type: "INTEGER", nullable: false), + CurrencyId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_CurrencyGlobals_CurrencyId", + column: x => x.CurrencyId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ApplicationId = table.Column(type: "TEXT", nullable: true), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "TEXT", nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + Scopes = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Subject = table.Column(type: "TEXT", maxLength: 400, nullable: true), + Type = table.Column(type: "TEXT", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "AccountCategories", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountCategories", x => x.Id); + table.ForeignKey( + name: "FK_AccountCategories_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Accounts", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CurrencyGlobalId = table.Column(type: "TEXT", nullable: false), + OwnerId = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Accounts", x => x.Id); + table.ForeignKey( + name: "FK_Accounts_CurrencyGlobals_CurrencyGlobalId", + column: x => x.CurrencyGlobalId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Accounts_Users_OwnerId", + column: x => x.OwnerId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ItemCategories", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + IsInternal = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemCategories", x => x.Id); + table.ForeignKey( + name: "FK_ItemCategories_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + ExternalId = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false, collation: "NOCASE"), + Provider = table.Column(type: "TEXT", nullable: false, collation: "NOCASE") + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Verifications", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + Code = table.Column(type: "TEXT", nullable: false), + DestinationType = table.Column(type: "TEXT", nullable: false), + Destination = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + ExpiresOn = table.Column(type: "TEXT", nullable: false), + VerifiedOn = table.Column(type: "TEXT", nullable: true), + Template = table.Column(type: "TEXT", nullable: false), + Metadata = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Verifications", x => x.Id); + table.ForeignKey( + name: "FK_Verifications_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ApplicationId = table.Column(type: "TEXT", nullable: true), + AuthorizationId = table.Column(type: "TEXT", nullable: true), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "TEXT", nullable: true), + ExpirationDate = table.Column(type: "TEXT", nullable: true), + Payload = table.Column(type: "TEXT", nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + RedemptionDate = table.Column(type: "TEXT", nullable: true), + ReferenceId = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Subject = table.Column(type: "TEXT", maxLength: 400, nullable: true), + Type = table.Column(type: "TEXT", maxLength: 150, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "AccountAccesses", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + OwnerId = table.Column(type: "TEXT", nullable: false), + IsAllowRead = table.Column(type: "INTEGER", nullable: false), + IsAllowWrite = table.Column(type: "INTEGER", nullable: false), + IsAllowManage = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccesses", x => x.Id); + table.ForeignKey( + name: "FK_AccountAccesses_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccesses_Users_OwnerId", + column: x => x.OwnerId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccesses_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountAccessInvites", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + AcceptedOn = table.Column(type: "TEXT", nullable: true), + RejectedOn = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: false), + IsAllowWrite = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccessInvites", x => x.Id); + table.ForeignKey( + name: "FK_AccountAccessInvites_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccessInvites_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccountAccountCategories", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(type: "TEXT", nullable: false), + CategoryId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountAccountCategories", x => x.Id); + table.ForeignKey( + name: "FK_AccountAccountCategories_AccountCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "AccountCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AccountAccountCategories_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Items", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CategoryId = table.Column(type: "TEXT", nullable: false), + ItemGlobalId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Items", x => x.Id); + table.ForeignKey( + name: "FK_Items_ItemCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "ItemCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Items_ItemGlobals_ItemGlobalId", + column: x => x.ItemGlobalId, + principalTable: "ItemGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Motions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + DateTime = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + ItemId = table.Column(type: "INTEGER", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + AmountPlus = table.Column(type: "TEXT", precision: 18, scale: 6, nullable: false), + AmountMinus = table.Column(type: "TEXT", precision: 18, scale: 6, nullable: false), + DeletedOn = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Motions", x => x.Id); + table.ForeignKey( + name: "FK_Motions_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Motions_Items_ItemId", + column: x => x.ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Motions_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Currencies", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CurrencyGlobalId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + ShortName = table.Column(type: "TEXT", nullable: false), + CurrentRateId = table.Column(type: "INTEGER", nullable: true), + IsPrimary = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Currencies", x => x.Id); + table.ForeignKey( + name: "FK_Currencies_CurrencyGlobals_CurrencyGlobalId", + column: x => x.CurrencyGlobalId, + principalTable: "CurrencyGlobals", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Currencies_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CurrencyRates", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CurrencyId = table.Column(type: "TEXT", nullable: false), + DateTime = table.Column(type: "TEXT", nullable: false), + Quantity = table.Column(type: "INTEGER", nullable: false), + Rate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CurrencyRates", x => x.Id); + table.ForeignKey( + name: "FK_CurrencyRates_Currencies_CurrencyId", + column: x => x.CurrencyId, + principalTable: "Currencies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId_OwnerId", + table: "AccountAccesses", + columns: new[] { "AccountId", "OwnerId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_AccountId_UserId", + table: "AccountAccesses", + columns: new[] { "AccountId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_OwnerId", + table: "AccountAccesses", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccesses_UserId", + table: "AccountAccesses", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccessInvites_AccountId", + table: "AccountAccessInvites", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccessInvites_UserId", + table: "AccountAccessInvites", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_AccountId_CategoryId", + table: "AccountAccountCategories", + columns: new[] { "AccountId", "CategoryId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AccountAccountCategories_CategoryId", + table: "AccountAccountCategories", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_AccountCategories_UserId", + table: "AccountCategories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_CurrencyGlobalId", + table: "Accounts", + column: "CurrencyGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_OwnerId", + table: "Accounts", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrencyGlobalId", + table: "Currencies", + column: "CurrencyGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_CurrentRateId", + table: "Currencies", + column: "CurrentRateId"); + + migrationBuilder.CreateIndex( + name: "IX_Currencies_UserId", + table: "Currencies", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_CurrencyRates_CurrencyId", + table: "CurrencyRates", + column: "CurrencyId"); + + migrationBuilder.CreateIndex( + name: "IX_ItemCategories_UserId", + table: "ItemCategories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Items_CategoryId", + table: "Items", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Items_ItemGlobalId", + table: "Items", + column: "ItemGlobalId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_AccountId", + table: "Motions", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_ItemId", + table: "Motions", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_Motions_UserId", + table: "Motions", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_CurrencyId", + table: "Users", + column: "CurrencyId"); + + migrationBuilder.CreateIndex( + name: "IX_Verifications_UserId", + table: "Verifications", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId", + table: "Currencies", + column: "CurrentRateId", + principalTable: "CurrencyRates", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Currencies_Users_UserId", + table: "Currencies"); + + migrationBuilder.DropForeignKey( + name: "FK_Currencies_CurrencyGlobals_CurrencyGlobalId", + table: "Currencies"); + + migrationBuilder.DropForeignKey( + name: "FK_Currencies_CurrencyRates_CurrentRateId", + table: "Currencies"); + + migrationBuilder.DropTable( + name: "AccountAccesses"); + + migrationBuilder.DropTable( + name: "AccountAccessInvites"); + + migrationBuilder.DropTable( + name: "AccountAccountCategories"); + + migrationBuilder.DropTable( + name: "EmailTemplates"); + + migrationBuilder.DropTable( + name: "Motions"); + + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "UserClaims"); + + migrationBuilder.DropTable( + name: "Verifications"); + + migrationBuilder.DropTable( + name: "AccountCategories"); + + migrationBuilder.DropTable( + name: "Accounts"); + + migrationBuilder.DropTable( + name: "Items"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "ItemCategories"); + + migrationBuilder.DropTable( + name: "ItemGlobals"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "CurrencyGlobals"); + + migrationBuilder.DropTable( + name: "CurrencyRates"); + + migrationBuilder.DropTable( + name: "Currencies"); + } + } +} diff --git a/MyOffice.Migration.Sqlite/Migrations/AppDbContextModelSnapshot.cs b/MyOffice.Migration.Sqlite/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..6364200 --- /dev/null +++ b/MyOffice.Migration.Sqlite/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,1046 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyOffice.DbContext; + +#nullable disable + +namespace MyOffice.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("IsAllowManage") + .HasColumnType("INTEGER"); + + b.Property("IsAllowRead") + .HasColumnType("INTEGER"); + + b.Property("IsAllowWrite") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.HasIndex("AccountId", "OwnerId") + .IsUnique(); + + b.HasIndex("AccountId", "UserId") + .IsUnique(); + + b.ToTable("AccountAccesses"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AcceptedOn") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsAllowWrite") + .HasColumnType("INTEGER"); + + b.Property("RejectedOn") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("UserId"); + + b.ToTable("AccountAccessInvites"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("AccountId", "CategoryId") + .IsUnique(); + + b.ToTable("AccountAccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccountCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AmountMinus") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("AmountPlus") + .HasPrecision(18, 6) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("DeletedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("UserId"); + + b.ToTable("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyGlobalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CurrentRateId") + .HasColumnType("INTEGER"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyGlobalId"); + + b.HasIndex("CurrentRateId"); + + b.HasIndex("UserId"); + + b.ToTable("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DefaultQuantity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CurrencyGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CurrencyId") + .HasColumnType("TEXT"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("CurrencyRates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("ItemGlobalId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ItemGlobalId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsInternal") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemCategories"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ItemGlobals"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Notifications.EmailTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Sender") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CurrencyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("FullName") + .HasColumnType("TEXT"); + + b.Property("IsEmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Phone") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Destination") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifiedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Verifications"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ClientSecret") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("JsonWebKeySet") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedirectUris") + .HasColumnType("TEXT"); + + b.Property("Requirements") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Scopes") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Descriptions") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DisplayNames") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("Resources") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationId") + .HasColumnType("TEXT"); + + b.Property("AuthorizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Payload") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT"); + + b.Property("RedemptionDate") + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Accounts") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("Accounts") + .HasForeignKey("OwnerId"); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccess", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("AccessRights") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "Owner") + .WithMany("AccountAccessOwners") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccessInvite", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Invites") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountAccessInvites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountAccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Categories") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Accounts.AccountCategory", "Category") + .WithMany("Accounts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("AccountCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Motion", b => + { + b.HasOne("MyOffice.Data.Models.Accounts.Account", "Account") + .WithMany("Motions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.Item", "Item") + .WithMany("Motions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Users.User", null) + .WithMany("AccountMotions") + .HasForeignKey("UserId"); + + b.Navigation("Account"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "CurrencyGlobal") + .WithMany("Currencies") + .HasForeignKey("CurrencyGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyRate", "CurrentRate") + .WithMany("Currencies") + .HasForeignKey("CurrentRateId"); + + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("Currencies") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CurrencyGlobal"); + + b.Navigation("CurrentRate"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.Currency", "Currency") + .WithMany("Rates") + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.HasOne("MyOffice.Data.Models.Items.ItemCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MyOffice.Data.Models.Items.ItemGlobal", "ItemGlobal") + .WithMany("Items") + .HasForeignKey("ItemGlobalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("ItemGlobal"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("ItemCategories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.HasOne("MyOffice.Data.Models.Currencies.CurrencyGlobal", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.UserExternal", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany("UserClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Verifications.VerificationCode", b => + { + b.HasOne("MyOffice.Data.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.Account", b => + { + b.Navigation("AccessRights"); + + b.Navigation("Categories"); + + b.Navigation("Invites"); + + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Accounts.AccountCategory", b => + { + b.Navigation("Accounts"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.Currency", b => + { + b.Navigation("Rates"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyGlobal", b => + { + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Currencies.CurrencyRate", b => + { + b.Navigation("Currencies"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.Item", b => + { + b.Navigation("Motions"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Items.ItemGlobal", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("MyOffice.Data.Models.Users.User", b => + { + b.Navigation("AccountAccess"); + + b.Navigation("AccountAccessInvites"); + + b.Navigation("AccountAccessOwners"); + + b.Navigation("AccountCategories"); + + b.Navigation("AccountMotions"); + + b.Navigation("Accounts"); + + b.Navigation("Currencies"); + + b.Navigation("ItemCategories"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyOffice.Migration.Sqlite/MyOffice.Migrations.Sqlite.csproj b/MyOffice.Migration.Sqlite/MyOffice.Migrations.Sqlite.csproj new file mode 100644 index 0000000..b5732ca --- /dev/null +++ b/MyOffice.Migration.Sqlite/MyOffice.Migrations.Sqlite.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/MyOffice.SPA/.editorconfig b/MyOffice.SPA/.editorconfig new file mode 100644 index 0000000..f99379e --- /dev/null +++ b/MyOffice.SPA/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = tabs +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/MyOffice.SPA/.eslintrc.json b/MyOffice.SPA/.eslintrc.json new file mode 100644 index 0000000..99d2907 --- /dev/null +++ b/MyOffice.SPA/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "root": true, + "ignorePatterns": [ + "projects/**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@angular-eslint/recommended", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ] + } + }, + { + "files": [ + "*.html" + ], + "extends": [ + "plugin:@angular-eslint/template/recommended" + ], + "rules": {} + } + ] +} diff --git a/MyOffice.SPA/.gitignore b/MyOffice.SPA/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/MyOffice.SPA/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/MyOffice.SPA/.npmrc b/MyOffice.SPA/.npmrc new file mode 100644 index 0000000..e9ee3cb --- /dev/null +++ b/MyOffice.SPA/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true \ No newline at end of file diff --git a/MyOffice.SPA/MyOffice.SPA.esproj b/MyOffice.SPA/MyOffice.SPA.esproj new file mode 100644 index 0000000..37d6a55 --- /dev/null +++ b/MyOffice.SPA/MyOffice.SPA.esproj @@ -0,0 +1,44 @@ + + + b02d9868-26cb-4e98-95d8-7286bf255f89 + + + npm start + false + Jasmine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyOffice.SPA/README.md b/MyOffice.SPA/README.md new file mode 100644 index 0000000..747ce2e --- /dev/null +++ b/MyOffice.SPA/README.md @@ -0,0 +1,27 @@ +# MyOffice + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.6. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/MyOffice.SPA/angular.json b/MyOffice.SPA/angular.json new file mode 100644 index 0000000..9ad369d --- /dev/null +++ b/MyOffice.SPA/angular.json @@ -0,0 +1,158 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "MyOffice": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/angular-template", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/silent-refresh.html" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "./node_modules/bootstrap/dist/css/bootstrap.min.css", + "src/assets/scss/style.scss", + "src/assets/scss/theme/all-themes.scss", + "src/styles.scss" + ], + "stylePreprocessorOptions": { + "includePaths": ["node_modules/"] + }, + "scripts": [ + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "docker": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.docker.ts" + } + ], + "outputHashing": "all" + }, + "proxmox": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.proxmox.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "MyOffice:build:production" + }, + "development": { + "buildTarget": "MyOffice:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "src/proxy.conf.js", + "port": 4300, + "open": false + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "MyOffice:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + ], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/MyOffice.SPA/aspnetcore-https.js b/MyOffice.SPA/aspnetcore-https.js new file mode 100644 index 0000000..81a924e --- /dev/null +++ b/MyOffice.SPA/aspnetcore-https.js @@ -0,0 +1,36 @@ +// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate +const fs = require('fs'); +const spawn = require('child_process').spawn; +const path = require('path'); + +const baseFolder = + process.env.APPDATA !== undefined && process.env.APPDATA !== '' + ? `${process.env.APPDATA}/ASP.NET/https` + : `${process.env.HOME}/.aspnet/https`; + +const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; +const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; + +if (!certificateName) { + console.error( + 'Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.'); + process.exit(-1); +} + +const certFilePath = path.join(baseFolder, `${certificateName}.pem`); +const keyFilePath = path.join(baseFolder, `${certificateName}.key`); + +if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { + spawn('dotnet', + [ + 'dev-certs', + 'https', + '--export-path', + certFilePath, + '--format', + 'Pem', + '--no-password', + ], + { stdio: 'inherit', }) + .on('exit', (code) => process.exit(code)); +} diff --git a/MyOffice.SPA/karma.conf.js b/MyOffice.SPA/karma.conf.js new file mode 100644 index 0000000..b5ce0eb --- /dev/null +++ b/MyOffice.SPA/karma.conf.js @@ -0,0 +1,45 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/angular-template'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/MyOffice.SPA/nuget.config b/MyOffice.SPA/nuget.config new file mode 100644 index 0000000..04437b9 --- /dev/null +++ b/MyOffice.SPA/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/MyOffice.SPA/package.json b/MyOffice.SPA/package.json new file mode 100644 index 0000000..92dfe34 --- /dev/null +++ b/MyOffice.SPA/package.json @@ -0,0 +1,75 @@ +{ + "name": "angular-template", + "version": "0.0.0", + "scripts": { + "ng": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js", + "start": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js serve", + "start-ssl": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js serve --ssl --ssl-cert %APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem --ssl-key %APPDATA%\\ASP.NET\\https\\%npm_package_name%.key", + "build": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js build", + "watch": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js build --watch --configuration development", + "test": "node --disable-warning=DEP0060 ./node_modules/@angular/cli/bin/ng.js test" + }, + "private": true, + "dependencies": { + "@abacritt/angularx-social-login": "^2.1.0", + "@angular/animations": "^18.2.14", + "@angular/cdk": "^18.2.14", + "@angular/common": "^18.2.14", + "@angular/compiler": "^18.2.14", + "@angular/core": "^18.2.14", + "@angular/forms": "^18.2.14", + "@angular/material": "^18.2.14", + "@angular/platform-browser": "^18.2.14", + "@angular/platform-browser-dynamic": "^18.2.14", + "@angular/router": "^18.2.14", + "@auth0/auth0-angular": "2.2.3", + "@ngx-loading-bar/core": "^6.0.2", + "@ngx-loading-bar/http-client": "^6.0.2", + "@ngx-loading-bar/router": "^6.0.2", + "@ngx-translate/core": "^14.0.0", + "@ngx-translate/http-loader": "^7.0.0", + "@popperjs/core": "^2.11.6", + "angular-animations": "^0.11.0", + "angular-feather": "^6.5.0", + "angular-oauth2-oidc": "^18.0.0", + "apexcharts": "^3.37.0", + "bootstrap": "^5.2.3", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "ng-apexcharts": "^1.11.0", + "ngx-currency": "^4.0.0", + "ngx-mask": "^18.0.4", + "ngx-scrollbar": "^13.0.3", + "rxjs": "~7.8.0", + "subsink": "^1.0.2", + "sweetalert2": "^11.4.8", + "tslib": "^2.3.0", + "xlsx": "^0.18.5", + "zone.js": "^0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.14", + "@angular-eslint/builder": "^18.4.3", + "@angular-eslint/eslint-plugin": "^18.4.3", + "@angular-eslint/eslint-plugin-template": "^18.4.3", + "@angular-eslint/schematics": "^18.4.3", + "@angular-eslint/template-parser": "^18.4.3", + "@angular/cli": "^18.2.14", + "@angular/compiler-cli": "^18.2.14", + "@types/jasmine": "~4.3.0", + "@types/lodash": "^4.14.196", + "@typescript-eslint/eslint-plugin": "5.48.2", + "@typescript-eslint/parser": "5.48.2", + "eslint": "^8.33.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "^5.5.4" + }, + "overrides": { + "@auth0/auth0-spa-js": "2.1.3" + } +} diff --git a/MyOffice.SPA/src/app/api-routes.ts b/MyOffice.SPA/src/app/api-routes.ts new file mode 100644 index 0000000..d795b5c --- /dev/null +++ b/MyOffice.SPA/src/app/api-routes.ts @@ -0,0 +1,46 @@ +import { environment } from '../environments/environment'; + +const api = (environment.apiUrl ?? '').replace(/\/$/, ''); + +export class ApiRoutes { + static UserRegister = `${api}/api/user/register`; + static UserLogin = `${api}/api/user/login`; + static UserProfile = `${api}/api/user/profile`; + static UserAttach = `${api}/api/user/attach`; + static UserDeattach = `${api}/api/user/deattach`; + + static GeneralCurrencies = `${api}/api/general/currencies`; + static SettingsCurrencies = `${api}/api/settings/currencies`; + static SettingsCurrency = `${api}/api/settings/currencies/:id`; + static SettingsCurrenciesRate = `${api}/api/settings/currencies/:id/rate`; + + static SettingsAccountCategories = `${api}/api/settings/account-categories`; + static SettingsAccountCategory = `${api}/api/settings/account-categories/:id`; + + static SettingsAccounts = `${api}/api/settings/accounts`; + static SettingsAccount = `${api}/api/settings/accounts/:id`; + static SettingsAccountAccesses = `${api}/api/settings/accounts/:id/access`; + static SettingsAccountAccess = `${api}/api/settings/accounts/:id/access/:access`; + static SettingsAccountAccountCategory = `${api}/api/settings/accounts/:id/category/:categoryId`; + static SettingsAccountInvites = `${api}/api/settings/accounts/invites`; + static SettingsAccountInviteAccept = `${api}/api/settings/accounts/invites/:id/accept`; + static SettingsAccountInviteReject = `${api}/api/settings/accounts/invites/:id/reject`; + + static SettingsItemCategories = `${api}/api/settings/item-categories`; + static SettingsItemCategory = `${api}/api/settings/item-categories/:id`; + + static SettingsItems = `${api}/api/settings/items`; + static SettingsItem = `${api}/api/settings/items/:id`; + + static Accounts = `${api}/api/accounts`; + static Account = `${api}/api/accounts/:id`; + + static Motions = `${api}/api/accounts/:id/motions`; + static Motion = `${api}/api/accounts/:id/motions/:motionId`; + + static Items = `${api}/api/items`; + + static Dashboard = `${api}/api/dashboard`; + static DashboardIncome = `${api}/api/dashboard/income`; + static DashboardOutcome = `${api}/api/dashboard/outcome`; +} diff --git a/MyOffice.SPA/src/app/app-routing.module.ts b/MyOffice.SPA/src/app/app-routing.module.ts new file mode 100644 index 0000000..6e21dbb --- /dev/null +++ b/MyOffice.SPA/src/app/app-routing.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { Page404Component } from './authentication/page404/page404.component'; +import { AuthGuard } from './core/guard/auth.guard'; +import { AuthLayoutComponent } from './layout/app-layout/auth-layout/auth-layout.component'; +import { MainLayoutComponent } from './layout/app-layout/main-layout/main-layout.component'; + +const routes: Routes = [ + { + path: '', + component: MainLayoutComponent, + canActivate: [AuthGuard], + children: [ + { path: '', redirectTo: '/authentication/signin', pathMatch: 'full' }, + { + path: 'dashboard', + loadChildren: () => + import('./dashboard/dashboard.module').then((m) => m.DashboardModule), + }, + ], + }, + { + path: 'authentication', + component: AuthLayoutComponent, + loadChildren: () => + import('./authentication/authentication.module').then( + (m) => m.AuthenticationModule + ), + }, + { + path: '', + component: MainLayoutComponent, + canActivate: [AuthGuard], + loadChildren: () => + import('./pages/pages.module').then( + (m) => m.PagesModule + ), + }, + { path: '**', component: Page404Component }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {})], + exports: [RouterModule], +}) +export class AppRoutingModule { +} diff --git a/MyOffice.SPA/src/app/app.component.html b/MyOffice.SPA/src/app/app.component.html new file mode 100644 index 0000000..7e31ebd --- /dev/null +++ b/MyOffice.SPA/src/app/app.component.html @@ -0,0 +1,2 @@ + + diff --git a/MyOffice.SPA/src/app/app.component.scss b/MyOffice.SPA/src/app/app.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/app.component.spec.ts b/MyOffice.SPA/src/app/app.component.spec.ts new file mode 100644 index 0000000..39d0388 --- /dev/null +++ b/MyOffice.SPA/src/app/app.component.spec.ts @@ -0,0 +1,39 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', + () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', + () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'spire'`, + () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('spire'); + }); + + it('should render title', + () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain('spire app is running!'); + }); + }); diff --git a/MyOffice.SPA/src/app/app.component.ts b/MyOffice.SPA/src/app/app.component.ts new file mode 100644 index 0000000..06e14b7 --- /dev/null +++ b/MyOffice.SPA/src/app/app.component.ts @@ -0,0 +1,30 @@ +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Event, Router, RouterOutlet, NavigationStart, NavigationEnd } from '@angular/router'; +import { PageLoaderComponent } from './layout/page-loader/page-loader.component'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + standalone: true, + imports: [RouterOutlet, PageLoaderComponent], +}) +export class AppComponent { + currentUrl!: string; + private readonly destroyRef = inject(DestroyRef); + + constructor(public _router: Router) { + this._router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((routerEvent: Event) => { + if (routerEvent instanceof NavigationStart) { + this.currentUrl = routerEvent.url.substring( + routerEvent.url.lastIndexOf('/') + 1 + ); + } + if (routerEvent instanceof NavigationEnd) { + /* empty */ + } + window.scrollTo(0, 0); + }); + } +} diff --git a/MyOffice.SPA/src/app/app.config.ts b/MyOffice.SPA/src/app/app.config.ts new file mode 100644 index 0000000..4fd0b7f --- /dev/null +++ b/MyOffice.SPA/src/app/app.config.ts @@ -0,0 +1,47 @@ +import { ApplicationConfig, importProvidersFrom } from '@angular/core'; +import { LocationStrategy, HashLocationStrategy } from '@angular/common'; +import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +// libs +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { LoadingBarRouterModule } from '@ngx-loading-bar/router'; +import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'; +import { NgScrollbarModule } from 'ngx-scrollbar'; + +// app +import { CoreModule } from './core/core.module'; +import { AppRoutingModule } from './app-routing.module'; +import { ErrorInterceptor } from './core/interceptor/error.interceptor'; +import { UpdateDateHttpInterceptor } from './core/interceptor/update.date.http.interceptor '; + +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, 'assets/i18n/', '.json'); +} + +export const appConfig: ApplicationConfig = { + providers: [ + importProvidersFrom( + BrowserAnimationsModule, + AppRoutingModule, + LoadingBarHttpClientModule, + LoadingBarRouterModule, + NgScrollbarModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient], + }, + }), + // core & shared + CoreModule, + ), + { provide: LocationStrategy, useClass: HashLocationStrategy }, + // Bearer tokens: angular-oauth2-oidc resourceServer (AuthModuleConfig.sendAccessToken) + { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: UpdateDateHttpInterceptor, multi: true }, + provideHttpClient(withInterceptorsFromDi()), + ], +}; diff --git a/MyOffice.SPA/src/app/authentication/authentication-routing.module.ts b/MyOffice.SPA/src/app/authentication/authentication-routing.module.ts new file mode 100644 index 0000000..ad46f3d --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/authentication-routing.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { SigninComponent } from './signin/signin.component'; +import { SignupComponent } from './signup/signup.component'; +import { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; +import { LockedComponent } from './locked/locked.component'; +import { Page404Component } from './page404/page404.component'; +import { Page500Component } from './page500/page500.component'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'signin', + pathMatch: 'full', + }, + { + path: 'signin', + component: SigninComponent, + }, + { + path: 'signup', + component: SignupComponent, + }, + { + path: 'forgot-password', + component: ForgotPasswordComponent, + }, + { + path: 'locked', + component: LockedComponent, + }, + { + path: 'page404', + component: Page404Component, + }, + { + path: 'page500', + component: Page500Component, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AuthenticationRoutingModule { +} diff --git a/MyOffice.SPA/src/app/authentication/authentication.module.ts b/MyOffice.SPA/src/app/authentication/authentication.module.ts new file mode 100644 index 0000000..0886973 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/authentication.module.ts @@ -0,0 +1,50 @@ +// angular +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; + +// libs +import { GoogleSigninButtonModule } from '@abacritt/angularx-social-login'; + +// app +import { AuthenticationRoutingModule } from './authentication-routing.module'; +import { Page500Component } from './page500/page500.component'; +import { Page404Component } from './page404/page404.component'; +import { SigninComponent } from './signin/signin.component'; +import { SignupComponent } from './signup/signup.component'; +import { LockedComponent } from './locked/locked.component'; +import { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + AuthenticationRoutingModule, + MatFormFieldModule, + MatInputModule, + MatIconModule, + MatButtonModule, + GoogleSigninButtonModule, + ], + declarations: [ + Page500Component, + Page404Component, + SigninComponent, + SignupComponent, + LockedComponent, + ForgotPasswordComponent, + ], + providers: [ + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] +}) +export class AuthenticationModule { +} diff --git a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.html b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.html new file mode 100644 index 0000000..68d6049 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.html @@ -0,0 +1,45 @@ +
+
+
+
+
+
+
+
+
+

Reset Password

+ +
+
+
+ + Enter your registered email address. + + + Email + + mail + + Please enter a valid email address + + +
+
+
+ +
+
+ +
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.scss b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.spec.ts b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.spec.ts new file mode 100644 index 0000000..cc3bdc0 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ForgotPasswordComponent } from './forgot-password.component'; +describe('ForgotPasswordComponent', + () => { + let component: ForgotPasswordComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ForgotPasswordComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(ForgotPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts new file mode 100644 index 0000000..eaac269 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { + FormBuilder, + FormControl, + FormGroup, + Validators, +} from '@angular/forms'; + +type ForgotPasswordForm = { + email: FormControl; +}; + +@Component({ + selector: 'app-forgot-password', + templateUrl: './forgot-password.component.html', + styleUrls: ['./forgot-password.component.scss'], +}) +export class ForgotPasswordComponent implements OnInit { + authForm!: FormGroup; + submitted = false; + returnUrl!: string; + + constructor( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router + ) { + } + + ngOnInit() { + this.authForm = this.formBuilder.group({ + email: [ + '', + [Validators.required, Validators.email, Validators.minLength(5)], + ], + }); + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + } + + get f() { + return this.authForm.controls; + } + + onSubmit() { + this.submitted = true; + // stop here if form is invalid + if (this.authForm.invalid) { + return; + } else { + this.router.navigate(['/dashboard/rests']); + } + } +} diff --git a/MyOffice.SPA/src/app/authentication/locked/locked.component.html b/MyOffice.SPA/src/app/authentication/locked/locked.component.html new file mode 100644 index 0000000..a0db47e --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/locked/locked.component.html @@ -0,0 +1,58 @@ +
+
+
+
+
+
+
+
+
+
+
+
+ User +
+
+ + {{userFullName}} + +
+

+ Locked +

+
+
+
+ + Enter your password here. + + + Password + + + {{hide ? 'visibility_off' : 'visibility'}} + + + Password is required + + +
+
+
+ +
+ +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/locked/locked.component.scss b/MyOffice.SPA/src/app/authentication/locked/locked.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/locked/locked.component.spec.ts b/MyOffice.SPA/src/app/authentication/locked/locked.component.spec.ts new file mode 100644 index 0000000..88664c6 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/locked/locked.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { LockedComponent } from './locked.component'; +describe('LockedComponent', + () => { + let component: LockedComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [LockedComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(LockedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/locked/locked.component.ts b/MyOffice.SPA/src/app/authentication/locked/locked.component.ts new file mode 100644 index 0000000..92a034a --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/locked/locked.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AuthService } from 'src/app/core/service/auth.service'; + +type LockedForm = { + password: FormControl; +}; + +@Component({ + selector: 'app-locked', + templateUrl: './locked.component.html', + styleUrls: ['./locked.component.scss'], +}) +export class LockedComponent implements OnInit { + authForm!: FormGroup; + submitted = false; + userImg!: string; + userFullName!: string; + hide = true; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private authService: AuthService + ) { + } + + ngOnInit() { + this.authForm = this.formBuilder.group({ + password: ['', Validators.required], + }); + + this.userImg = this.authService.currentUserValue.img || 'assets/images/user/admin.jpg'; + + this.userFullName = + this.authService.currentUserValue.firstName + + ' ' + + this.authService.currentUserValue.lastName; + } + + get f() { + return this.authForm.controls; + } + + onSubmit() { + this.submitted = true; + // stop here if form is invalid + if (this.authForm.invalid) { + return; + } else { + this.router.navigate(['/dashboard/rests']); + } + } +} diff --git a/MyOffice.SPA/src/app/authentication/page404/page404.component.html b/MyOffice.SPA/src/app/authentication/page404/page404.component.html new file mode 100644 index 0000000..29e5aeb --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page404/page404.component.html @@ -0,0 +1,37 @@ +
+
+
+
+
+
+
+
+
+
+ + 404 + + + Looks Like You're Lost + + + The Page You Are Looking For Not Available! + +
+ +
+ +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/page404/page404.component.scss b/MyOffice.SPA/src/app/authentication/page404/page404.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/page404/page404.component.spec.ts b/MyOffice.SPA/src/app/authentication/page404/page404.component.spec.ts new file mode 100644 index 0000000..f4a6b82 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page404/page404.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Page404Component } from './page404.component'; + +describe('Page404Component', + () => { + let component: Page404Component; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [Page404Component], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(Page404Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/page404/page404.component.ts b/MyOffice.SPA/src/app/authentication/page404/page404.component.ts new file mode 100644 index 0000000..f3ba68c --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page404/page404.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-page404', + templateUrl: './page404.component.html', + styleUrls: ['./page404.component.scss'], +}) +export class Page404Component { + constructor() { + // constructor + } +} diff --git a/MyOffice.SPA/src/app/authentication/page500/page500.component.html b/MyOffice.SPA/src/app/authentication/page500/page500.component.html new file mode 100644 index 0000000..ce00254 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page500/page500.component.html @@ -0,0 +1,34 @@ +
+
+
+
+
+
+
+
+
+
+ + 500 + + + Oops, Something went wrong. Please try after some times. + +
+ +
+ +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/page500/page500.component.scss b/MyOffice.SPA/src/app/authentication/page500/page500.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/page500/page500.component.spec.ts b/MyOffice.SPA/src/app/authentication/page500/page500.component.spec.ts new file mode 100644 index 0000000..adaba77 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page500/page500.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Page500Component } from './page500.component'; + +describe('Page500Component', + () => { + let component: Page500Component; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [Page500Component], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(Page500Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/page500/page500.component.ts b/MyOffice.SPA/src/app/authentication/page500/page500.component.ts new file mode 100644 index 0000000..589f767 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/page500/page500.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-page500', + templateUrl: './page500.component.html', + styleUrls: ['./page500.component.scss'], +}) +export class Page500Component { + constructor() { + // constructor + } +} diff --git a/MyOffice.SPA/src/app/authentication/signin/signin.component.html b/MyOffice.SPA/src/app/authentication/signin/signin.component.html new file mode 100644 index 0000000..5493ecf --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signin/signin.component.html @@ -0,0 +1,114 @@ +
+
+
+
+
+
+
+
+
+

+
Welcome to ase.com.ua
+
angular/asp.net core template
+

+ + +
+
+
+ + Email + + mail + + Email is required + + +
+
+
+
+ + Password + + + + {{hide ? 'visibility_off' : 'visibility'}} + + + + Password is required + + +
+
+
+
+ +
+ Forgot Password? +
+ +
{{error}}
+ +
+
+ +
+
+
+ + +
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/signin/signin.component.scss b/MyOffice.SPA/src/app/authentication/signin/signin.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/signin/signin.component.spec.ts b/MyOffice.SPA/src/app/authentication/signin/signin.component.spec.ts new file mode 100644 index 0000000..639f76b --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signin/signin.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { SigninComponent } from './signin.component'; + +describe('SigninComponent', + () => { + let component: SigninComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [SigninComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(SigninComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/signin/signin.component.ts b/MyOffice.SPA/src/app/authentication/signin/signin.component.ts new file mode 100644 index 0000000..374e5d3 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signin/signin.component.ts @@ -0,0 +1,117 @@ +// angular +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; + +// libs +import { SocialAuthService } from '@abacritt/angularx-social-login'; + +// app +import { AuthService } from 'src/app/core/service/auth.service'; +import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter'; +import { getSafeRedirectUrl } from 'src/app/core/utils/safe-redirect'; +import { environment } from '../../../environments/environment'; + +type SigninForm = { + username: FormControl; + password: FormControl; +}; + +@Component({ + selector: 'app-signin', + templateUrl: './signin.component.html', + styleUrls: ['./signin.component.scss'], +}) +export class SigninComponent extends UnsubscribeOnDestroyAdapter +implements OnInit { + authForm!: FormGroup; + submitted = false; + loading = false; + error?= ''; + hide = true; + allowGoogle: boolean; + allowAuth: boolean; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private route: ActivatedRoute, + private authService: AuthService, + ) { + super(); + + this.allowGoogle = !!(environment.externalLogins && + environment.externalLogins.google && + environment.externalLogins.google.clientId); + + this.allowAuth = !!(environment.externalLogins && + environment.externalLogins.auth0 && + environment.externalLogins.auth0.clientId); + } + + ngOnInit() { + this.authForm = this.formBuilder.group({ + username: ['', Validators.required], + password: ['', Validators.required], + }); + + this.subs.sink = this.authService.isAuthenticated$.subscribe(isAuthenticated => { + if (isAuthenticated) { + this.router.navigate([this.getRedirect() || '/dashboard/rests']); + } + }); + } + + onSubmit() { + if (this.authForm.invalid) { + this.error = 'Username and Password not valid!'; + return; + } + + this.submitted = true; + this.loading = true; + this.error = ''; + + var user = { + username: this.authForm.controls.username.value!, + password: this.authForm.controls.password.value! + }; + + this.subs.sink = this.authService + .login(user) + .subscribe({ + next: (resp) => { + if (!resp.success) { + this.error = 'Invalid Login'; + if (!resp.success && resp.error?.code === 'invalid_grant') { + this.error = 'Invalid username or password'; + } + this.submitted = false; + this.loading = false; + } + }, + error: (error) => { + this.error = 'Invalid username or password'; + this.submitted = false; + this.loading = false; + }, + }); + } + + loginAuth0() { + this.subs.sink = this.authService.loginAuth0().subscribe(resp => { + if (!resp.success) { + this.error = 'Invalid login'; + } + }); + } + + private getRedirect(): string | undefined { + const raw = this.route.snapshot.queryParams['r'] as string | undefined; + if (!raw) { + return undefined; + } + const safe = getSafeRedirectUrl(raw, ''); + return safe || undefined; + } +} diff --git a/MyOffice.SPA/src/app/authentication/signup/signup.component.html b/MyOffice.SPA/src/app/authentication/signup/signup.component.html new file mode 100644 index 0000000..e9eb396 --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signup/signup.component.html @@ -0,0 +1,128 @@ +
+
+
+
+
+
+
+
+
+

Sign Up

+ +
+
+
+ + Email + + mail + + Please enter a valid email address + + +
+
+
+
+ + Password + + + + {{hide ? 'visibility_off' : 'visibility'}} + + + + Password is required + + + Password must be at least 8 characters + + + Use upper, lower, digit and special character + + +
+
+
+
+ + Confirm Password + + + + {{chide ? 'visibility_off' : 'visibility'}} + + + + Confirm Password is required + + +
+
+
+
+ Passwords do not match +
+
+
+
+ {{error.description}} +
+
+
+
+ + Already Registered? + + Login + + +
+
+
+ +
+
+ + +
+
+
+
+
diff --git a/MyOffice.SPA/src/app/authentication/signup/signup.component.scss b/MyOffice.SPA/src/app/authentication/signup/signup.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/authentication/signup/signup.component.spec.ts b/MyOffice.SPA/src/app/authentication/signup/signup.component.spec.ts new file mode 100644 index 0000000..e43607c --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signup/signup.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { SignupComponent } from './signup.component'; + +describe('SignupComponent', + () => { + let component: SignupComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [SignupComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(SignupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/authentication/signup/signup.component.ts b/MyOffice.SPA/src/app/authentication/signup/signup.component.ts new file mode 100644 index 0000000..ceb5d1a --- /dev/null +++ b/MyOffice.SPA/src/app/authentication/signup/signup.component.ts @@ -0,0 +1,113 @@ +import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Router, ActivatedRoute } from '@angular/router'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { GeneralErrorModel } from '../../core/models/general-error.model'; +import { AuthService } from '../../core/service/auth.service'; + +type SignupForm = { + email: FormControl; + password: FormControl; + cpassword: FormControl; +}; + +@Component({ + selector: 'app-signup', + templateUrl: './signup.component.html', + styleUrls: ['./signup.component.scss'], +}) +export class SignupComponent implements OnInit { + authForm!: FormGroup; + submitted = false; + returnUrl!: string; + hide = true; + chide = true; + generalError?: GeneralErrorModel; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private authService: AuthService + ) { + + } + + ngOnInit() { + this.authForm = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email, Validators.minLength(5)]], + password: [ + '', + [ + Validators.required, + Validators.minLength(8), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).+$/), + ], + ], + cpassword: ['', Validators.required], + }, { + validators: (group: AbstractControl) => { + const password = group.get('password')?.value; + const confirm = group.get('cpassword')?.value; + return password && confirm && password !== confirm + ? { passwordMismatch: true } + : null; + }, + }); + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + } + + get f() { + return this.authForm.controls; + } + + onSubmit() { + this.submitted = true; + // stop here if form is invalid + if (this.authForm.invalid) { + return; + } + + if (this.authForm.hasError('passwordMismatch')) { + return; + } + + this.authService.register({ + username: this.authForm.get('email')!.value!, + password: this.authForm.get('password')!.value!, + confirmPassword: this.authForm.get('cpassword')!.value!, + }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe( + resp => { + if (!resp.succeeded) { + this.generalError = resp as GeneralErrorModel; + return; + } + + // Auto sign-in with email+password after successful register + this.authService.login({ + username: this.authForm.get('email')!.value!, + password: this.authForm.get('password')!.value!, + }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(login => { + if (login.success) { + this.router.navigate([this.returnUrl || '/dashboard/rests']); + } else { + this.router.navigate(['authentication/signin']); + } + }); + }, + err => { + this.generalError = err.error as GeneralErrorModel; + } + ); + } + + loginAuth0() { + this.authService.loginAuth0() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + }); + } +} diff --git a/MyOffice.SPA/src/app/config.external-login.ts b/MyOffice.SPA/src/app/config.external-login.ts new file mode 100644 index 0000000..bccd451 --- /dev/null +++ b/MyOffice.SPA/src/app/config.external-login.ts @@ -0,0 +1,66 @@ +import { SocialAuthServiceConfig } from '@abacritt/angularx-social-login'; +import { GoogleLoginProvider } from '@abacritt/angularx-social-login'; +import { FacebookLoginProvider } from '@abacritt/angularx-social-login'; +import { MicrosoftLoginProvider } from '@abacritt/angularx-social-login'; + +import { environment } from '../environments/environment'; + +export class ExternalLoginConfig { + + static readonly GOOGLE = 'google'; + static readonly AUTH0 = 'auth0'; + static readonly FACEBOOK = 'facebook'; + static readonly MICROSOFT = 'microsoft'; + + static getConfiguredProviders() { + return [ + { + provider: ExternalLoginConfig.GOOGLE, + name: 'Google', + }, + { + provider: ExternalLoginConfig.AUTH0, + name: 'Auth0', + }, + /*{ + provider: ExternalLoginConfig.FACEBOOK, + name: "FaceBook", + }, + { + provider: ExternalLoginConfig.MICROSOFT, + name: "Microsoft", + },*/ + ]; + } + + static getSocialConfig(): SocialAuthServiceConfig { + var providers = []; + + if (environment.externalLogins && + environment.externalLogins.google && + environment.externalLogins.google.clientId + ) { + providers.push({ + id: GoogleLoginProvider.PROVIDER_ID, + provider: new GoogleLoginProvider( + environment.externalLogins.google.clientId, + { scopes: 'email', } + ) + }); + } + return { + autoLogin: false, + providers: providers, + onError: (err) => { + console.error(err); + } + }; + } + + static getAuth0Config() { + return { + clientId: environment.externalLogins.auth0.clientId, + domain: environment.externalLogins.auth0.domain, + }; + } +} diff --git a/MyOffice.SPA/src/app/config.oidc.ts b/MyOffice.SPA/src/app/config.oidc.ts new file mode 100644 index 0000000..292097b --- /dev/null +++ b/MyOffice.SPA/src/app/config.oidc.ts @@ -0,0 +1,61 @@ +import { AuthConfig } from 'angular-oauth2-oidc'; +import { OAuthModuleConfig } from 'angular-oauth2-oidc'; + +import { environment } from '../environments/environment'; + +/** Public origin for same-origin deploys when environment URLs are left empty. */ +function publicOrigin(): string { + return window.location.origin; +} + +function resolveIdentityServer(): string { + const configured = (environment.identityServer ?? '').trim(); + if (configured) { + return configured.endsWith('/') ? configured : `${configured}/`; + } + return `${publicOrigin()}/`; +} + +function resolveAllowedUrls(): string[] { + const configured = environment.allowedUrls ?? []; + if (configured.length > 0) { + return configured; + } + const origin = publicOrigin(); + const api = (environment.apiUrl ?? '').trim().replace(/\/$/, ''); + return api ? [origin, api] : [origin]; +} + +export const AuthCodeFlowConfig: AuthConfig = { + // Url of the Identity Provider + issuer: resolveIdentityServer(), + // Local/Docker HTTP demos can set environment.requireHttps = false. + requireHttps: (environment as { requireHttps?: boolean }).requireHttps ?? environment.production, + strictDiscoveryDocumentValidation: + (environment as { requireHttps?: boolean }).requireHttps ?? environment.production, + + // URL of the SPA to redirect the user to after login + redirectUri: window.location.origin + '/', + // Password/external grants issue refresh tokens; no cookie session for iframe silent refresh. + useSilentRefresh: false, + sessionChecksEnabled: false, + timeoutFactor: 0.75, + + clientId: 'angulartemplate_spa', + + // Authorization Code + PKCE for interactive OIDC; email/password still uses ROPC (see docs/AUTH.md). + responseType: 'code', + disablePKCE: false, + + // offline_access → refresh_token (used by setupAutomaticSilentRefresh) + scope: 'openid profile email api offline_access', + + showDebugInformation: !environment.production, +}; + +export const AuthModuleConfig: OAuthModuleConfig = { + resourceServer: { + allowedUrls: resolveAllowedUrls(), + sendAccessToken: true, + } +}; diff --git a/MyOffice.SPA/src/app/config/config.service.ts b/MyOffice.SPA/src/app/config/config.service.ts new file mode 100644 index 0000000..01aa1b5 --- /dev/null +++ b/MyOffice.SPA/src/app/config/config.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { InConfiguration } from '../core/models/config.interface'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfigService { + configData!: InConfiguration; + + constructor() { + this.setConfigData(); + } + + setConfigData() { + this.configData = { + layout: { + rtl: false, // options: true & false + variant: 'light', // options: light & dark + theme_color: 'white', // options: white, black, purple, blue, cyan, green, orange + logo_bg_color: 'white', // options: white, black, purple, blue, cyan, green, orange + sidebar: { + collapsed: false, // options: true & false + backgroundColor: 'light', // options: light & dark + }, + }, + }; + } +} diff --git a/MyOffice.SPA/src/app/config/ui.config.ts b/MyOffice.SPA/src/app/config/ui.config.ts new file mode 100644 index 0000000..d379726 --- /dev/null +++ b/MyOffice.SPA/src/app/config/ui.config.ts @@ -0,0 +1,7 @@ +/** Placement of Cancel/Save (and similar) actions in Material dialogs. */ +export type ModalButtonsPosition = 'top' | 'bottom'; + +export const UI_CONFIG = { + /** Dialog action buttons: `top` (next to title) or `bottom` (under content). */ + modal_buttons: 'top' as ModalButtonsPosition, +}; diff --git a/MyOffice.SPA/src/app/core/core.module.ts b/MyOffice.SPA/src/app/core/core.module.ts new file mode 100644 index 0000000..af58986 --- /dev/null +++ b/MyOffice.SPA/src/app/core/core.module.ts @@ -0,0 +1,63 @@ +// angular +import { NgModule } from '@angular/core'; +import { Optional } from '@angular/core'; +import { SkipSelf } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { APP_INITIALIZER } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// libs +import { OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc'; +import { AuthConfig } from 'angular-oauth2-oidc'; +import { AuthModule } from '@auth0/auth0-angular'; +import { SocialLoginModule } from '@abacritt/angularx-social-login'; + +// app +import { AuthGuard } from './guard/auth.guard'; +import { RightSidebarService } from './service/rightsidebar.service'; +import { AuthService, authAppInitializerFactory } from './service/auth.service'; +import { DirectionService } from './service/direction.service'; +import { throwIfAlreadyLoaded } from './guard/module-import.guard'; +import { OidcHelperService } from './service/oidc-helper.service'; +import { AuthCodeFlowConfig, AuthModuleConfig } from '../config.oidc'; +import { SubjectExtensions } from './extensions/general.extensions'; +import { ExternalLoginConfig } from '../config.external-login'; +import { AccountCategoryService } from '../services/account.category.service'; +import { AccountService } from '../services/account.service'; + +export function storageFactory(): OAuthStorage { + return localStorage; +} + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + SocialLoginModule, + OAuthModule.forRoot(), + AuthModule.forRoot(ExternalLoginConfig.getAuth0Config()), + ], + providers: [ + { provide: APP_INITIALIZER, useFactory: authAppInitializerFactory, deps: [AuthService], multi: true }, + { provide: AuthConfig, useValue: AuthCodeFlowConfig }, + { provide: OAuthModuleConfig, useValue: AuthModuleConfig }, + { provide: OAuthStorage, useFactory: storageFactory }, + { provide: 'SocialAuthServiceConfig', useValue: ExternalLoginConfig.getSocialConfig() }, + RightSidebarService, + AuthGuard, + AuthService, + DirectionService, + OidcHelperService, + SubjectExtensions, + AccountCategoryService, + AccountService, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] +}) +export class CoreModule { + constructor(@Optional() @SkipSelf() parentModule: CoreModule) { + throwIfAlreadyLoaded(parentModule, 'CoreModule'); + } +} diff --git a/MyOffice.SPA/src/app/core/extensions/general.extensions.ts b/MyOffice.SPA/src/app/core/extensions/general.extensions.ts new file mode 100644 index 0000000..515c2db --- /dev/null +++ b/MyOffice.SPA/src/app/core/extensions/general.extensions.ts @@ -0,0 +1,42 @@ +// angular +import { ActivatedRoute } from '@angular/router'; + +// libs +import { BehaviorSubject, Observable, Subject, of, throwError, from, combineLatest } from 'rxjs'; + +import { getSafeRedirectUrl } from '../utils/safe-redirect'; + +export { } + +declare global { + interface RouterExtensions { + addDays(days: number): Date; + } + } + +export class ExActivatedRoute extends ActivatedRoute { + + getCurrentRoute(): string { + const raw = this.snapshot.queryParams['r'] as string | undefined; + return getSafeRedirectUrl(raw); + } +} + +interface Action { + (item: T): void; +} + +interface Func { + (item: T): TResult; +} + +export class SubjectExtensions { + + static start(start: Action>): Subject { + var result = new Subject(); + + start(result); + + return result; + } +} diff --git a/MyOffice.SPA/src/app/core/extensions/router.extensions.ts b/MyOffice.SPA/src/app/core/extensions/router.extensions.ts new file mode 100644 index 0000000..ed347f0 --- /dev/null +++ b/MyOffice.SPA/src/app/core/extensions/router.extensions.ts @@ -0,0 +1,18 @@ +import { ActivatedRoute } from '@angular/router'; +import { getSafeRedirectUrl } from '../utils/safe-redirect'; + +export { } + +declare global { + interface RouterExtensions { + addDays(days: number): Date; + } + } + +export class ExActivatedRoute extends ActivatedRoute { + + getCurrentRoute(): string { + const raw = this.snapshot.queryParams['r'] as string | undefined; + return getSafeRedirectUrl(raw); + } +} diff --git a/MyOffice.SPA/src/app/core/guard/auth.guard.ts b/MyOffice.SPA/src/app/core/guard/auth.guard.ts new file mode 100644 index 0000000..ce462cb --- /dev/null +++ b/MyOffice.SPA/src/app/core/guard/auth.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; + +import { AuthService } from '../service/auth.service'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard { + constructor( + private authService: AuthService, + private router: Router + ) { + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + if (this.authService.isAuthenticated) { + return true; + } + this.router.navigate(['/authentication/signin'], { queryParams: { r: encodeURIComponent(state.url) } }); + return false; + } +} diff --git a/MyOffice.SPA/src/app/core/guard/module-import.guard.ts b/MyOffice.SPA/src/app/core/guard/module-import.guard.ts new file mode 100644 index 0000000..3526a01 --- /dev/null +++ b/MyOffice.SPA/src/app/core/guard/module-import.guard.ts @@ -0,0 +1,12 @@ +import { CoreModule } from '../core.module'; + +export function throwIfAlreadyLoaded( + parentModule: CoreModule, + moduleName: string +) { + if (parentModule) { + throw new Error( + `${moduleName} has already been loaded. Import ${moduleName} modules in the AppModule only.` + ); + } +} diff --git a/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts b/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts new file mode 100644 index 0000000..33c54b4 --- /dev/null +++ b/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts @@ -0,0 +1,40 @@ +import { AuthService } from '../service/auth.service'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + constructor( + private authenticationService: AuthService, + private router: Router + ) {} + + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + return next.handle(request).pipe( + catchError((err) => { + if (err.status === 401) { + const returnUrl = this.router.url; + this.authenticationService.logout(); + void this.router.navigate(['/authentication/signin'], { + queryParams: returnUrl && returnUrl !== '/' + ? { r: returnUrl } + : undefined, + }); + } + + let error = err.error || err.message || err.statusText; + + if (!error) { + console.log('not parsed error', err); + } + return throwError(() => error); + }) + ); + } +} diff --git a/MyOffice.SPA/src/app/core/interceptor/fake-backend.ts b/MyOffice.SPA/src/app/core/interceptor/fake-backend.ts new file mode 100644 index 0000000..9b64489 --- /dev/null +++ b/MyOffice.SPA/src/app/core/interceptor/fake-backend.ts @@ -0,0 +1,101 @@ +/*import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpResponse, + HttpHandler, + HttpEvent, + HttpInterceptor, + HTTP_INTERCEPTORS, +} from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { UserModel } from '../models/user.model'; + +const users: UserModel[] = [ + { + id: 'fake-user', + img: 'assets/images/user/admin.jpg', + username: 'admin@software.com', + //password: 'admin@123', + firstName: 'Sarah', + lastName: 'Smith', + token: 'admin-token', + }, +]; + +@Injectable() +export class FakeBackendInterceptor implements HttpInterceptor { + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + const { url, method, headers, body } = request; + // wrap in delayed observable to simulate server api call + return of(null).pipe(mergeMap(handleRoute)); + + function handleRoute() { + switch (true) { + case url.endsWith('/authenticate') && method === 'POST': + return authenticate(); + default: + // pass through any requests not handled above + return next.handle(request); + } + } + + // route functions + + function authenticate() { + const { username, password } = body; + const user = users.find( + (x) => x.username === username && x.password === password + ); + if (!user) { + return error('Username or password is incorrect'); + } + return ok({ + id: user.id, + username: user.username, + img: user.img, + firstName: user.firstName, + lastName: user.lastName, + token: user.token, + }); + } + + // helper functions + + function ok(body?: { + id: string; + username: string; + img?: string; + firstName?: string; + lastName?: string; + token?: string; + }) { + return of(new HttpResponse({ status: 200, body })); + } + + function error(message: string) { + return throwError({ error: { message } }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function unauthorized() { + return throwError({ status: 401, error: { message: 'Unauthorised' } }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function isLoggedIn() { + return headers.get('Authorization') === 'Bearer fake-jwt-token'; + } + } +} + +export const fakeBackendProvider = { + // use fake backend in place of Http service for backend-less development + provide: HTTP_INTERCEPTORS, + useClass: FakeBackendInterceptor, + multi: true, +}; +*/ diff --git a/MyOffice.SPA/src/app/core/interceptor/update.date.http.interceptor .ts b/MyOffice.SPA/src/app/core/interceptor/update.date.http.interceptor .ts new file mode 100644 index 0000000..594334e --- /dev/null +++ b/MyOffice.SPA/src/app/core/interceptor/update.date.http.interceptor .ts @@ -0,0 +1,42 @@ +// angular +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpInterceptor } from '@angular/common/http'; +import { HttpRequest } from '@angular/common/http'; +import { HttpHandler } from '@angular/common/http'; +import { HttpEvent } from '@angular/common/http'; + +// libs +import moment from 'moment'; + + +@Injectable() +export class UpdateDateHttpInterceptor implements HttpInterceptor { + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + if (request.method === 'POST' || request.method === 'PUT') { + this.shiftDates(request.body); + } + + return next.handle(request); + } + + shiftDates(body: any) { + if (body === null || body === undefined) { + return body; + } + + if (typeof body !== 'object') { + return body; + } + + for (const key of Object.keys(body)) { + const value = body[key]; + if (value instanceof Date) { + body[key] = moment(value).utcOffset(0, true).format(); + } else if (typeof value === 'object') { + this.shiftDates(value); + } + } + } +} diff --git a/MyOffice.SPA/src/app/core/models/config.interface.ts b/MyOffice.SPA/src/app/core/models/config.interface.ts new file mode 100644 index 0000000..fea99e3 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/config.interface.ts @@ -0,0 +1,12 @@ +export interface InConfiguration { + layout: { + rtl: boolean; + variant: string; + theme_color: string; + logo_bg_color: string; + sidebar: { + collapsed: boolean; + backgroundColor: string; + }; + }; +} diff --git a/MyOffice.SPA/src/app/core/models/general-error.model.ts b/MyOffice.SPA/src/app/core/models/general-error.model.ts new file mode 100644 index 0000000..2cbcb47 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/general-error.model.ts @@ -0,0 +1,6 @@ +export interface GeneralErrorModel { + //succeeded?: boolean; + code?: string; + description?: string; + errors?: GeneralErrorModel[]; +} diff --git a/MyOffice.SPA/src/app/core/models/general-result.model.ts b/MyOffice.SPA/src/app/core/models/general-result.model.ts new file mode 100644 index 0000000..5f3950d --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/general-result.model.ts @@ -0,0 +1,7 @@ +import { GeneralErrorModel } from './general-error.model'; + +export interface GeneralResultModel { + success: boolean; + error?: GeneralErrorModel; + data?: any; +} diff --git a/MyOffice.SPA/src/app/core/models/login.model.ts b/MyOffice.SPA/src/app/core/models/login.model.ts new file mode 100644 index 0000000..2de9c3c --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/login.model.ts @@ -0,0 +1,4 @@ +export interface LoginModel { + username: string; + password: string; +} diff --git a/MyOffice.SPA/src/app/core/models/register.model.ts b/MyOffice.SPA/src/app/core/models/register.model.ts new file mode 100644 index 0000000..ab84657 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/register.model.ts @@ -0,0 +1,5 @@ +export interface RegisterModel { + username: string; + password: string; + confirmPassword: string; +} diff --git a/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts b/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts new file mode 100644 index 0000000..1c4ee7b --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts @@ -0,0 +1,7 @@ +import { SelectableModel } from './selectable.model'; + +describe('SelectableModel', () => { + it('should create an instance', () => { + expect(new SelectableModel()).toBeTruthy(); + }); +}); diff --git a/MyOffice.SPA/src/app/core/models/selectable.model.ts b/MyOffice.SPA/src/app/core/models/selectable.model.ts new file mode 100644 index 0000000..bba11c1 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/selectable.model.ts @@ -0,0 +1,4 @@ +export interface ISelectableModel { + model: T; + selected: boolean; +} diff --git a/MyOffice.SPA/src/app/core/models/user-profile.model.ts b/MyOffice.SPA/src/app/core/models/user-profile.model.ts new file mode 100644 index 0000000..ce03098 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/user-profile.model.ts @@ -0,0 +1,9 @@ +export interface UserProfileModel { + id: string; + email?: string; + phone?: string; + firstName?: string; + lastName?: string; + fullName?: string; + currency?: string; +} diff --git a/MyOffice.SPA/src/app/core/models/user.model.ts b/MyOffice.SPA/src/app/core/models/user.model.ts new file mode 100644 index 0000000..7a9298e --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/user.model.ts @@ -0,0 +1,9 @@ +export interface UserModel { + id: string; + userName: string; + img?: string; + firstName?: string; + lastName?: string; + email?: string; + //token?: string; +} diff --git a/MyOffice.SPA/src/app/core/service/auth.service.spec.ts b/MyOffice.SPA/src/app/core/service/auth.service.spec.ts new file mode 100644 index 0000000..9b6666e --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', + () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', + () => { + expect(service).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/core/service/auth.service.ts b/MyOffice.SPA/src/app/core/service/auth.service.ts new file mode 100644 index 0000000..2e19184 --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/auth.service.ts @@ -0,0 +1,266 @@ +// angular +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; + +// libs +import { BehaviorSubject } from 'rxjs'; +import { Observable } from 'rxjs'; +import { Subject } from 'rxjs'; +import { of } from 'rxjs'; +import { throwError } from 'rxjs'; +import { from } from 'rxjs'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { catchError } from 'rxjs/operators'; +import { AuthService as Auth0Service } from '@auth0/auth0-angular'; + +import { SocialAuthService } from '@abacritt/angularx-social-login'; +import { GoogleLoginProvider } from '@abacritt/angularx-social-login'; + +// app +import { OidcHelperService } from './oidc-helper.service'; +import { UserModel } from '../models/user.model'; +import { ApiRoutes } from '../../api-routes'; +import { RegisterModel } from '../models/register.model'; +import { LoginModel } from '../models/login.model'; +import { UserProfileModel } from '../models/user-profile.model'; +import { GeneralResultModel } from '../models/general-result.model'; +import { getSafeRedirectUrl } from '../utils/safe-redirect'; +import { SubjectExtensions } from '../extensions/general.extensions'; + +export function authAppInitializerFactory(authService: AuthService): () => Promise { + if (window.location.href.indexOf('/silent-refresh.html') !== -1) { + return () => Promise.resolve(); + } + return () => authService.runInitialLoginSequence(); +} + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private currentUserSubject: BehaviorSubject; + currentUser$: Observable; + + private isAuthenticatedSubject$ = new BehaviorSubject(false); + isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); + + constructor( + private http: HttpClient, + private router: Router, + private route: ActivatedRoute, + private oidcHelperService: OidcHelperService, + private auth0Service: Auth0Service, + private externalAuthService: SocialAuthService, + ) { + this.currentUserSubject = new BehaviorSubject({} as UserModel); + this.currentUser$ = this.currentUserSubject.asObservable(); + + // on external social login + this.externalAuthService.authState.subscribe((user) => { + if (user?.idToken) { + oidcHelperService.loginByExternalLogin('google', user.idToken); + } + }); + + this.oidcHelperService.isDoneLoading$ + .pipe( + filter(isDone => isDone), + switchMap(() => this.oidcHelperService.isAuthenticated$) + ) + .subscribe(authState => { + let redirectUrl = this.getRedirect(); + if (authState.isAuthenticated) { + this.oidcHelperService.loadUserProfile().then(e => { + let resp = (e as any).info; + let userProfile = { + id: resp.id, + email: resp.email, + phone: resp.phone, + firstName: resp.firstName, + lastName: resp.lastName, + fullName: resp.fullName, + }; + + this.setCurrentUserValue(userProfile.id, userProfile); + + if (authState.action === AuthenticatedActionEnum.loggedIn) { + this.router.navigate([redirectUrl || '/dashboard/rests']); + } + }); + } + }); + } + + setCurrentUserValue(id: string, profile?: UserProfileModel) { + this.currentUserSubject.next({ + id: id, + img: 'assets/images/user/admin.jpg', + userName: profile?.email || '', + firstName: profile?.firstName || '', + lastName: profile?.lastName || '', + }); + } + + get currentUserValue(): UserModel { + return this.currentUserSubject.value; + } + + login(loginModel: LoginModel): Subject { + return SubjectExtensions.start(subject => { + this.oidcHelperService + .login(loginModel.username, loginModel.password) + .subscribe(resp => { + if (resp.access_token) { + this.isAuthenticatedSubject$.next(true); + } + subject.next({ success: !!(resp.access_token) }); + }, + err => { + subject.next({ success: false, error: err.error }); + }); + }); + } + + logout() { + this.currentUserSubject.next({} as UserModel); + this.isAuthenticatedSubject$.next(false); + this.oidcHelperService.logout(); + + return of({ success: false }); + } + + register(registerModel: RegisterModel): Observable { + return this.http + .post(ApiRoutes.UserRegister, registerModel) + .pipe( + map(resp => resp), + catchError(error => { + return throwError(error); + }) + ); + } + + runInitialLoginSequence() { + return this.oidcHelperService.runInitialLoginSequence(); + } + + get isAuthenticated(): boolean { + return this.oidcHelperService.hasValidAccessToken(); + } + + getAccessToken() { + return this.oidcHelperService.getAccessToken(); + } + + loginGoogle(redirectUrl?: string): Observable { + return from(this.externalAuthService.signIn(GoogleLoginProvider.PROVIDER_ID)); + } + + attachGoogle(): Observable { + var result = new Subject(); + + /*this.externalAuthService.signIn(GoogleLoginProvider.PROVIDER_ID).then(function (resp) { + //console.log('attachGoogle', resp); + //return result.next(resp); + //this.attach(idToken.__raw, result); + });*/ + + return result; + } + + loginAuth0(): Observable { + return SubjectExtensions.start((subject) => { + var subs = this.auth0Service.idTokenClaims$.subscribe(token => { + if (token && token?.__raw) { + subs.unsubscribe(); + + this.oidcHelperService.loginByExternalLogin('auth0', token.__raw).subscribe(login => { + if (login.success) { + this.isAuthenticatedSubject$.next(true); + } + subject.next({ success: login.success, error: login.error }); + }); + } else { + //subject.next({ success: false, error: { description: 'Login failed' } }); + } + }); + + this.auth0Service.getAccessTokenWithPopup().subscribe(popupToken => { + if (!popupToken) { + this.auth0Service.getAccessTokenSilently().subscribe(silently => { + }); + } + }); + }); + } + + attachAuth0(): Observable { + return SubjectExtensions.start((subject) => { + var subs = this.auth0Service.idTokenClaims$.subscribe(token => { + if (token && token?.__raw) { + subs.unsubscribe(); + + this.attach(token?.__raw).subscribe( + (resp) => subject.next({ success: true, data: token?.__raw }), + (err) => subject.next({ success: false, data: err }) + ); + } else { + subject.next({ success: false, data: 'Failed' }); + } + }); + + this.auth0Service.getAccessTokenWithPopup().subscribe(popupToken => { + if (!popupToken) { + this.auth0Service.getAccessTokenSilently().subscribe(silently => { + }); + } + }); + }); + } + + deattach(provider: string): Observable { + return this.http + .post(ApiRoutes.UserDeattach, { provider: provider }) + .pipe( + catchError(error => throwError(() => error)) + ); + } + + private attach(token: string): Observable { + return this.http.post(ApiRoutes.UserAttach, { + provider: 'auth0', + token: token, + }); + } + + private getRedirect(): string | undefined { + const raw = this.route.snapshot.queryParams['r'] as string | undefined; + if (!raw) { + return undefined; + } + const safe = getSafeRedirectUrl(raw, ''); + return safe || undefined; + } +} + +export enum AuthenticatedActionEnum { + init, + update, + loggedIn, + loggedOff, +} + +export interface IAuthenticatedState { + isAuthenticated: boolean, + action: AuthenticatedActionEnum, + redirectUrl?: string, +} + +export enum StateEnum { + undefined, + inited, + completed, + failed, +} diff --git a/MyOffice.SPA/src/app/core/service/direction.service.ts b/MyOffice.SPA/src/app/core/service/direction.service.ts new file mode 100644 index 0000000..9acd559 --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/direction.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable() +export class DirectionService { + private data = new BehaviorSubject(''); + currentData = this.data.asObservable(); + + constructor() { + //constructor + } + + updateDirection(item: string) { + this.data.next(item); + } +} diff --git a/MyOffice.SPA/src/app/core/service/language.service.ts b/MyOffice.SPA/src/app/core/service/language.service.ts new file mode 100644 index 0000000..27a17cc --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/language.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root', +}) +export class LanguageService { + languages: string[] = ['en', 'es', 'de', 'ua']; + + constructor(public translate: TranslateService) { + let browserLang: string; + translate.addLangs(this.languages); + + if (localStorage.getItem('lang')) { + browserLang = localStorage.getItem('lang') as string; + } else { + browserLang = translate.getBrowserLang() as string; + } + translate.use(browserLang.match(/en|es|de|ua/) ? browserLang : 'en'); + } + + setLanguage(lang: string) { + this.translate.use(lang); + localStorage.setItem('lang', lang); + } +} diff --git a/MyOffice.SPA/src/app/core/service/oidc-helper.service.ts b/MyOffice.SPA/src/app/core/service/oidc-helper.service.ts new file mode 100644 index 0000000..0626ca4 --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/oidc-helper.service.ts @@ -0,0 +1,252 @@ +// angular +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { OAuthErrorEvent } from 'angular-oauth2-oidc'; + +// libs +import { filter, map } from 'rxjs/operators'; +import { Subject, Observable, BehaviorSubject, from } from 'rxjs'; + +// app +import { GeneralResultModel } from '../models/general-result.model'; + +@Injectable() +export class OidcHelperService { + private isAuthenticatedSubject$ = new BehaviorSubject({ + isAuthenticated: false, + action: AuthenticatedActionEnum.init + }); + isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); + + private isDoneLoadingFailed = false; + private isDoneLoadingSubject$ = new BehaviorSubject(false); + isDoneLoading$ = this.isDoneLoadingSubject$.asObservable(); + + constructor( + private oauthService: OAuthService, + private router: Router, + ) { + this.oauthService.oidc = false; + + // all events handler + this.oauthService.events.subscribe(event => { + if (event instanceof OAuthErrorEvent) { + console.error('OAuthErrorEvent Object:', event); + if (event.type === 'discovery_document_validation_error') { + this.isDoneLoadingFailed = true; + this.setIsAuthenticatedSubject$(false, AuthenticatedActionEnum.update); + } + } else { + var hasValidAccessToken = this.hasValidAccessToken(); + var isAuthenticated = this.isAuthenticatedSubject$.getValue().isAuthenticated; + if (isAuthenticated !== hasValidAccessToken) { + this.setIsAuthenticatedSubject$(hasValidAccessToken, AuthenticatedActionEnum.update); + } + } + }); + + // This is tricky, as it might cause race conditions (where access_token is set in another + // tab before everything is said and done there. + // TODO: Improve this setup. See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2 + window.addEventListener('storage', + (event) => { + // The `key` is `null` if the event was caused by `.clear()` + if (event.key !== 'access_token' && event.key !== null) { + return; + } + + console.warn( + 'Noticed changes to access_token (most likely from another tab), updating isAuthenticated'); + this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.update); + + if (!this.hasValidAccessToken()) { + console.log('storage access_token updated !this.hasValidAccessToken()'); + this.logout(); + this.navigateToLoginPage(); + } + }); + + // init login stata + this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.update); + + // TODO: some time receive 'message' with data 'error' == 'session_error'' + /*window.addEventListener('message', e => { + console.log('message', e);; + if (e.origin === 'https://dev-w08xm0pi.us.auth0.com') { + console.log('message stopPropagation');; + e.stopPropagation(); + } + }); + this.oauthService.events.subscribe(x => { + console.log('events', x); + });*/ + this.oauthService.events + .pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type))) + .subscribe(e => { + console.log('events session_terminated', e, this.hasValidAccessToken()); + this.navigateToLoginPage(); + }); + + this.oauthService.setupAutomaticSilentRefresh(); + } + + private setIsAuthenticatedSubject$( + isAuthenticated: boolean, + action: AuthenticatedActionEnum + ) { + var state = this.isAuthenticatedSubject$.getValue(); + if (state.isAuthenticated !== isAuthenticated || state.action !== action + ) { + this.isAuthenticatedSubject$.next({ isAuthenticated, action }); + } + } + + private navigateToLoginPage() { + this.router.navigateByUrl('/authentication/signin'); + } + + logout() { + this.oauthService.logOut(true); + } + + refresh() { + const refreshToken = this.oauthService.getRefreshToken(); + if (refreshToken) { + this.oauthService.refreshToken(); + } + } + + hasValidAccessToken() { + return this.oauthService.hasValidAccessToken(); + } + + getAccessToken() { + return this.oauthService.getAccessToken(); + } + + login(username: string, password: string) { + //return this.oauthService.fetchTokenUsingPasswordFlow(username, password); + return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)); + } + + loginByExternalLogin( + provider: string, + token: string, + redirectUrl?: string + ): Observable { + var result = new Subject(); + + let params = { + token: token, + provider: provider, + }; + this.oauthService + .fetchTokenUsingGrant('external', params) + .then(x => { + this.setIsAuthenticatedSubject$(this.hasValidAccessToken(), AuthenticatedActionEnum.loggedIn); + result.next({ success: true }); + }) + .catch(err => { + result.next({ success: false, error: { code: err, description: err } }); + }); + + return result; + } + + loadUserProfile(): Promise { + return this.oauthService.loadUserProfile(); + } + + runInitialLoginSequence(): Promise { + if (location.hash) { + //console.log('Encountered hash fragment, plotting as table...'); + //console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('='))); + } + if (this.isDoneLoadingFailed) { + return new Promise(resolve => setTimeout(() => resolve(), 1500)); + } + // 0. LOAD CONFIG: + // First we have to check to see how the IdServer is + // currently configured: + return this.oauthService.loadDiscoveryDocument() + + // For demo purposes, we pretend the previous call was very slow + .then(() => { + new Promise(resolve => setTimeout(() => resolve(), 1500)); + }) + + // 1. HASH LOGIN: + // Try to log in via hash fragment after redirect back + // from IdServer from initImplicitFlow: + .then(() => { + this.oauthService.tryLogin(); + }) + .then(() => { + if (this.hasValidAccessToken()) { + return Promise.resolve(); + } + + // Refresh via refresh_token (no /connect/authorize cookie session). + const refreshToken = this.oauthService.getRefreshToken(); + if (!refreshToken) { + return Promise.resolve(); + } + + return this.oauthService.refreshToken() + .then(() => Promise.resolve()) + .catch(result => { + const error = + result?.reason?.error ?? + result?.params?.error ?? + result?.error; + const softErrors = [ + 'interaction_required', + 'login_required', + 'account_selection_required', + 'consent_required', + 'access_denied', + 'invalid_grant', + ]; + + if (error && softErrors.indexOf(error) >= 0) { + console.warn('Token refresh needs user login.', error); + return Promise.resolve(); + } + + return Promise.reject(result); + }); + }) + .then(() => { + this.isDoneLoadingSubject$.next(true); + + // Check for the strings 'undefined' and 'null' just to be sure. Our current + // login(...) should never have this, but in case someone ever calls + // initImplicitFlow(undefined | null) this could happen. + if (this.oauthService.state && + this.oauthService.state !== 'undefined' && + this.oauthService.state !== 'null') { + let stateUrl = this.oauthService.state; + if (stateUrl.startsWith('/') === false) { + stateUrl = decodeURIComponent(stateUrl); + } + console.log(`There was state of ${this.oauthService.state}, so we are sending you to: ${stateUrl}`); + this.router.navigateByUrl(stateUrl); + } + }) + .catch(() => this.isDoneLoadingSubject$.next(true)); + } +} + +export enum AuthenticatedActionEnum { + init, + update, + loggedIn, + loggedOff, +} + +export interface IAuthenticatedState { + isAuthenticated: boolean, + action: AuthenticatedActionEnum, + redirectUrl?: string, +} diff --git a/MyOffice.SPA/src/app/core/service/rightsidebar.service.ts b/MyOffice.SPA/src/app/core/service/rightsidebar.service.ts new file mode 100644 index 0000000..7b5d47b --- /dev/null +++ b/MyOffice.SPA/src/app/core/service/rightsidebar.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable() +export class RightSidebarService { + private sidebarSubject: BehaviorSubject = new BehaviorSubject(false); + sidebarState = this.sidebarSubject.asObservable(); + + setRightSidebar = (value: boolean) => { + this.sidebarSubject.next(value); + }; + + constructor() { + //constructor + } +} diff --git a/MyOffice.SPA/src/app/core/utils/safe-redirect.spec.ts b/MyOffice.SPA/src/app/core/utils/safe-redirect.spec.ts new file mode 100644 index 0000000..4e295e0 --- /dev/null +++ b/MyOffice.SPA/src/app/core/utils/safe-redirect.spec.ts @@ -0,0 +1,22 @@ +import { getSafeRedirectUrl } from './safe-redirect'; + +describe('getSafeRedirectUrl', () => { + it('returns fallback when raw is empty', () => { + expect(getSafeRedirectUrl(null)).toBe('/dashboard/rests'); + expect(getSafeRedirectUrl(undefined)).toBe('/dashboard/rests'); + expect(getSafeRedirectUrl('')).toBe('/dashboard/rests'); + }); + + it('allows relative in-app paths', () => { + expect(getSafeRedirectUrl('/accounts')).toBe('/accounts'); + expect(getSafeRedirectUrl('%2Fdashboard%2Frests')).toBe('/dashboard/rests'); + }); + + it('blocks open redirects', () => { + expect(getSafeRedirectUrl('//evil.com')).toBe('/dashboard/rests'); + expect(getSafeRedirectUrl('https://evil.com')).toBe('/dashboard/rests'); + expect(getSafeRedirectUrl('http://evil.com')).toBe('/dashboard/rests'); + expect(getSafeRedirectUrl('/path?next=https://evil.com')).toBe('/dashboard/rests'); + }); +}); + diff --git a/MyOffice.SPA/src/app/core/utils/safe-redirect.ts b/MyOffice.SPA/src/app/core/utils/safe-redirect.ts new file mode 100644 index 0000000..90bb5ac --- /dev/null +++ b/MyOffice.SPA/src/app/core/utils/safe-redirect.ts @@ -0,0 +1,24 @@ +/** + * Only allow in-app relative paths (blocks open redirects via ?r=). + */ +export function getSafeRedirectUrl( + raw: string | null | undefined, + fallback = '/dashboard/rests' +): string { + if (!raw) { + return fallback; + } + + let url: string; + try { + url = decodeURIComponent(raw).trim(); + } catch { + return fallback; + } + + if (!url.startsWith('/') || url.startsWith('//') || url.includes('://')) { + return fallback; + } + + return url; +} diff --git a/MyOffice.SPA/src/app/core/validators/atleastone.validator.ts b/MyOffice.SPA/src/app/core/validators/atleastone.validator.ts new file mode 100644 index 0000000..8968db7 --- /dev/null +++ b/MyOffice.SPA/src/app/core/validators/atleastone.validator.ts @@ -0,0 +1,21 @@ +import { + FormGroup, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; + +export const atLeastOne = (validator: ValidatorFn, controls: string[] = []) => ( + group: FormGroup, +): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + + const hasAtLeastOne = group && group.controls && controls + .some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : { + atLeastOne: true, + }; +}; diff --git a/MyOffice.SPA/src/app/core/validators/atleastonenumber.validator.ts b/MyOffice.SPA/src/app/core/validators/atleastonenumber.validator.ts new file mode 100644 index 0000000..49cdcb2 --- /dev/null +++ b/MyOffice.SPA/src/app/core/validators/atleastonenumber.validator.ts @@ -0,0 +1,24 @@ +import { + FormGroup, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; + +export const atLeastOneNumber = (validator: ValidatorFn, controls: string[] = []) => + (group: FormGroup,): ValidationErrors | null => { + + if (!controls) { + controls = Object.keys(group.controls); + } + + const hasAtLeastOne = group && group.controls && controls + .some(k => { + var v = group.controls[k].value; + return !!(v && !isNaN(v) && parseFloat(v) !== 0 && !validator(group.controls[k])); + }); + + return hasAtLeastOne ? null : { + atLeastOne: true, + }; + }; diff --git a/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts b/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts new file mode 100644 index 0000000..d53cfa3 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts @@ -0,0 +1,37 @@ +// angular +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +// app +import { Page404Component } from '../authentication/page404/page404.component'; +import { Dashboard2Component } from './dashboard2/dashboard2.component'; +import { DashboardIncomeComponent } from './income/dashboard.component'; +import { DashboardOutcomeComponent } from './outcome/dashboard.component'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'rests', + pathMatch: 'full', + }, + { + path: 'rests', + component: Dashboard2Component, + }, + { + path: 'income', + component: DashboardIncomeComponent, + }, + { + path: 'outcome', + component: DashboardOutcomeComponent, + }, + { path: '**', component: Page404Component }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class DashboardRoutingModule { +} diff --git a/MyOffice.SPA/src/app/dashboard/dashboard.module.ts b/MyOffice.SPA/src/app/dashboard/dashboard.module.ts new file mode 100644 index 0000000..6c769a5 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/dashboard.module.ts @@ -0,0 +1,55 @@ +// angular +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DecimalPipe } from '@angular/common'; + +// libs +import { NgScrollbarModule } from 'ngx-scrollbar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { NgApexchartsModule } from 'ng-apexcharts'; +import { TranslateModule } from '@ngx-translate/core'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; + +// app +import { DashboardRoutingModule } from './dashboard-routing.module'; +import { Dashboard2Component } from './dashboard2/dashboard2.component'; +import { DashboardIncomeComponent } from './income/dashboard.component'; +import { DashboardOutcomeComponent } from './outcome/dashboard.component'; +import { ComponentsModule } from 'src/app/shared/components/components.module'; +import { SharedModule } from '../shared/shared.module'; + +@NgModule({ + declarations: [ + Dashboard2Component, + DashboardIncomeComponent, + DashboardOutcomeComponent, + ], + imports: [ + CommonModule, + DashboardRoutingModule, + NgApexchartsModule, + NgScrollbarModule, + MatIconModule, + MatButtonModule, + MatMenuModule, + MatTooltipModule, + MatCheckboxModule, + DragDropModule, + MatProgressBarModule, + ComponentsModule, + SharedModule, + TranslateModule, + ], + providers: [ + { provide: MAT_DATE_LOCALE, useValue: 'en-GB' }, + DecimalPipe, + ] +}) +export class DashboardModule { +} diff --git a/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.html b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.html new file mode 100644 index 0000000..919b567 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.html @@ -0,0 +1,103 @@ +
+
+
+ + +
+ +
+ +
+
+
+
+
+
{{'BALANCE' | translate}}
+
+

{{dashboardModel?.balance | number: '0.2-2'}}

+
+
+
+
+ +
+
+
+
+
+
{{'DEBIT.BALANCE' | translate}}
+

+
+

{{dashboardModel?.balanceDebit | number: '0.2-2'}}

+
+
+
+
+ +
+
+
+
+
+
{{'CREDIT.BALANCE' | translate}}
+
+

{{dashboardModel?.balanceCredit | number: '0.2-2'}}

+
+
+
+
+
+ +
+ +
+
+
+

{{'CURRENT.BALANCE' | translate}}

+ + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
{{item.name}}{{item.balance | number: '1.2'}} ({{item.currencyShortName}}){{item.balanceAtRate | number: '1.2'}}
{{'OTHER' | translate}}{{top10BalanceOther | number: '1.2'}}
{{'TOTAL' | translate}}{{top10BalanceTotal | number: '1.2'}}
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.scss b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.spec.ts b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.spec.ts new file mode 100644 index 0000000..00dd7f9 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Dashboard2Component } from './dashboard2.component'; + +describe('Dashboard2Component', + () => { + let component: Dashboard2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [Dashboard2Component] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(Dashboard2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.ts b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.ts new file mode 100644 index 0000000..1f8dc52 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/dashboard2/dashboard2.component.ts @@ -0,0 +1,155 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { OnInit } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { DecimalPipe } from '@angular/common'; + +// libs +import { + ApexAxisChartSeries, + ApexChart, + ApexXAxis, + ApexDataLabels, + ApexStroke, + ApexMarkers, + ApexYAxis, + ApexGrid, + ApexTitleSubtitle, + ApexTooltip, + ApexLegend, + ApexFill, + ApexResponsive, + ApexNonAxisChartSeries, +} from 'ng-apexcharts'; +import { ChartComponent } from "ng-apexcharts"; + +// app +import { ApiRoutes } from '../../api-routes'; +import { DashboardModel } from '../../model/dashboard.model'; +import { DashboardRestModel } from '../../model/dashboard.model'; + +export type ChartOptions = { + series: ApexAxisChartSeries; + series2: ApexNonAxisChartSeries; + chart: ApexChart; + xaxis: ApexXAxis; + stroke: ApexStroke; + dataLabels: ApexDataLabels; + markers: ApexMarkers; + colors: string[]; + yaxis: ApexYAxis; + grid: ApexGrid; + legend: ApexLegend; + tooltip: ApexTooltip; + fill: ApexFill; + title: ApexTitleSubtitle; + responsive: ApexResponsive[]; + labels: string[]; +}; + +@Component({ + selector: 'app-dashboard2', + templateUrl: './dashboard2.component.html', + styleUrls: ['./dashboard2.component.scss'], +}) +export class Dashboard2Component implements OnInit { + + @ViewChild("chart") chart!: ChartComponent; + public pieChartOptions!: Partial; + top10Balance?: DashboardRestModel[]; + top10BalanceOther?: number; + top10BalanceTotal?: number; + dashboardModel?: DashboardModel; + + private readonly destroyRef = inject(DestroyRef); + + // color: ["#3FA7DC", "#F6A025", "#9BC311"], + constructor( + private httpClient: HttpClient, + private _decimalPipe: DecimalPipe + ) { + + } + + ngOnInit() { + this.httpClient + .get(ApiRoutes.Dashboard) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + data.incomeChange = this.calcChanges(data.incomeLast, data.incomePrevious); + data.outcomeChange = this.calcChanges(data.outcomeLast, data.outcomePrevious); + + var labels = []; + var series2: number[] = []; + for (var i = 0; i < data.balanceRests.length; i++) { + let item = data.balanceRests[i]; + labels.push(item.name); + series2.push(item.balanceAtRate); + } + + this.balanceChart(labels, series2); + + this.top10Balance = data.balanceRests + .filter(x => x.balanceAtRate > 0) + .sort(x => x.balanceAtRate) + .slice(0, 10); + + this.top10BalanceTotal = data.balanceRests.reduce((sum, current) => sum + current.balanceAtRate, 0); + this.top10BalanceOther = this.top10Balance.reduce((sum, current) => sum + current.balanceAtRate, 0); + this.top10BalanceOther = this.top10BalanceTotal - this.top10BalanceOther; + + this.dashboardModel = data; + }); + } + + private calcChanges(newValue: number, oldValue: number): number { + if (newValue > oldValue) { + return (newValue - oldValue) / oldValue * 100; + } + else if (newValue < oldValue) { + return (oldValue - newValue) / oldValue * 100; + } + + return 0; + } + + private balanceChart(labels: string[], series2: number[]) { + var self = this; + + this.pieChartOptions = { + labels: labels, + series2: series2, + chart: { + type: 'donut', + width: 600, + height: 600, + }, + legend: { + show: true, + }, + dataLabels: { + enabled: false, + }, + responsive: [{ + breakpoint: 480, + options: { + legend: { + position: 'bottom' + } + }, + }], + tooltip: { + x: { + show: false, + }, + y: { + formatter: function (value, series) { + return self._decimalPipe.transform(value, '1.2-2') || ""; + } + } + } + }; + } +} diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html new file mode 100644 index 0000000..0a55e51 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html @@ -0,0 +1,70 @@ +
+
+
+ + +
+ +
+
+
+
+

{{'PERIOD' | translate}}

+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+

{{'INCOME' | translate}}

+ + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + + + +
{{item.name}}{{item.valueRaw | number: '1.2'}}{{item.value | number: '1.2'}}
{{'TOTAL' | translate}}{{total | number: '1.2'}}
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.scss b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts new file mode 100644 index 0000000..00dd7f9 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Dashboard2Component } from './dashboard2.component'; + +describe('Dashboard2Component', + () => { + let component: Dashboard2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [Dashboard2Component] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(Dashboard2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts new file mode 100644 index 0000000..26848e5 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts @@ -0,0 +1,146 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { OnInit } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { HttpParams } from '@angular/common/http'; +import { DecimalPipe } from '@angular/common'; + +// libs +import { + ChartComponent, + ApexAxisChartSeries, + ApexTitleSubtitle, + ApexDataLabels, + ApexChart, + ApexPlotOptions, + ApexLegend +} from "ng-apexcharts"; +import moment from 'moment'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { DashboardInOutModel } from '../../model/dashboard.model'; + +export type ChartOptions = { + series: ApexAxisChartSeries; + chart: ApexChart; + dataLabels: ApexDataLabels; + title: ApexTitleSubtitle; + plotOptions: ApexPlotOptions; + legend: ApexLegend; +}; + +@Component({ + selector: 'app-dashboard-income', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], +}) +export class DashboardIncomeComponent implements OnInit { + + @ViewChild("chart") chart!: ChartComponent; + public chartOptions!: Partial; + dashboardModel?: DashboardInOutModel; + total?: number; + public dateFrom: Date = moment(new Date).add(-30, 'days').toDate(); + public dateTo: Date = moment(new Date).add(0, 'days').toDate(); + + colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private _decimalPipe: DecimalPipe + ) { + + } + + ngOnInit() { + this.loadData(); + } + + onChangePeriod(dates: Date[]) { + this.dateFrom = dates[0]; + this.dateTo = dates[1]; + this.loadData(); + } + + loadData(update?: boolean, category?: string) { + let params = new HttpParams() + .set('from', moment(this.dateFrom).format('YYYY-MM-DD')) + .set('to', moment(this.dateTo).format('YYYY-MM-DD')) + .set('category', category || '') + ; + + this.httpClient + .get(ApiRoutes.DashboardIncome, { params: params }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dashboardModel = response; + + var series = response.data.map(x => ({ + x: x.name + '(' + (this._decimalPipe.transform(x.value, '1.2-2') || "") + ')', + y: x.value + })); + var min = Math.min(...response.data.map(x => x.value)); + var max = Math.max(...response.data.map(x => x.value)); + var step = max / 10; + var ranges = []; + for (var i = 0; i < this.colors.length; i++) { + ranges.push({ + from: i === 0 ? min : step * i, + to: i === this.colors.length - 1 ? max + 10 : step * (i + 1), + color: this.colors[this.colors.length - i - 1], + }); + } + + this.total = response.data.reduce((sum, current) => sum + current.value, 0); + + if (update) { + this.chart.updateSeries([{ + data: series + }]); + } else { + this.setChartOptions(series, ranges); + } + }); + } + + onRightClick(event?: Event) { + event?.preventDefault(); + this.loadData(true); + } + + private setChartOptions(data: any[], ranges: any[]) { + var self = this; + + this.chartOptions = { + series: [{ + data: data + }], + chart: { + type: 'treemap', + width: 600, + height: 600, + events: { + click: function (event, chartContext, config) { + var data = self.dashboardModel!.data[config.dataPointIndex]; + self.loadData(true, data.id); + } + } + }, + plotOptions: { + treemap: { + enableShades: true, + shadeIntensity: 0.5, + reverseNegativeShade: true, + colorScale: { + ranges: ranges + } + } + }, + }; + } +} diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html new file mode 100644 index 0000000..d372db6 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html @@ -0,0 +1,72 @@ +
+
+
+ + +
+ +
+
+
+
+

{{'PERIOD' | translate}}

+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+

{{'OUTCOME' | translate}}

+ +
+
+ + +
+ + + + + + + + + + + + + +
{{item.name}}{{item.valueRaw | number: '1.2'}}{{item.value | number: '1.2'}}
{{'TOTAL' | translate}}{{total | number: '1.2'}}
+
+
+
+
+
+ + +
+
diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.scss b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts new file mode 100644 index 0000000..00dd7f9 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Dashboard2Component } from './dashboard2.component'; + +describe('Dashboard2Component', + () => { + let component: Dashboard2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [Dashboard2Component] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(Dashboard2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts new file mode 100644 index 0000000..6db6b1c --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts @@ -0,0 +1,145 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { OnInit } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { HttpParams } from '@angular/common/http'; +import { DecimalPipe } from '@angular/common'; + +// libs +import { + ChartComponent, + ApexAxisChartSeries, + ApexTitleSubtitle, + ApexDataLabels, + ApexChart, + ApexPlotOptions, + ApexLegend +} from "ng-apexcharts"; +import moment from 'moment'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { DashboardInOutModel } from '../../model/dashboard.model'; + +export type ChartOptions = { + series: ApexAxisChartSeries; + chart: ApexChart; + dataLabels: ApexDataLabels; + title: ApexTitleSubtitle; + plotOptions: ApexPlotOptions; + legend: ApexLegend; +}; + +@Component({ + selector: 'app-dashboard-outcome', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], +}) +export class DashboardOutcomeComponent implements OnInit { + + @ViewChild("chart") chart!: ChartComponent; + public chartOptions!: Partial; + dashboardModel?: DashboardInOutModel; + total?: number; + public dateFrom: Date = moment(new Date).add(-30, 'days').toDate(); + public dateTo: Date = moment(new Date).add(0, 'days').toDate(); + + colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private _decimalPipe: DecimalPipe + ) { + } + + ngOnInit() { + this.loadData(); + } + + onChangePeriod(dates: Date[]) { + this.dateFrom = dates[0]; + this.dateTo = dates[1]; + this.loadData(); + } + + onRightClick(event?: Event) { + event?.preventDefault(); + this.loadData(true); + } + + private loadData(update?: boolean, category?: string) { + let params = new HttpParams() + .set('from', moment(this.dateFrom).format('YYYY-MM-DD')) + .set('to', moment(this.dateTo).format('YYYY-MM-DD')) + .set('category', category || '') + ; + + this.httpClient + .get(ApiRoutes.DashboardOutcome, { params: params }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dashboardModel = response; + + var series = response.data.map(x => ({ + x: x.name + ' (' + (this._decimalPipe.transform(x.value, '1.2-2') || '') + ')', + y: x.value + })); + var min = Math.min(...response.data.map(x => x.value)); + var max = Math.max(...response.data.map(x => x.value)); + var step = max / 10; + var ranges = []; + for (var i = 0; i < this.colors.length; i++) { + ranges.push({ + from: i === 0 ? min : step * i, + to: i === this.colors.length - 1 ? max + 10 : step * (i + 1), + color: this.colors[this.colors.length - i - 1], + }); + } + + this.total = response.data.reduce((sum, current) => sum + current.value, 0); + + if (update) { + this.chart.updateSeries([{ + data: series + }]); + } else { + this.setChartOptions(series, ranges); + } + }); + } + + private setChartOptions(data: any[], ranges: any[]) { + var self = this; + + this.chartOptions = { + series: [{ + data: data + }], + chart: { + type: 'treemap', + width: 600, + height: 600, + events: { + click: function (event, chartContext, config) { + var data = self.dashboardModel!.data[config.dataPointIndex]; + self.loadData(true, data.id); + } + } + }, + plotOptions: { + treemap: { + enableShades: true, + shadeIntensity: 0.5, + reverseNegativeShade: true, + colorScale: { + ranges: ranges + } + } + }, + }; + } +} diff --git a/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.html b/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.html new file mode 100644 index 0000000..d1c5fa2 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.ts b/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.ts new file mode 100644 index 0000000..f700c55 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/app-layout/auth-layout/auth-layout.component.ts @@ -0,0 +1,66 @@ +import { BidiModule, Direction } from '@angular/cdk/bidi'; +import { Component, DestroyRef, Inject, inject, Renderer2 } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { RouterOutlet } from '@angular/router'; +import { InConfiguration } from 'src/app/core/models/config.interface'; +import { DirectionService } from 'src/app/core/service/direction.service'; +import { ConfigService } from 'src/app/config/config.service'; +import { DOCUMENT } from '@angular/common'; + +@Component({ + selector: 'app-auth-layout', + templateUrl: './auth-layout.component.html', + styleUrls: [], + standalone: true, + imports: [BidiModule, RouterOutlet], +}) +export class AuthLayoutComponent { + direction!: Direction; + config!: InConfiguration; + private readonly destroyRef = inject(DestroyRef); + + constructor( + @Inject(DOCUMENT) private document: Document, + private directoryService: DirectionService, + private configService: ConfigService, + private renderer: Renderer2 + ) { + this.config = this.configService.configData; + this.directoryService.currentData + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((currentData) => { + if (currentData) { + this.direction = currentData === 'ltr' ? 'ltr' : 'rtl'; + } else { + if (localStorage.getItem('isRtl')) { + if (localStorage.getItem('isRtl') === 'true') { + this.direction = 'rtl'; + } else if (localStorage.getItem('isRtl') === 'false') { + this.direction = 'ltr'; + } + } else { + if (this.config) { + if (this.config.layout.rtl === true) { + this.direction = 'rtl'; + localStorage.setItem('isRtl', 'true'); + } else { + this.direction = 'ltr'; + localStorage.setItem('isRtl', 'false'); + } + } + } + } + }); + + // set theme on startup + if (localStorage.getItem('theme')) { + this.renderer.removeClass(this.document.body, this.config.layout.variant); + this.renderer.addClass( + this.document.body, + localStorage.getItem('theme') as string + ); + } else { + this.renderer.addClass(this.document.body, this.config.layout.variant); + } + } +} diff --git a/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.html b/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.html new file mode 100644 index 0000000..74b91bd --- /dev/null +++ b/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.html @@ -0,0 +1,6 @@ + + + +
+ +
diff --git a/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.ts b/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.ts new file mode 100644 index 0000000..b77f5eb --- /dev/null +++ b/MyOffice.SPA/src/app/layout/app-layout/main-layout/main-layout.component.ts @@ -0,0 +1,55 @@ +import { BidiModule, Direction } from '@angular/cdk/bidi'; +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { RouterOutlet } from '@angular/router'; +import { InConfiguration } from 'src/app/core/models/config.interface'; +import { DirectionService } from 'src/app/core/service/direction.service'; +import { ConfigService } from 'src/app/config/config.service'; +import { HeaderComponent } from '../../header/header.component'; +import { SidebarComponent } from '../../sidebar/sidebar.component'; +import { RightSidebarComponent } from '../../right-sidebar/right-sidebar.component'; + +@Component({ + selector: 'app-main-layout', + templateUrl: './main-layout.component.html', + styleUrls: [], + standalone: true, + imports: [BidiModule, RouterOutlet, HeaderComponent, SidebarComponent, RightSidebarComponent], +}) +export class MainLayoutComponent { + direction!: Direction; + config!: InConfiguration; + private readonly destroyRef = inject(DestroyRef); + + constructor( + private directoryService: DirectionService, + private configService: ConfigService + ) { + this.config = this.configService.configData; + this.directoryService.currentData + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((currentData) => { + if (currentData) { + this.direction = currentData === 'ltr' ? 'ltr' : 'rtl'; + } else { + if (localStorage.getItem('isRtl')) { + if (localStorage.getItem('isRtl') === 'true') { + this.direction = 'rtl'; + } else if (localStorage.getItem('isRtl') === 'false') { + this.direction = 'ltr'; + } + } else { + if (this.config) { + if (this.config.layout.rtl === true) { + this.direction = 'rtl'; + localStorage.setItem('isRtl', 'true'); + } else { + this.direction = 'ltr'; + localStorage.setItem('isRtl', 'false'); + } + } + } + } + }); + } +} diff --git a/MyOffice.SPA/src/app/layout/header/header.component.html b/MyOffice.SPA/src/app/layout/header/header.component.html new file mode 100644 index 0000000..d381862 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/header/header.component.html @@ -0,0 +1,179 @@ + diff --git a/MyOffice.SPA/src/app/layout/header/header.component.scss b/MyOffice.SPA/src/app/layout/header/header.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/header/header.component.scss @@ -0,0 +1 @@ + diff --git a/MyOffice.SPA/src/app/layout/header/header.component.spec.ts b/MyOffice.SPA/src/app/layout/header/header.component.spec.ts new file mode 100644 index 0000000..4cff7eb --- /dev/null +++ b/MyOffice.SPA/src/app/layout/header/header.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HeaderComponent } from './header.component'; +describe('HeaderComponent', + () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/layout/header/header.component.ts b/MyOffice.SPA/src/app/layout/header/header.component.ts new file mode 100644 index 0000000..c5bbdbe --- /dev/null +++ b/MyOffice.SPA/src/app/layout/header/header.component.ts @@ -0,0 +1,264 @@ +// angular +import { DOCUMENT } from '@angular/common'; +import { Component } from '@angular/core'; +import { Inject } from '@angular/core'; +import { ElementRef } from '@angular/core'; +import { OnInit } from '@angular/core'; +import { Renderer2 } from '@angular/core'; +import { AfterViewInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// libs +import { NgScrollbarModule } from 'ngx-scrollbar'; + +// app +import { ConfigService } from 'src/app/config/config.service'; +import { InConfiguration } from 'src/app/core/models/config.interface'; +import { AuthService } from 'src/app/core/service/auth.service'; +import { LanguageService } from 'src/app/core/service/language.service'; +import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter'; +import { SharedModule } from 'src/app/shared/shared.module'; + +interface Notifications { + message: string; + time: string; + icon: string; + color: string; + status: string; +} + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], + standalone: true, + imports: [SharedModule, NgScrollbarModule], +}) +export class HeaderComponent extends UnsubscribeOnDestroyAdapter implements OnInit, AfterViewInit { + + config!: InConfiguration; + + userImg?: string; + userName?: string; + + homePage?: string; + isNavbarCollapsed = true; + flagvalue: string | string[] | undefined; + countryName: string | string[] = []; + langStoreValue?: string; + defaultFlag?: string; + isOpenSidebar?: boolean; + docElement: HTMLElement | undefined; + isFullScreen = false; + + constructor( + @Inject(DOCUMENT) private document: Document, + private renderer: Renderer2, + public elementRef: ElementRef, + private configService: ConfigService, + private authService: AuthService, + private router: Router, + public languageService: LanguageService + ) { + super(); + + this.subs.sink = this.authService.currentUser$.subscribe(() => { + this.setUser(); + }); + } + + listLang = [ + { text: 'English', flag: 'assets/images/flags/us.jpg', lang: 'en' }, + { text: 'Spanish', flag: 'assets/images/flags/spain.jpg', lang: 'es' }, + { text: 'German', flag: 'assets/images/flags/germany.jpg', lang: 'de' }, + { text: 'Ukraine', flag: 'assets/images/flags/ukraine.png', lang: 'ua' }, + ]; + notifications: Notifications[] = [ + { + message: 'Please check your mail', + time: '14 mins ago', + icon: 'mail', + color: 'nfc-green', + status: 'msg-unread', + }, + { + message: 'New Employee Added..', + time: '22 mins ago', + icon: 'person_add', + color: 'nfc-blue', + status: 'msg-read', + }, + { + message: 'Your leave is approved!! ', + time: '3 hours ago', + icon: 'event_available', + color: 'nfc-orange', + status: 'msg-read', + }, + { + message: 'Lets break for lunch...', + time: '5 hours ago', + icon: 'lunch_dining', + color: 'nfc-blue', + status: 'msg-read', + }, + { + message: 'Employee report generated', + time: '14 mins ago', + icon: 'description', + color: 'nfc-green', + status: 'msg-read', + }, + { + message: 'Please check your mail', + time: '22 mins ago', + icon: 'mail', + color: 'nfc-red', + status: 'msg-read', + }, + { + message: 'Salary credited...', + time: '3 hours ago', + icon: 'paid', + color: 'nfc-purple', + status: 'msg-read', + }, + ]; + + ngOnInit() { + this.config = this.configService.configData; + + this.setUser(); + + this.homePage = 'dashboard/rests'; + + this.langStoreValue = localStorage.getItem('lang') as string; + const val = this.listLang.filter((x) => x.lang === this.langStoreValue); + this.countryName = val.map((element) => element.text); + if (val.length === 0) { + if (this.flagvalue === undefined) { + this.defaultFlag = 'assets/images/flags/us.jpg'; + } + } else { + this.flagvalue = val.map((element) => element.flag); + } + } + + ngAfterViewInit() { + // set theme on startup + if (localStorage.getItem('theme')) { + this.renderer.removeClass(this.document.body, this.config.layout.variant); + this.renderer.addClass( + this.document.body, + localStorage.getItem('theme') as string + ); + } else { + this.renderer.addClass(this.document.body, this.config.layout.variant); + } + + if (localStorage.getItem('menuOption')) { + this.renderer.addClass( + this.document.body, + localStorage.getItem('menuOption') as string + ); + } else { + this.renderer.addClass( + this.document.body, + 'menu_' + this.config.layout.sidebar.backgroundColor + ); + } + + if (localStorage.getItem('choose_logoheader')) { + this.renderer.addClass( + this.document.body, + localStorage.getItem('choose_logoheader') as string + ); + } else { + this.renderer.addClass( + this.document.body, + 'logo-' + this.config.layout.logo_bg_color + ); + } + + if (localStorage.getItem('sidebar_status')) { + if (localStorage.getItem('sidebar_status') === 'close') { + this.renderer.addClass(this.document.body, 'side-closed'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + } else { + this.renderer.removeClass(this.document.body, 'side-closed'); + this.renderer.removeClass(this.document.body, 'submenu-closed'); + } + } else { + if (this.config.layout.sidebar.collapsed === true) { + this.renderer.addClass(this.document.body, 'side-closed'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + } + } + } + + callFullscreen() { + if (!this.isFullScreen) { + this.docElement?.requestFullscreen(); + } else { + document.exitFullscreen(); + } + this.isFullScreen = !this.isFullScreen; + } + + setLanguage(text: string, lang: string, flag: string) { + this.countryName = text; + this.flagvalue = flag; + this.langStoreValue = lang; + this.languageService.setLanguage(lang); + } + + mobileMenuSidebarOpen(event: Event, className: string) { + const hasClass = (event.target as HTMLInputElement).classList.contains( + className + ); + if (hasClass) { + this.renderer.removeClass(this.document.body, className); + } else { + this.renderer.addClass(this.document.body, className); + } + + const hasClass2 = this.document.body.classList.contains('side-closed'); + if (hasClass2) { + // this.renderer.removeClass(this.document.body, "side-closed"); + this.renderer.removeClass(this.document.body, 'submenu-closed'); + } else { + // this.renderer.addClass(this.document.body, "side-closed"); + this.renderer.addClass(this.document.body, 'submenu-closed'); + } + } + + callSidemenuCollapse() { + const hasClass = this.document.body.classList.contains('side-closed'); + if (hasClass) { + this.renderer.removeClass(this.document.body, 'side-closed'); + this.renderer.removeClass(this.document.body, 'submenu-closed'); + } else { + this.renderer.addClass(this.document.body, 'side-closed'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + } + } + + logout() { + this.subs.sink = this.authService.logout().subscribe((res) => { + if (!res.success) { + this.router.navigate(['/authentication/signin']); + } + }); + } + + private setUser() { + this.userImg = this.authService.currentUserValue.img; + + this.userName = [ + this.authService.currentUserValue.firstName, + this.authService.currentUserValue.lastName + ].join(' ').trim(); + + this.userName = this.userName || this.authService.currentUserValue.userName; + } +} diff --git a/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.html b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.html new file mode 100644 index 0000000..c0c959c --- /dev/null +++ b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.html @@ -0,0 +1,2 @@ + + diff --git a/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.scss b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.scss @@ -0,0 +1 @@ + diff --git a/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.spec.ts b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.spec.ts new file mode 100644 index 0000000..d2479a5 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { PageLoaderComponent } from './page-loader.component'; +describe('PageLoaderComponent', + () => { + let component: PageLoaderComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [PageLoaderComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(PageLoaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.ts b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.ts new file mode 100644 index 0000000..5eab98c --- /dev/null +++ b/MyOffice.SPA/src/app/layout/page-loader/page-loader.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { LoadingBarModule } from '@ngx-loading-bar/core'; + +@Component({ + selector: 'app-page-loader', + templateUrl: './page-loader.component.html', + styleUrls: ['./page-loader.component.scss'], + standalone: true, + imports: [LoadingBarModule], +}) +export class PageLoaderComponent { + constructor() { + // constructor + } +} diff --git a/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.html b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.html new file mode 100644 index 0000000..9e15752 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.html @@ -0,0 +1,48 @@ +
+ + + + +
+
+
+ Setting Panel +
+
+
Select Layout
+
+
+ +
Light
+
+
+ +
Dark
+
+
+
+
+
Sidebar Menu Color
+ + Light + Dark + +
+
+
RTL Layout
+ +
+
+
+
+
diff --git a/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.scss b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.scss @@ -0,0 +1 @@ + diff --git a/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.spec.ts b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.spec.ts new file mode 100644 index 0000000..2528b67 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { RightSidebarComponent } from './right-sidebar.component'; +describe('RightSidebarComponent', + () => { + let component: RightSidebarComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [RightSidebarComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(RightSidebarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.ts b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.ts new file mode 100644 index 0000000..84f3a00 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/right-sidebar/right-sidebar.component.ts @@ -0,0 +1,258 @@ +import { DOCUMENT } from '@angular/common'; +import { + Component, + Inject, + ElementRef, + OnInit, + AfterViewInit, + Renderer2, + ChangeDetectionStrategy, + ChangeDetectorRef, +} from '@angular/core'; +import { ConfigService } from 'src/app/config/config.service'; +import { RightSidebarService } from 'src/app/core/service/rightsidebar.service'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { UnsubscribeOnDestroyAdapter } from 'src/app/shared/UnsubscribeOnDestroyAdapter'; +import { DirectionService } from 'src/app/core/service/direction.service'; +import { InConfiguration } from 'src/app/core/models/config.interface'; +import { SharedModule } from 'src/app/shared/shared.module'; +import { NgScrollbarModule } from 'ngx-scrollbar'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-right-sidebar', + templateUrl: './right-sidebar.component.html', + styleUrls: ['./right-sidebar.component.scss'], + standalone: true, + imports: [SharedModule, NgScrollbarModule], +}) +export class RightSidebarComponent +extends UnsubscribeOnDestroyAdapter +implements OnInit, AfterViewInit { + selectedBgColor = 'white'; + maxHeight!: string; + maxWidth!: string; + showpanel = false; + isOpenSidebar!: boolean; + isDarkSidebar = false; + isDarTheme = false; + innerHeight?: number; + headerHeight = 60; + isRtl = false; + config!: InConfiguration; + + constructor( + @Inject(DOCUMENT) private document: Document, + private renderer: Renderer2, + public elementRef: ElementRef, + private rightSidebarService: RightSidebarService, + private configService: ConfigService, + private directionService: DirectionService, + private cdr: ChangeDetectorRef + ) { + super(); + } + + ngOnInit() { + this.config = this.configService.configData; + this.subs.sink = this.rightSidebarService.sidebarState.subscribe( + (isRunning) => { + this.isOpenSidebar = isRunning; + } + ); + this.setRightSidebarWindowHeight(); + } + + ngAfterViewInit() { + // Light/dark skins only (clamp legacy color-skin values). + const storedSkin = localStorage.getItem('choose_skin_active'); + const skin = storedSkin === 'black' || this.config.layout.theme_color === 'black' + ? 'black' + : 'white'; + this.selectedBgColor = skin; + this.renderer.addClass(this.document.body, 'theme-' + skin); + localStorage.setItem('choose_skin', 'theme-' + skin); + localStorage.setItem('choose_skin_active', skin); + + if (localStorage.getItem('menuOption')) { + if (localStorage.getItem('menuOption') === 'menu_dark') { + this.isDarkSidebar = true; + } else if (localStorage.getItem('menuOption') === 'menu_light') { + this.isDarkSidebar = false; + } else { + this.isDarkSidebar = + this.config.layout.sidebar.backgroundColor === 'dark' ? true : false; + } + } else { + this.isDarkSidebar = + this.config.layout.sidebar.backgroundColor === 'dark' ? true : false; + } + + if (localStorage.getItem('theme')) { + if (localStorage.getItem('theme') === 'dark') { + this.isDarTheme = true; + } else if (localStorage.getItem('theme') === 'light') { + this.isDarTheme = false; + } else { + this.isDarTheme = this.config.layout.variant === 'dark' ? true : false; + } + } else { + this.isDarTheme = this.config.layout.variant === 'dark' ? true : false; + } + + // Content styles use body.dark; keep in sync with the layout toggle (theme-black alone is chrome-only). + if (this.isDarTheme) { + this.renderer.removeClass(this.document.body, 'light'); + this.renderer.addClass(this.document.body, 'dark'); + } else { + this.renderer.removeClass(this.document.body, 'dark'); + this.renderer.addClass(this.document.body, 'light'); + } + + if (localStorage.getItem('isRtl')) { + if (localStorage.getItem('isRtl') === 'true') { + this.setRTLSettings(); + } else if (localStorage.getItem('isRtl') === 'false') { + this.setLTRSettings(); + } + } else { + if (this.config.layout.rtl == true) { + this.setRTLSettings(); + } else { + this.setLTRSettings(); + } + } + this.cdr.markForCheck(); + } + + lightSidebarBtnClick() { + this.renderer.removeClass(this.document.body, 'menu_dark'); + this.renderer.removeClass(this.document.body, 'logo-black'); + this.renderer.addClass(this.document.body, 'menu_light'); + this.renderer.addClass(this.document.body, 'logo-white'); + this.isDarkSidebar = false; + localStorage.setItem('choose_logoheader', 'logo-white'); + localStorage.setItem('menuOption', 'menu_light'); + this.cdr.markForCheck(); + } + + darkSidebarBtnClick() { + this.renderer.removeClass(this.document.body, 'menu_light'); + this.renderer.removeClass(this.document.body, 'logo-white'); + this.renderer.addClass(this.document.body, 'menu_dark'); + this.renderer.addClass(this.document.body, 'logo-black'); + this.isDarkSidebar = true; + localStorage.setItem('choose_logoheader', 'logo-black'); + localStorage.setItem('menuOption', 'menu_dark'); + this.cdr.markForCheck(); + } + + lightThemeBtnClick() { + this.removeBodyThemeClasses(); + this.renderer.removeClass(this.document.body, 'dark'); + this.renderer.removeClass(this.document.body, 'menu_dark'); + this.renderer.removeClass(this.document.body, 'logo-black'); + + this.renderer.addClass(this.document.body, 'light'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + this.renderer.addClass(this.document.body, 'menu_light'); + this.renderer.addClass(this.document.body, 'logo-white'); + this.renderer.addClass(this.document.body, 'theme-white'); + + this.selectedBgColor = 'white'; + this.isDarkSidebar = false; + this.isDarTheme = false; + localStorage.setItem('choose_logoheader', 'logo-white'); + localStorage.setItem('choose_skin', 'theme-white'); + localStorage.setItem('choose_skin_active', 'white'); + localStorage.setItem('theme', 'light'); + localStorage.setItem('menuOption', 'menu_light'); + this.cdr.markForCheck(); + } + + darkThemeBtnClick() { + this.removeBodyThemeClasses(); + this.renderer.removeClass(this.document.body, 'light'); + this.renderer.removeClass(this.document.body, 'menu_light'); + this.renderer.removeClass(this.document.body, 'logo-white'); + + this.renderer.addClass(this.document.body, 'dark'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + this.renderer.addClass(this.document.body, 'menu_dark'); + this.renderer.addClass(this.document.body, 'logo-black'); + this.renderer.addClass(this.document.body, 'theme-black'); + + this.selectedBgColor = 'black'; + this.isDarkSidebar = true; + this.isDarTheme = true; + localStorage.setItem('choose_logoheader', 'logo-black'); + localStorage.setItem('choose_skin', 'theme-black'); + localStorage.setItem('choose_skin_active', 'black'); + localStorage.setItem('theme', 'dark'); + localStorage.setItem('menuOption', 'menu_dark'); + this.cdr.markForCheck(); + } + + /** Clears light/dark theme-* classes before applying a layout. */ + private removeBodyThemeClasses() { + ['theme-white', 'theme-black', 'theme-purple', 'theme-orange', 'theme-cyan', 'theme-green', 'theme-blue'] + .forEach((themeClass) => this.renderer.removeClass(this.document.body, themeClass)); + } + + setRightSidebarWindowHeight() { + this.innerHeight = window.innerHeight; + const height = this.innerHeight - this.headerHeight; + this.maxHeight = height + ''; + this.maxWidth = '500px'; + } + + onClickedOutside(event: Event) { + const button = event.target as HTMLButtonElement; + if (button.id !== 'settingBtn') { + if (this.isOpenSidebar === true) { + this.toggleRightSidebar(); + } + } + } + + toggleRightSidebar(): void { + this.rightSidebarService.setRightSidebar( + (this.isOpenSidebar = !this.isOpenSidebar) + ); + } + + switchDirection(event: MatSlideToggleChange) { + const isrtl = String(event.checked); + if ( + isrtl === 'false' && + document.getElementsByTagName('html')[0].hasAttribute('dir') + ) { + document.getElementsByTagName('html')[0].removeAttribute('dir'); + this.renderer.removeClass(this.document.body, 'rtl'); + this.directionService.updateDirection('ltr'); + } else if ( + isrtl === 'true' && + !document.getElementsByTagName('html')[0].hasAttribute('dir') + ) { + document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl'); + this.renderer.addClass(this.document.body, 'rtl'); + this.directionService.updateDirection('rtl'); + } + localStorage.setItem('isRtl', isrtl); + this.isRtl = event.checked; + } + + setRTLSettings() { + document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl'); + this.renderer.addClass(this.document.body, 'rtl'); + this.isRtl = true; + localStorage.setItem('isRtl', 'true'); + } + + setLTRSettings() { + document.getElementsByTagName('html')[0].removeAttribute('dir'); + this.renderer.removeClass(this.document.body, 'rtl'); + this.isRtl = false; + localStorage.setItem('isRtl', 'false'); + } +} diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts b/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts new file mode 100644 index 0000000..08d9e58 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts @@ -0,0 +1,142 @@ +import { RouteInfo } from './sidebar.metadata'; + +export const ROUTES: RouteInfo[] = [ + { + path: '', + title: 'MENUITEMS.MAIN.TEXT', + iconType: '', + icon: '', + class: '', + groupTitle: true, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: '', + title: 'MENUITEMS.DASHBOARD.TEXT', + iconType: 'feather', + icon: 'home', + class: 'menu-toggle', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [ + { + path: 'dashboard/rests', + title: 'MENUITEMS.DASHBOARD.LIST.DASHBOARD', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: 'dashboard/income', + title: 'MENUITEMS.DASHBOARD.LIST.INCOME', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: 'dashboard/outcome', + title: 'MENUITEMS.DASHBOARD.LIST.OUTCOME', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + ], + }, + + // Common Modules + { + id: 'accounts', + path: '', + title: 'MENUITEMS.ACCOUNTS.TEXT', + iconType: 'feather', + icon: 'chevrons-down', + class: 'menu-toggle', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [ + ], + }, + { + path: '', + title: 'MENUITEMS.SETTINGS.TEXT', + iconType: 'feather', + icon: 'settings', + class: 'menu-toggle', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [ + { + path: '/settings/currencies', + title: 'MENUITEMS.SETTINGS.LIST.CURRENCIES', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: '/settings/account-categories', + title: 'MENUITEMS.SETTINGS.LIST.ACCOUNTCATEGORIES', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: '/settings/accounts', + title: 'MENUITEMS.SETTINGS.LIST.ACCOUNTS', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: '/settings/item-categories', + title: 'MENUITEMS.SETTINGS.LIST.ITEMCATEGORIES', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: '/settings/items', + title: 'MENUITEMS.SETTINGS.LIST.ITEMS', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + ], + }, +]; diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.html b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.html new file mode 100644 index 0000000..aa8d3f3 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.html @@ -0,0 +1,75 @@ + diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.scss b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.scss @@ -0,0 +1 @@ + diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.spec.ts b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.spec.ts new file mode 100644 index 0000000..5d604f1 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { SidebarComponent } from './sidebar.component'; +describe('SidebarComponent', + () => { + let component: SidebarComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [SidebarComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(SidebarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.ts b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.ts new file mode 100644 index 0000000..f76d647 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar.component.ts @@ -0,0 +1,208 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// angular +import { Router, NavigationEnd } from '@angular/router'; +import { DOCUMENT } from '@angular/common'; +import { Component, DestroyRef, Inject, ElementRef, OnInit, Renderer2, HostListener, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { filter } from 'rxjs/operators'; + +// libs +import { NgScrollbarModule } from 'ngx-scrollbar'; +import { TranslateModule } from '@ngx-translate/core'; + +// app +import { ROUTES } from './sidebar-items'; +import { AuthService } from 'src/app/core/service/auth.service'; +import { AccountCategoryService } from '../../services/account.category.service'; +import { RouteInfo } from './sidebar.metadata'; +import { SharedModule } from 'src/app/shared/shared.module'; + +@Component({ + selector: 'app-sidebar', + templateUrl: './sidebar.component.html', + styleUrls: ['./sidebar.component.scss'], + standalone: true, + imports: [SharedModule, NgScrollbarModule, TranslateModule], +}) +export class SidebarComponent implements OnInit { + sidebarItems!: RouteInfo[]; + innerHeight?: number; + bodyTag!: HTMLElement; + listMaxHeight?: string; + listMaxWidth?: string; + userFullName?: string; + userImg?: string; + userType?: string; + headerHeight = 60; + currentRoute?: string; + menuIcon = 'radio_button_checked'; + private readonly destroyRef = inject(DestroyRef); + + constructor( + @Inject(DOCUMENT) private document: Document, + private renderer: Renderer2, + public elementRef: ElementRef, + private authService: AuthService, + private router: Router, + private accountCategoryService: AccountCategoryService, + ) { + this.elementRef.nativeElement.closest('body'); + this.router.events + .pipe( + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + // close sidebar on mobile screen after menu select + this.renderer.removeClass(this.document.body, 'overlay-open'); + }); + } + + @HostListener('window:resize', ['$event']) + windowResizecall() { + this.setMenuHeight(); + this.checkStatuForResize(false); + } + + @HostListener('document:mousedown', ['$event']) + onGlobalClick(event: Event): void { + if (!this.elementRef.nativeElement.contains(event.target)) { + this.renderer.removeClass(this.document.body, 'overlay-open'); + } + } + + callToggleMenu(event: Event, length: number) { + if (length > 0) { + const parentElement = (event.target as HTMLInputElement).closest('li'); + const activeClass = parentElement?.classList.contains('active'); + + if (activeClass) { + this.renderer.removeClass(parentElement, 'active'); + } else { + this.renderer.addClass(parentElement, 'active'); + } + } + } + + ngOnInit() { + if (this.authService.currentUserValue) { + this.userFullName = + this.authService.currentUserValue.firstName + + ' ' + + this.authService.currentUserValue.lastName; + this.userImg = this.authService.currentUserValue.img; + this.userType = 'Admin'; + this.sidebarItems = ROUTES.filter((sidebarItem) => sidebarItem); + + this.accountCategoryService + .getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(categories => { + var accounts = this.sidebarItems.filter(x => x.id === 'accounts'); + accounts[0].submenu = []; + for (var category of categories) { + accounts[0].submenu.push({ + path: '/account-category/' + category.id, + title: category.name!, + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + + }); + } + }); + } + + // this.sidebarItems = ROUTES.filter((sidebarItem) => sidebarItem); + this.initLeftSidebar(); + this.bodyTag = this.document.body; + } + + initLeftSidebar() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const _this = this; + // Set menu height + _this.setMenuHeight(); + _this.checkStatuForResize(true); + } + + setMenuHeight() { + this.innerHeight = window.innerHeight; + const height = this.innerHeight - this.headerHeight; + this.listMaxHeight = height + ''; + this.listMaxWidth = '500px'; + } + + isOpen() { + return this.bodyTag.classList.contains('overlay-open'); + } + + checkStatuForResize(firstTime: boolean) { + if (window.innerWidth < 1170) { + this.renderer.addClass(this.document.body, 'ls-closed'); + } else { + this.renderer.removeClass(this.document.body, 'ls-closed'); + } + } + + mouseHover() { + const body = this.elementRef.nativeElement.closest('body'); + if (body.classList.contains('submenu-closed')) { + this.renderer.addClass(this.document.body, 'side-closed-hover'); + this.renderer.removeClass(this.document.body, 'submenu-closed'); + } + } + + mouseOut() { + const body = this.elementRef.nativeElement.closest('body'); + if (body.classList.contains('side-closed-hover')) { + this.renderer.removeClass(this.document.body, 'side-closed-hover'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + } + } + + mobileMenuSidebarOpen(event: Event, className: string) { + const hasClass = (event.target as HTMLInputElement).classList.contains( + className + ); + if (hasClass) { + this.renderer.removeClass(this.document.body, className); + } else { + this.renderer.addClass(this.document.body, className); + } + } + + callSidemenuCollapse() { + const hasClass = this.document.body.classList.contains('side-closed'); + if (hasClass) { + this.renderer.removeClass(this.document.body, 'side-closed'); + this.renderer.removeClass(this.document.body, 'submenu-closed'); + this.menuIcon = 'radio_button_checked'; + } else { + this.renderer.addClass(this.document.body, 'side-closed'); + this.renderer.addClass(this.document.body, 'submenu-closed'); + this.menuIcon = 'radio_button_unchecked'; + } + + const sideClosedHover = + this.document.body.classList.contains('side-closed'); + if (sideClosedHover) { + this.renderer.removeClass(this.document.body, 'side-closed-hover'); + } + } + + logout() { + this.authService.logout() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((res) => { + if (!res.success) { + console.log('sidebar.logout'); + this.router.navigate(['/authentication/signin']); + } + }); + } +} diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar.metadata.ts b/MyOffice.SPA/src/app/layout/sidebar/sidebar.metadata.ts new file mode 100644 index 0000000..1be5931 --- /dev/null +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar.metadata.ts @@ -0,0 +1,13 @@ +// Sidebar route metadata +export interface RouteInfo { + id?: string; + path: string; + title: string; + iconType: string; + icon: string; + class: string; + groupTitle: boolean; + badge: string; + badgeClass: string; + submenu: RouteInfo[]; +} diff --git a/MyOffice.SPA/src/app/model/account.accessRight.model.ts b/MyOffice.SPA/src/app/model/account.accessRight.model.ts new file mode 100644 index 0000000..0836e3f --- /dev/null +++ b/MyOffice.SPA/src/app/model/account.accessRight.model.ts @@ -0,0 +1,10 @@ +import { UserModel } from '../core/models/user.model'; + +export interface AccountAccessRightModel { + user: UserModel; + isAllowRead: boolean; + isAllowWrite: boolean; + isAllowManage: boolean; + isAllowDelete: boolean; + isOwner: boolean; +} diff --git a/MyOffice.SPA/src/app/model/account.category.model.ts b/MyOffice.SPA/src/app/model/account.category.model.ts new file mode 100644 index 0000000..2d33b16 --- /dev/null +++ b/MyOffice.SPA/src/app/model/account.category.model.ts @@ -0,0 +1,5 @@ +export interface AccountCategoryModel { + id?: string, + name?: string, + allowDelete: boolean, +} diff --git a/MyOffice.SPA/src/app/model/account.detailed.model.ts b/MyOffice.SPA/src/app/model/account.detailed.model.ts new file mode 100644 index 0000000..6c0bcc9 --- /dev/null +++ b/MyOffice.SPA/src/app/model/account.detailed.model.ts @@ -0,0 +1,6 @@ +import { AccountModel } from './account.model'; + +export interface AccountDetailedModel { + account: AccountModel; + rest: number; +} diff --git a/MyOffice.SPA/src/app/model/account.invite.model.ts b/MyOffice.SPA/src/app/model/account.invite.model.ts new file mode 100644 index 0000000..7c45d0b --- /dev/null +++ b/MyOffice.SPA/src/app/model/account.invite.model.ts @@ -0,0 +1,5 @@ +export interface AccountInviteModel { + id?: string, + account?: string, + allowWrite: boolean, +} diff --git a/MyOffice.SPA/src/app/model/account.model.ts b/MyOffice.SPA/src/app/model/account.model.ts new file mode 100644 index 0000000..f617014 --- /dev/null +++ b/MyOffice.SPA/src/app/model/account.model.ts @@ -0,0 +1,15 @@ +import { AccountCategoryModel } from './account.category.model'; +import { AccountAccessRightModel } from './account.accessRight.model'; + +export interface AccountModel { + id?: string, + name?: string, + currencyId?: string, + currencyName?: string, + type?: string, + allowWrite: boolean, + allowDelete: boolean, + allowManage: boolean, + categories?: AccountCategoryModel[], + accessRights?: AccountAccessRightModel[], +} diff --git a/MyOffice.SPA/src/app/model/currency.model.ts b/MyOffice.SPA/src/app/model/currency.model.ts new file mode 100644 index 0000000..5ad985a --- /dev/null +++ b/MyOffice.SPA/src/app/model/currency.model.ts @@ -0,0 +1,12 @@ +export interface CurrencyModel { + id?: string, + name?: string, + code?: string, + shortName?: string, + symbol?: string, + quantity?: number, + rate?: number, + rateDate?: Date, + isConnected?: boolean, + isPrimary?: boolean, +} diff --git a/MyOffice.SPA/src/app/model/dashboard.model.ts b/MyOffice.SPA/src/app/model/dashboard.model.ts new file mode 100644 index 0000000..1545917 --- /dev/null +++ b/MyOffice.SPA/src/app/model/dashboard.model.ts @@ -0,0 +1,38 @@ +export interface DashboardModel { + incomeLast: number; + incomePrevious: number; + incomeChange: number; + outcomeLast: number; + outcomePrevious: number; + outcomeChange: number; + + balance: number; + balanceDebit: number; + balanceCredit: number; + + balanceRests: DashboardRestModel[]; +} + +export interface DashboardRestModel { + id: string; + name: string; + currencyName: string; + currencyShortName: string; + balance: number; + currencyRate: number; + currencyQuantity: number; + balanceAtRate: number; +} + +export interface DashboardInOutModel { + data: DashboardInOutItemModel[]; + details: DashboardInOutItemModel[]; +} + +export interface DashboardInOutItemModel { + id: string; + currency: string; + name: string; + value: number; + valueRaw: number; +} diff --git a/MyOffice.SPA/src/app/model/item.category.model.ts b/MyOffice.SPA/src/app/model/item.category.model.ts new file mode 100644 index 0000000..73a4191 --- /dev/null +++ b/MyOffice.SPA/src/app/model/item.category.model.ts @@ -0,0 +1,6 @@ +export interface ItemCategoryModel { + id?: string, + name?: string, + allowDelete: boolean, + internal: boolean, +} diff --git a/MyOffice.SPA/src/app/model/item.model.ts b/MyOffice.SPA/src/app/model/item.model.ts new file mode 100644 index 0000000..661682d --- /dev/null +++ b/MyOffice.SPA/src/app/model/item.model.ts @@ -0,0 +1,7 @@ +export interface ItemModel { + id?: string, + name?: string, + categoryId?: string, + category?: string, + allowDelete: boolean, +} diff --git a/MyOffice.SPA/src/app/model/motion.model.ts b/MyOffice.SPA/src/app/model/motion.model.ts new file mode 100644 index 0000000..40a82d5 --- /dev/null +++ b/MyOffice.SPA/src/app/model/motion.model.ts @@ -0,0 +1,11 @@ +export interface MotionModel { + id?: string; + date?: Date; + item?: string; + description?: string; + itemId?: string; + accountId?: string; + plus?: number; + minus?: number; + amountBalancing?: number; +} diff --git a/MyOffice.SPA/src/app/pages/accounts/account.component.html b/MyOffice.SPA/src/app/pages/accounts/account.component.html new file mode 100644 index 0000000..50c925d --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/account.component.html @@ -0,0 +1,22 @@ + + + +

{{account.account.name}}

+

+ {{account.rest | number: '1.2-6'}} + ({{account.account.currencyId}}) +

+
+
+
+ +
+
+ + + + +
diff --git a/MyOffice.SPA/src/app/pages/accounts/account.component.scss b/MyOffice.SPA/src/app/pages/accounts/account.component.scss new file mode 100644 index 0000000..493077c --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/account.component.scss @@ -0,0 +1,15 @@ +:host-context(body.dark)account-motion:nth-child(even) { + filter: brightness(1); +} + +:host-context(body.dark)account-motion:nth-child(odd) { + filter: brightness(1.75); +} + +:host-context(body.light)account-motion:nth-child(even) { + filter: brightness(0.75); +} + +:host-context(body.light)account-motion:nth-child(odd) { + filter: brightness(1); +} diff --git a/MyOffice.SPA/src/app/pages/accounts/account.component.ts b/MyOffice.SPA/src/app/pages/accounts/account.component.ts new file mode 100644 index 0000000..852fdfb --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/account.component.ts @@ -0,0 +1,110 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Input } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Output } from '@angular/core'; +import { EventEmitter } from '@angular/core'; + +// libs +import moment from 'moment'; +import { Observable } from 'rxjs'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { AccountDetailedModel } from '../../model/account.detailed.model'; +import { MotionModel } from '../../model/motion.model'; + +@Component({ + templateUrl: './account.component.html', + styleUrls: ['./account.component.scss'], + selector: 'account' +}) +export class AccountComponent { + public motions?: MotionModel[]; + public newMotion: MotionModel; + public dateFrom: Date = moment(new Date).add(-7, 'days').toDate(); + public dateTo: Date = moment(new Date).add(0, 'days').toDate(); + @Input('account') account!: AccountDetailedModel; + @Input('active') active: boolean = false; + @Input() reloadEvent!: Observable; + + @Output() onUpdate: EventEmitter = new EventEmitter(); + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private activatedRoute: ActivatedRoute + ) { + this.newMotion = { + date: new Date(), + plus: 0, + minus: 0, + }; + } + + ngOnInit(): void { + this.loadMotions(); + + this.reloadEvent + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x.find(id => id === this.account.account.id)) { + this.loadMotions(); + } + }); + } + + onSubmitClick() { + } + + handlerOnAdd(motions: MotionModel[]) { + this.onUpdate?.emit(motions.filter(x => x.accountId !== this.account.account!.id)); + this.loadMotions(); + + this.httpClient + .get(ApiRoutes.Account.replace(':id', this.account.account!.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.account = data; + }); + } + + handlerOnDelete(motion: MotionModel) { + const index = this.motions!.findIndex(x => x.id === motion.id); + this.motions!.splice(index, 1); + } + + onChangePeriod(dates: Date[]) { + this.dateFrom = dates[0]; + this.dateTo = dates[1]; + this.loadMotions(); + } + + private loadMotions() { + var url = ApiRoutes.Motions.replace(':id', this.account.account.id!); + url += '?from=' + moment(this.dateFrom).format('yyyy-MM-DD'); + url += '&to=' + moment(this.dateTo).format('yyyy-MM-DD'); + + this.httpClient + .get(url) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.motions = data; + }); + } + + afterExpand() { + var refresh = window.location.protocol + "//"; + refresh += window.location.host; + refresh += window.location.pathname; + refresh += window.location.hash; + var idx = refresh.indexOf('/account/'); + if (idx > -1) { + refresh = refresh.substr(0, idx); + } + refresh += '/account/' + this.account.account.id; + window.history.pushState({ path: refresh }, '', refresh); + } +} diff --git a/MyOffice.SPA/src/app/pages/accounts/account.list.component.html b/MyOffice.SPA/src/app/pages/accounts/account.list.component.html new file mode 100644 index 0000000..8af7ec9 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/account.list.component.html @@ -0,0 +1,21 @@ +
+
+
+ + + +
+
+
+ +
+ +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/accounts/account.list.component.scss b/MyOffice.SPA/src/app/pages/accounts/account.list.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/accounts/account.list.component.ts b/MyOffice.SPA/src/app/pages/accounts/account.list.component.ts new file mode 100644 index 0000000..c61c669 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/account.list.component.ts @@ -0,0 +1,62 @@ +// angular +import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; + +// libs +import { Subject } from 'rxjs'; + +// app +import { AccountCategoryService } from '../../services/account.category.service'; +import { ApiRoutes } from '../../api-routes'; +import { AccountDetailedModel } from '../../model/account.detailed.model'; +import { MotionModel } from '../../model/motion.model'; + +@Component({ + templateUrl: './account.list.component.html', + styleUrls: ['./account.list.component.scss'], +}) +export class AccountListComponent implements OnInit { + public accounts?: AccountDetailedModel[]; + public category = ''; + public activeAccount = ''; + + reloadSubject: Subject = new Subject(); + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private activatedRoute: ActivatedRoute, + private accountCategoryService: AccountCategoryService, + ) { + this.activatedRoute.params + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.activeAccount = x['accountId']); + } + + ngOnInit(): void { + this.activatedRoute.params + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(params => { + var categoryId = params['id']; + + this.httpClient + .get(ApiRoutes.Accounts + '?category=' + categoryId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.accounts = data; + }); + this.accountCategoryService + .getCategory(categoryId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.category = data.name!; + }); + }); + } + + handlerOnUpdate(motions: MotionModel[]) { + this.reloadSubject.next(motions.map(x => x.accountId!)); + } +} diff --git a/MyOffice.SPA/src/app/pages/accounts/motion.component.html b/MyOffice.SPA/src/app/pages/accounts/motion.component.html new file mode 100644 index 0000000..91e378b --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/motion.component.html @@ -0,0 +1,84 @@ +
+
+
+
+ + arrow_back + + + + + YYYY/MM/DD + + + + Please enter rate date + + + + + arrow_forward + +
+
+
+ + + Motion + !! + + + + Please enter motion + + + + {{item.name}} + + + + + + +
+
+ + + + Please enter motion + + + + + + Please enter motion + + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/accounts/motion.component.scss b/MyOffice.SPA/src/app/pages/accounts/motion.component.scss new file mode 100644 index 0000000..47f7e12 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/motion.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-option.mdc-list-item.mat-mdc-option-active { + filter: brightness(50%) +} diff --git a/MyOffice.SPA/src/app/pages/accounts/motion.component.ts b/MyOffice.SPA/src/app/pages/accounts/motion.component.ts new file mode 100644 index 0000000..82938a9 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/accounts/motion.component.ts @@ -0,0 +1,274 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder } from '@angular/forms'; +import { FormGroup } from '@angular/forms'; +import { Validators } from '@angular/forms'; +import { Input } from '@angular/core'; +import { Output } from '@angular/core'; +import { EventEmitter } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { ElementRef } from '@angular/core'; + +// libs +import moment from 'moment'; +import Swal from 'sweetalert2'; +import { debounceTime, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { pulseAnimation } from 'angular-animations'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { AccountModel } from '../../model/account.model'; +import { MotionModel } from '../../model/motion.model'; +import { ItemModel } from '../../model/item.model'; +import { atLeastOneNumber } from '../../core/validators/atleastonenumber.validator'; + +@Component({ + selector: 'account-motion', + templateUrl: './motion.component.html', + styleUrls: ['./motion.component.scss'], + animations: [ + pulseAnimation({ direction: '<=>', duration: 200 }), + ], +}) +export class MotionComponent { + public form!: FormGroup; + public errorMessage?: string; + public filteredItems: ItemModel[] = []; + public option?: string; + public selectedItem?: ItemModel; + public inProgress: boolean = false; + + @Input('motion') motion!: MotionModel; + @Input('account') account!: AccountModel; + + @Output() onAdd: EventEmitter = new EventEmitter(); + @Output() onDelete: EventEmitter = new EventEmitter(); + + @ViewChild('motionInput') motionInput?: ElementRef; + @ViewChild('dateInput') dateInput?: ElementRef; + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + ) { + } + + ngOnInit(): void { + this.form = this.fb.group({ + id: [ + this.motion.id, + ], + date: [ + this.motion.date, + [Validators.required], + ], + item: [ + this.motion.item, + [Validators.required], + ], + description: [ + this.motion.description, + ], + plus: [ + this.motion.plus, + ], + minus: [ + this.motion.minus, + ], + }, { + validator: atLeastOneNumber(Validators.required, ['plus', 'minus']) + }); + + this.form.controls['item'].valueChanges + .pipe( + debounceTime(200), + switchMap(value => { + if (!value || typeof value !== 'string') { + return of([] as ItemModel[]); + } + this.selectedItem = undefined; + return this.httpClient.get( + ApiRoutes.Items + '?term=' + encodeURIComponent(value) + ); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(x => { + this.filteredItems = x; + }); + + this.form.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.form.controls['plus'].markAsPristine(); + this.form.controls['plus'].markAsUntouched(); + this.form.controls['plus'].setErrors(null); + this.form.controls['minus'].markAsPristine(); + this.form.controls['minus'].markAsUntouched(); + this.form.controls['minus'].setErrors(null); + }); + } + + displayFn(item: any): string { + var value = item && item.name ? item.name : item; + return value; + } + + selectedFn(item: ItemModel) { + this.selectedItem = item; + } + + add() { + this.form.markAllAsTouched(); + + if (this.form.hasError('atLeastOne')) { + this.form.get('plus')!.setErrors(this.form.errors); + this.form.get('minus')!.setErrors(this.form.errors); + return; + } + + if (!this.form.valid) { + return; + } + + this.setInProgress(); + + var data: MotionModel = { + date: this.form.value.date, + description: this.form.value.description, + plus: this.form.value.plus || 0, + minus: this.form.value.minus || 0, + }; + + if (this.form.value.item.name) { + data.date = this.form.value.date; + data.item = this.form.value.item.name; + data.itemId = this.form.value.item.id; + data.accountId = this.form.value.item.accountId; + data.amountBalancing = data.plus! > 0 ? data.plus : data.minus; + } else { + data.item = this.form.value.item; + } + + this.httpClient + .post(ApiRoutes.Motions.replace(':id', this.account.id!), data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: response => { + this.onAdd?.emit(response); + + this.form.patchValue({ + item: '', + plus: 0, + minus: 0, + }); + + this.filteredItems = []; + this.motionInput?.nativeElement.focus(); + this.form.markAsUntouched(); + this.setInProgress(false); + }, + error: error => { + this.errorMessage = error.detail; + this.setInProgress(false); + } + }); + } + + private setInProgress(inProgress: boolean = true) { + this.inProgress = inProgress; + } + + update() { + this.form.markAllAsTouched(); + + if (!this.form.valid) { + if (this.form.hasError('atLeastOne')) { + this.form.get('plus')!.setErrors(this.form.errors); + this.form.get('minus')!.setErrors(this.form.errors); + } + return; + } + + this.setInProgress(); + + var url = ApiRoutes.Motion + .replace(':id', this.account.id!) + .replace(':motionId', this.motion.id!); + + var data = this.form.value; + data.motion = data.item.name + ? data.item.name + : data.motion; + data.itemId = this.selectedItem?.id; + + this.httpClient + .put(url, data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.form.markAsUntouched(); + this.setInProgress(false); + }, + error: error => { + this.errorMessage = error.detail; + this.setInProgress(false); + } + }); + } + + delete() { + Swal.fire({ + title: 'Delete motion ' + this.motion.item, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + var url = ApiRoutes.Motion + .replace(':id', this.account.id!) + .replace(':motionId', this.motion.id!); + + this.setInProgress(); + + this.httpClient.delete(url) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: data => { + resolve(data); + this.setInProgress(false); + }, + error: error => { + Swal.showValidationMessage(error.detail); + reject(); + this.setInProgress(false); + } + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + if (result.isConfirmed) { + this.onDelete?.emit((result.value! as MotionModel)); + } + }); + } + + import() { + + } + + addDay(days: number) { + var date = moment(this.form!.get('date')!.value).add(days, 'days').toDate(); + this.form.patchValue({ + date: date, + }); + } +} diff --git a/MyOffice.SPA/src/app/pages/pages-routing.module.ts b/MyOffice.SPA/src/app/pages/pages-routing.module.ts new file mode 100644 index 0000000..9341c68 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/pages-routing.module.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { UserProfileComponent } from './user-profile/user-profile.component'; +import { SettingsCurrencyComponent } from './settings/currency/currency.component'; +import { SettingsAccountCategoryComponent } from './settings/account/account.category.component'; +import { SettingsAccountComponent } from './settings/account/account.component'; +import { SettingsItemCategoryComponent } from './settings/item/item.category.component'; +import { SettingsItemComponent } from './settings/item/item.component'; +import { AccountListComponent } from './accounts/account.list.component'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'signin', + pathMatch: 'full', + }, + { + path: 'user/profile', + component: UserProfileComponent, + }, + { + path: 'settings/currencies', + component: SettingsCurrencyComponent, + }, + { + path: 'settings/account-categories', + component: SettingsAccountCategoryComponent, + }, + { + path: 'settings/accounts', + component: SettingsAccountComponent, + }, + { + path: 'settings/item-categories', + component: SettingsItemCategoryComponent, + }, + { + path: 'settings/items', + component: SettingsItemComponent, + }, + { + path: 'account-category/:id', + component: AccountListComponent, + }, + { + path: 'account-category/:id/account/:accountId', + component: AccountListComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class PagesRoutingModule { +} diff --git a/MyOffice.SPA/src/app/pages/pages.module.ts b/MyOffice.SPA/src/app/pages/pages.module.ts new file mode 100644 index 0000000..7a18563 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/pages.module.ts @@ -0,0 +1,102 @@ +// angular +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; + +// libs +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { MatCardModule } from '@angular/material/card'; +import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; +import { NgxCurrencyDirective } from 'ngx-currency'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatMenuModule } from '@angular/material/menu'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; + +// app +import { PagesRoutingModule } from './pages-routing.module'; +import { ComponentsModule } from '../shared/components/components.module'; +import { SettingsCurrencyComponent } from './settings/currency/currency.component'; +import { SettingsConnectCurrencyComponent } from './settings/currency/connect.currency.component'; +import { UserProfileComponent } from './user-profile/user-profile.component'; +import { SettingsRateCurrencyComponent } from './settings/currency/rate.currency.component'; +import { SettingsAccountCategoryComponent } from './settings/account/account.category.component'; +import { SettingsAccountComponent } from './settings/account/account.component'; +import { SettingsAccountAddComponent } from './settings/account/add.account.component'; +import { SettingsAccountEditComponent } from './settings/account/edit.account.component'; +import { SettingsAccountAccessComponent } from './settings/account/access.account.component'; +import { CurrencyService } from '../services/currency.service'; +import { ItemService } from '../services/item.service'; +import { SettingsItemCategoryComponent } from './settings/item/item.category.component'; +import { SettingsItemComponent } from './settings/item/item.component'; +import { SettingsItemEditComponent } from './settings/item/edit.item.component'; +import { AccountListComponent } from './accounts/account.list.component'; +import { AccountComponent } from './accounts/account.component'; +import { MotionComponent } from './accounts/motion.component'; +import { SettingsItemCategoryEditComponent } from './settings/item/edit.item.category.component'; + +@NgModule({ + declarations: [ + UserProfileComponent, + + SettingsCurrencyComponent, + SettingsConnectCurrencyComponent, + SettingsRateCurrencyComponent, + + SettingsAccountCategoryComponent, + SettingsAccountComponent, + SettingsAccountAddComponent, + SettingsAccountEditComponent, + SettingsAccountAccessComponent, + + SettingsItemCategoryComponent, + SettingsItemComponent, + SettingsItemEditComponent, + SettingsItemCategoryEditComponent, + + AccountListComponent, + AccountComponent, + MotionComponent, + ], + imports: [ + CommonModule, + ComponentsModule, + FormsModule, + ReactiveFormsModule, + PagesRoutingModule, + MatFormFieldModule, + MatSelectModule, + MatInputModule, + MatIconModule, + MatButtonModule, + MatTabsModule, + MatDialogModule, + MatDatepickerModule, + MatGridListModule, + MatCardModule, + MatExpansionModule, + MatAutocompleteModule, + NgxMaskDirective, + NgxMaskPipe, + NgxCurrencyDirective, + MatCheckboxModule, + MatMenuModule, + ], + providers: [ + { provide: MAT_DATE_LOCALE, useValue: 'en-GB' }, + CurrencyService, + ItemService, + ], +}) +export class PagesModule { +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/access.account.component.html b/MyOffice.SPA/src/app/pages/settings/account/access.account.component.html new file mode 100644 index 0000000..78ddef0 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/access.account.component.html @@ -0,0 +1,47 @@ +
+
+ + + + + + +
+
+ +
+ + Email + + +
+
+
+
User rights
+
+ + Allow write + +
+
+
+
+
+
+ + {{access.user.email}} + (owner) +
+
+ + Allow write + +
+
+ +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/account/access.account.component.scss b/MyOffice.SPA/src/app/pages/settings/account/access.account.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/account/access.account.component.ts b/MyOffice.SPA/src/app/pages/settings/account/access.account.component.ts new file mode 100644 index 0000000..818d251 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/access.account.component.ts @@ -0,0 +1,128 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { Inject } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +// libs +import Swal from 'sweetalert2'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import * as ld from 'lodash'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { AccountModel } from '../../../model/account.model'; +import { MatDialogRef } from '@angular/material/dialog'; +import { CurrencyService } from '../../../services/currency.service'; +import { CurrencyModel } from '../../../model/currency.model'; +import { AccountCategoryService } from '../../../services/account.category.service'; +import { AccountCategoryModel } from '../../../model/account.category.model'; +import { AccountAccessRightModel } from '../../../model/account.accessRight.model'; +import { AccountService } from '../../../services/account.service'; + +@Component({ + templateUrl: './access.account.component.html', + styleUrls: ['./access.account.component.scss'], +}) +export class SettingsAccountAccessComponent { + public editForm!: FormGroup; + public errorMessage?: string; + public currencies?: CurrencyModel[]; + public categories?: AccountCategoryModel[]; + public types = new Map(); + + public accessRightsSorted: AccountAccessRightModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + private accountService: AccountService, + @Inject(MAT_DIALOG_DATA) public account: AccountModel + ) { + this.types = this.accountService.getTypes(); + + this.accessRightsSorted = ld.orderBy(account.accessRights!, ['isOwner', 'user.email'], ['desc', 'asc']); + + } + + ngOnInit(): void { + const accesses = this.fb.array( + this.accessRightsSorted.map(x => this.fb.group({ + userId: [ + x.user.id + ], + allowWrite: [{ + value: x.isAllowWrite, + disabled: !x.isAllowDelete, + }], + })) + ); + + this.editForm = this.fb.group({ + email: [ + '', + ], + allowWrite: [ + false, + ], + accesses: accesses, + }); + } + + removeAccessRight(accessRight: AccountAccessRightModel) { + Swal.fire({ + title: 'Delete access ' + accessRight.user.email, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + var url = ApiRoutes.SettingsAccountAccess + .replace(':id', this.account.id!) + .replace(':access', accessRight.user.id); + + this.errorMessage = undefined; + this.httpClient + .delete(url, this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + resolve(response); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + if (result.isConfirmed) { + this.dialogRef.close({ refresh: true }); + } + }); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + if (this.editForm.valid) { + this.errorMessage = undefined; + this.httpClient + .post(ApiRoutes.SettingsAccountAccesses.replace(':id', this.account.id!), this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.category.component.html b/MyOffice.SPA/src/app/pages/settings/account/account.category.component.html new file mode 100644 index 0000000..55ca473 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/account.category.component.html @@ -0,0 +1,47 @@ +
+
+
+ + + +
+
+
+ + + + + Account categories + + + + +
+ +
+ +
+ + + + + + + +
{{item.name}} + + +
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.category.component.scss b/MyOffice.SPA/src/app/pages/settings/account/account.category.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.category.component.ts b/MyOffice.SPA/src/app/pages/settings/account/account.category.component.ts new file mode 100644 index 0000000..2c6c309 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/account.category.component.ts @@ -0,0 +1,129 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; + +// libs +import Swal from 'sweetalert2'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { AccountCategoryModel } from '../../../model/account.category.model'; + +@Component({ + templateUrl: './account.category.component.html', + styleUrls: ['./account.category.component.scss'], +}) +export class SettingsAccountCategoryComponent { + public categories?: AccountCategoryModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + ) { + } + + ngOnInit(): void { + this.load(); + } + + private load() { + this.httpClient + .get(ApiRoutes.SettingsAccountCategories) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.categories = data; + }); + } + + public add() { + Swal.fire({ + title: 'Account category name', + input: 'text', + inputAttributes: { + autocapitalize: 'off', + }, + showCancelButton: true, + confirmButtonText: 'Add', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.post(ApiRoutes.SettingsAccountCategories, { name: name }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + } + + public edit(category: AccountCategoryModel) { + Swal.fire({ + title: 'Account category name', + input: 'text', + inputValue: category.name, + inputAttributes: { + autocapitalize: 'off', + }, + showCancelButton: true, + confirmButtonText: 'Update', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.put(ApiRoutes.SettingsAccountCategory.replace(':id', category.id!), { name: name }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + } + + public remove(category: AccountCategoryModel) { + Swal.fire({ + title: 'Delete account category ' + category.name, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.delete(ApiRoutes.SettingsAccountCategory.replace(':id', category.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.component.html b/MyOffice.SPA/src/app/pages/settings/account/account.component.html new file mode 100644 index 0000000..7f820bc --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/account.component.html @@ -0,0 +1,99 @@ +
+
+
+ + + +
+
+
+ + + + + Accounts + + + + +
+
+ + + + +
+ +
+ +
+ + + + + + + + + + + +
{{account.name}}{{account.currencyId}}{{account.type}} + + + +
+
+ + + + + + + + + + +
{{account.name}}{{account.currencyId}}{{account.type}} + + +
+
+ + + + + + + + + +
{{account.name}}{{account.currencyId}} + + +
+
+
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.component.scss b/MyOffice.SPA/src/app/pages/settings/account/account.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/account/account.component.ts b/MyOffice.SPA/src/app/pages/settings/account/account.component.ts new file mode 100644 index 0000000..d2bcf46 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/account.component.ts @@ -0,0 +1,214 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; + +// libs +import Swal from 'sweetalert2'; +import { MatDialog } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { AccountModel } from '../../../model/account.model'; +import { AccountCategoryModel } from '../../../model/account.category.model'; +import { AccountInviteModel } from '../../../model/account.invite.model'; +import { SettingsAccountAddComponent } from './add.account.component'; +import { SettingsAccountEditComponent } from './edit.account.component'; +import { SettingsAccountAccessComponent } from './access.account.component'; +import { AccountCategoryService } from '../../../services/account.category.service'; + +@Component({ + templateUrl: './account.component.html', + styleUrls: ['./account.component.scss'], +}) +export class SettingsAccountComponent { + public accountCategories?: AccountCategoryModel[]; + public accounts?: AccountModel[]; + public invites?: AccountInviteModel[]; + public tabIndex = 0; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private dialogModel: MatDialog, + private accountCategoryService: AccountCategoryService, + ) { + } + + ngOnInit(): void { + this.loadCategories(); + this.loadInvites(); + } + + private loadAccounts(accountToShow?: string) { + this.httpClient + .get(ApiRoutes.SettingsAccounts) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.accounts = data; + if (accountToShow) { + var idx = -1; + var account = this.accounts.find(x => x.id === accountToShow); + if (account && account.categories && account.categories.length > 0) { + for (var i = 0; i < this.accountCategories!.length; i++) { + if (account!.categories!.find(c => c.id === this.accountCategories![i].id)) { + idx = i; + break; + } + } + } + this.tabIndex = idx === -1 ? this.accountCategories!.length : idx; + } + }); + } + + private loadInvites() { + return this.httpClient + .get(ApiRoutes.SettingsAccountInvites) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.invites = response; + }); + } + + private loadCategories() { + this.accountCategoryService + .getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + this.accountCategories = x; + this.loadAccounts(); + }); + } + + public accountInCategory(category?: AccountCategoryModel) { + if (this.accounts) { + return this.accounts.filter(acc => (!category && (!acc.categories || acc.categories.length === 0)) || (category && acc.categories && acc.categories.filter(cat => cat.id === category.id).length > 0)); + } + return null; + } + + public add() { + this.dialogModel.open(SettingsAccountAddComponent, { + width: '640px', + disableClose: true, + data: this.accounts, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.loadAccounts(); + } + }); + } + + public edit(account: AccountModel) { + this.dialogModel.open(SettingsAccountEditComponent, { + width: '640px', + disableClose: true, + data: account, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.loadAccounts(); + } + }); + } + + public remove(account: AccountModel) { + Swal.fire({ + title: 'Remove account ' + account.name, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.delete(ApiRoutes.SettingsAccount.replace(':id', account.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.loadAccounts(); + }); + } + + public access(account: AccountModel) { + this.dialogModel.open(SettingsAccountAccessComponent, { + width: '640px', + disableClose: true, + data: account, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.loadAccounts(); + } + }); + } + + public startInvite(invite: AccountInviteModel) { + Swal.fire({ + title: 'Accept to invite the account "' + invite.account + '"', + input: 'text', + inputLabel: 'Accept with name', + inputValue: invite.account, + showDenyButton: true, + showCancelButton: true, + confirmButtonText: 'Accept', + denyButtonText: 'Reject', + showLoaderOnConfirm: true, + preConfirm: (input) => { + return new Promise((resolve, reject) => { + this.httpClient.post(ApiRoutes.SettingsAccountInviteAccept.replace(':id', invite.id!), { name: input }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + preDeny: (x) => { + return new Promise((resolve, reject) => { + this.httpClient.post(ApiRoutes.SettingsAccountInviteReject.replace(':id', invite.id!), null) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + var id; + if (result.value) { + var account = result.value as AccountModel; + id = account.id; + } + this.loadInvites(); + this.loadAccounts(id); + }); + + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/add.account.component.html b/MyOffice.SPA/src/app/pages/settings/account/add.account.component.html new file mode 100644 index 0000000..5f94441 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/add.account.component.html @@ -0,0 +1,62 @@ +
+
+ + + + + + +
+
+
+ + Type + + + {{type.key}} ({{type.value}}) + + + +
+
+
+ +
+
+
+ + Name + + +
+
+
+
+ + Currency + + + {{currency.code}} ({{currency.name}}) + + + +
+
+
+
+
+
+ + Category + + + {{category.name}} + + + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/account/add.account.component.scss b/MyOffice.SPA/src/app/pages/settings/account/add.account.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/account/add.account.component.ts b/MyOffice.SPA/src/app/pages/settings/account/add.account.component.ts new file mode 100644 index 0000000..70688cd --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/add.account.component.ts @@ -0,0 +1,101 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import Swal from 'sweetalert2'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { AccountModel } from '../../../model/account.model'; +import { MatDialogRef } from '@angular/material/dialog'; +import { CurrencyService } from '../../../services/currency.service'; +import { CurrencyModel } from '../../../model/currency.model'; +import { AccountCategoryService } from '../../../services/account.category.service'; +import { AccountCategoryModel } from '../../../model/account.category.model'; +import { AuthService } from '../../../core/service/auth.service'; +import { AccountService } from '../../../services/account.service'; + +@Component({ + templateUrl: './add.account.component.html', + styleUrls: ['./add.account.component.scss'], +}) +export class SettingsAccountAddComponent { + public editForm!: FormGroup; + public errorMessage?: string; + public currencies?: CurrencyModel[]; + public categories?: AccountCategoryModel[]; + public username: string; + public types = new Map(); + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + private currencyService: CurrencyService, + private accountCategoryService: AccountCategoryService, + private authService: AuthService, + private accountService: AccountService, + @Inject(MAT_DIALOG_DATA) public account: AccountModel + ) { + this.username = authService.currentUserValue.userName; + this.types = this.accountService.getTypes(); + } + + ngOnInit(): void { + this.editForm = this.fb.group({ + id: [ + this.account.id, + ], + name: [ + this.account.name, + [Validators.required], + ], + currencyId: [ + this.account.currencyId, + [Validators.required], + ], + categoryId: [ + '', + [Validators.required], + ], + type: [ + this.account.type + ] + }); + + this.currencyService + .getCurrencies() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.currencies = x); + + this.accountCategoryService + .getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.categories = x); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + if (this.editForm.valid) { + this.errorMessage = undefined; + this.httpClient + .post(ApiRoutes.SettingsAccounts, this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.html b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.html new file mode 100644 index 0000000..913a55a --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.html @@ -0,0 +1,93 @@ +
+
+ + + + + + +
+
+
+ + Type + + + {{type.key}} ({{type.value}}) + + + +
+
+
+ +
+
+
+ + Name + + +
+
+
+
+ + Currency + + + {{currency.code}} ({{currency.name}}) + + + +
+
+
+ +
+
+ + + + + + + + +
{{category.name}} + +
+ +
No categories assigned
+
+
+
+ +
+
+ +
+ + Category + + + {{category.name}} + + + + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.scss b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.scss new file mode 100644 index 0000000..07f1433 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.scss @@ -0,0 +1,24 @@ +.section-label { + display: block; + font-weight: 500; + margin-bottom: 8px; +} + +.category-add-block { + margin-bottom: 8px; +} + +.category-add-row { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.category-add-select { + flex: 1; +} + +.empty-categories { + opacity: 0.7; + padding: 8px 0 16px; +} diff --git a/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.ts b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.ts new file mode 100644 index 0000000..d5550e1 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/account/edit.account.component.ts @@ -0,0 +1,161 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import Swal from 'sweetalert2'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import * as ld from 'lodash'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { AccountModel } from '../../../model/account.model'; +import { MatDialogRef } from '@angular/material/dialog'; +import { CurrencyService } from '../../../services/currency.service'; +import { CurrencyModel } from '../../../model/currency.model'; +import { AccountCategoryService } from '../../../services/account.category.service'; +import { AccountCategoryModel } from '../../../model/account.category.model'; +import { AccountAccessRightModel } from '../../../model/account.accessRight.model'; +import { AccountService } from '../../../services/account.service'; + +@Component({ + templateUrl: './edit.account.component.html', + styleUrls: ['./edit.account.component.scss'], +}) +export class SettingsAccountEditComponent { + public editForm!: FormGroup; + public errorMessage?: string; + public currencies?: CurrencyModel[]; + public categories?: AccountCategoryModel[]; + public types = new Map(); + + public categoriesSorted: AccountCategoryModel[]; + public accessRightsSorted: AccountAccessRightModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + private currencyService: CurrencyService, + private accountCategoryService: AccountCategoryService, + private accountService: AccountService, + @Inject(MAT_DIALOG_DATA) public account: AccountModel + ) { + this.types = this.accountService.getTypes(); + + this.categoriesSorted = ld.orderBy(account.categories ?? [], ['name'], ['asc']); + this.accessRightsSorted = ld.orderBy(account.accessRights!, ['isOwner', 'user.email'], ['desc', 'asc']); + } + + /** Categories not yet assigned to this account (for the Add select). */ + get availableCategories(): AccountCategoryModel[] { + const assigned = new Set((this.categoriesSorted ?? []).map(c => c.id)); + return (this.categories ?? []).filter(c => !assigned.has(c.id)); + } + + ngOnInit(): void { + this.editForm = this.fb.group({ + id: [ + this.account.id, + ], + name: [ + this.account.name, + [Validators.required], + ], + currencyId: [ + this.account.currencyId, + [Validators.required], + ], + categoryId: [ + '', + ], + type: [ + this.account.type, + ], + userEmail: [ + '', + ], + }); + + this.currencyService + .getCurrencies() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.currencies = x); + + this.accountCategoryService + .getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.categories = x); + } + + addCategory() { + const categoryId = this.editForm.get('categoryId')?.value; + if (!categoryId || !this.editForm.valid) { + this.editForm.markAllAsTouched(); + return; + } + + this.errorMessage = undefined; + this.httpClient + .put(ApiRoutes.SettingsAccount.replace(':id', this.account.id!), this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: account => { + this.applyAccountCategories(account); + this.editForm.patchValue({ categoryId: '' }); + }, + error: error => { + this.errorMessage = error.detail; + }, + }); + } + + removeCategory(category: AccountCategoryModel) { + const url = ApiRoutes.SettingsAccountAccountCategory + .replace(':id', this.account.id!) + .replace(':categoryId', category.id!); + + this.errorMessage = undefined; + this.httpClient + .delete(url) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: account => this.applyAccountCategories(account), + error: error => { + this.errorMessage = error.detail; + }, + }); + } + + private applyAccountCategories(account: AccountModel) { + this.account.categories = account.categories ?? []; + this.categoriesSorted = ld.orderBy(this.account.categories, ['name'], ['asc']); + } + + removeAccessRight(accessRight: AccountAccessRightModel) { + + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + if (this.editForm.valid) { + this.errorMessage = undefined; + this.httpClient + .put(ApiRoutes.SettingsAccount.replace(':id', this.account.id!), this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.html b/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.html new file mode 100644 index 0000000..8d21ed8 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.html @@ -0,0 +1,85 @@ +
+
+ + + + + + +
+
+
+ + Symbol + + +
+
+
+
+ + Code + + +
+
+
+
+
+
+ + Name + + +
+
+
+
+ + Short Name + + +
+
+
+
+
+
+ + Rate + + + Please enter rate + + +
+
+
+
+ + Quantity + + + Please enter rate + + +
+
+
+
+ + Rate Date + + YYYY/MM/DD + + + + Please enter rate date + + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.scss b/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.ts b/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.ts new file mode 100644 index 0000000..7d142ee --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/connect.currency.component.ts @@ -0,0 +1,80 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import { MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { CurrencyModel } from '../../../model/currency.model'; + +@Component({ + templateUrl: './connect.currency.component.html', + styleUrls: ['./connect.currency.component.scss'], +}) +export class SettingsConnectCurrencyComponent { + public addForm!: FormGroup; + public errorMessage?: string; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public currency: CurrencyModel + ) { + } + + public ngOnInit(): void { + this.addForm = this.fb.group({ + id: [ + this.currency.id, + ], + symbol: [ + this.currency.symbol, + ], + name: [ + this.currency.name, + ], + shortName: [ + this.currency.id, + ], + quantity: [ + this.currency.quantity || 1, + [Validators.required], + ], + rate: [ + this.currency.rate || 1, + [Validators.required], + ], + rateDate: [ + this.currency.rateDate, + [Validators.required], + ], + }); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + if (this.addForm.valid) { + this.errorMessage = undefined; + this.httpClient + .post(ApiRoutes.SettingsCurrencies, this.addForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/currency/currency.component.html b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.html new file mode 100644 index 0000000..f3c3b86 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.html @@ -0,0 +1,88 @@ +
+
+
+ + + +
+
+
+
+
+

My currencies

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
CodeNameShort NameQuantityRateRate Date
{{item.code}}{{item.name}}{{item.shortName}} + {{item.quantity}} + + {{item.rate | number: '1.2-6'}} + {{item.symbol}} + {{item.rateDate | date:'yyyy-MM-dd'}} + + +
+
+
+ +
+ + + + + + + + + + + + + + + + + +
{{item.symbol}}{{item.id}}{{item.name}} + +
+
+
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/currency/currency.component.scss b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.scss new file mode 100644 index 0000000..faf4a57 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.scss @@ -0,0 +1,15 @@ +// Primary currency highlight must target cells — Bootstrap paints td backgrounds, +// so a class on alone is invisible in light theme. +.table tbody tr.currency-row-primary > td, +.table tbody tr.currency-row-primary > th { + background-color: rgba(0, 188, 212, 0.18) !important; + color: inherit; +} + +:host-context(body.dark) { + .table tbody tr.currency-row-primary > td, + .table tbody tr.currency-row-primary > th { + background-color: rgba(33, 150, 243, 0.22) !important; + color: #cfe8ff; + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/currency/currency.component.ts b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.ts new file mode 100644 index 0000000..adab3f8 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/currency.component.ts @@ -0,0 +1,133 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; + +// libs +import { MatDialog } from '@angular/material/dialog'; +import Swal from 'sweetalert2'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { SettingsConnectCurrencyComponent } from './connect.currency.component'; +import { CurrencyModel } from '../../../model/currency.model'; +import { SettingsRateCurrencyComponent } from './rate.currency.component'; + +@Component({ + templateUrl: './currency.component.html', + styleUrls: ['./currency.component.scss'], +}) +export class SettingsCurrencyComponent { + public tabIndex = 0; + + public currencies?: CurrencyModel[]; + public myCurrencies?: CurrencyModel[]; + public primaryId?: string; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private dialogModel: MatDialog + ) { + } + + ngOnInit(): void { + this.load(); + } + + connect(currency: CurrencyModel) { + this.dialogModel.open(SettingsConnectCurrencyComponent, { + width: '640px', + disableClose: true, + data: currency, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.tabIndex = 0; + this.loadMyCurrency(); + } + }); + } + + disconnect(currency: CurrencyModel) { + } + + setRate(currency: CurrencyModel) { + this.dialogModel.open(SettingsRateCurrencyComponent, { + width: '640px', + disableClose: true, + data: currency, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.loadMyCurrency(); + } + }); + } + + delete(currency: CurrencyModel) { + Swal.fire({ + title: 'Delete currency ' + currency.name, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.delete(ApiRoutes.SettingsCurrency.replace(':id', currency.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + + } + + private load() { + this.httpClient + .get(ApiRoutes.GeneralCurrencies) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.currencies = data; + this.loadMyCurrency(); + }); + } + + private loadMyCurrency() { + this.httpClient + .get(ApiRoutes.SettingsCurrencies) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.myCurrencies = data + .sort((a, b) => a.code!.localeCompare(b.code!)) + .map(myCurrency => { + var globalCurrency = this.currencies?.find(x => x.id === myCurrency.code); + + return { + id: myCurrency.id, + code: myCurrency.code, + name: myCurrency.name, + quantity: myCurrency.quantity, + rate: myCurrency.rate, + rateDate: myCurrency.rateDate, + shortName: myCurrency.shortName, + symbol: globalCurrency?.symbol, + isPrimary: myCurrency.isPrimary, + }; + }); + }); + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html new file mode 100644 index 0000000..61c3640 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html @@ -0,0 +1,95 @@ +
+
+ + + + + + +
+
+
+ + Symbol + + +
+
+
+
+ + Code + + +
+
+
+
+ + Name + + +
+
+
+
+ + Short name + + +
+
+
+
+
+
+ + Rate + + + Please enter rate + + +
+
+
+
+ + Quantity + + + Please enter rate + + +
+
+
+
+ + Rate Date + + DD/MM/YYYY + + + + Please enter rate date + + +
+
+
+
+
+
+ + Primary currency + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.scss b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts new file mode 100644 index 0000000..88b4777 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts @@ -0,0 +1,99 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import { MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { CurrencyModel } from '../../../model/currency.model'; + +@Component({ + templateUrl: './rate.currency.component.html', + styleUrls: ['./rate.currency.component.scss'], +}) +export class SettingsRateCurrencyComponent { + public addForm!: FormGroup; + public error?: any; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public currency: CurrencyModel + ) { + } + + public ngOnInit(): void { + this.addForm = this.fb.group({ + id: [ + this.currency.id, + ], + code: [ + this.currency.code, + ], + symbol: [ + this.currency.symbol, + ], + name: [ + this.currency.name, + ], + shortName: [ + this.currency.shortName, + ], + quantity: [ + this.currency.quantity || 1, + [Validators.required], + ], + rate: [ + this.currency.rate || 1, + [Validators.required], + ], + rateDate: [ + this.currency.rateDate, + [Validators.required], + ], + isPrimary: [ + this.currency.isPrimary, + ], + }); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + this.error = undefined; + if (this.addForm.valid) { + this.httpClient + .put(ApiRoutes.SettingsCurrency.replace(':id', this.addForm.value.id), this.addForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (response) => { + this.httpClient + .post(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (response) => { + this.dialogRef.close({ refresh: true }); + }, + error: (error) => { + this.error = error; + }, + }); + }, + error: (error) => { + this.error = error; + } + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.html b/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.html new file mode 100644 index 0000000..dafe1c2 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.html @@ -0,0 +1,26 @@ +
+
+ + + + + + +
+
+
+ + Name + + +
+
+
+
+ Is internal motion +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.scss b/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.ts b/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.ts new file mode 100644 index 0000000..017b03e --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/edit.item.category.component.ts @@ -0,0 +1,77 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatDialogRef } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { ItemCategoryModel } from '../../../model/item.category.model'; + +@Component({ + templateUrl: './edit.item.category.component.html', + styleUrls: ['./edit.item.category.component.scss'], +}) +export class SettingsItemCategoryEditComponent { + public editForm!: FormGroup; + public errorMessage?: string; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public category: ItemCategoryModel + ) { + } + + ngOnInit(): void { + this.editForm = this.fb.group({ + id: [ + this.category.id, + ], + name: [ + this.category.name, + [Validators.required], + ], + internal: [ + this.category?.internal ?? false, + ], + }); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + if (this.editForm.valid) { + this.errorMessage = undefined; + if (!this.category.id) { + this.httpClient + .post(ApiRoutes.SettingsItemCategories, this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ category: response, refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } else { + this.httpClient + .put(ApiRoutes.SettingsItemCategory.replace(':id', this.category.id!), this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ category: response, refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.html b/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.html new file mode 100644 index 0000000..910111a --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.html @@ -0,0 +1,33 @@ +
+
+ + + + + + +
+
+
+ + Name + + +
+
+
+
+ + Category + + + {{category.name}} + + + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.scss b/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.ts b/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.ts new file mode 100644 index 0000000..42bbe1b --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/edit.item.component.ts @@ -0,0 +1,78 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatDialogRef } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { ItemCategoryModel } from '../../../model/item.category.model'; +import { ItemModel } from '../../../model/item.model'; +import { ItemService } from '../../../services/item.service'; + +@Component({ + templateUrl: './edit.item.component.html', + styleUrls: ['./edit.item.component.scss'], +}) +export class SettingsItemEditComponent { + public editForm!: FormGroup; + public errorMessage?: string; + public categories?: ItemCategoryModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private dialogRef: MatDialogRef, + private itemService: ItemService, + @Inject(MAT_DIALOG_DATA) public item: ItemModel + ) { + } + + ngOnInit(): void { + this.editForm = this.fb.group({ + id: [ + this.item.id, + ], + name: [ + this.item.name, + [Validators.required], + ], + category: [ + this.item.categoryId, + [Validators.required], + ], + }); + + this.itemService + .getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => this.categories = x); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onSubmitClick() { + console.log(this.item); + console.log(this.editForm.value); + if (this.editForm.valid) { + this.errorMessage = undefined; + this.httpClient + .put(ApiRoutes.SettingsItem.replace(':id', this.item.id!), this.editForm.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(response => { + this.dialogRef.close({ motion: response, refresh: true }); + }, error => { + this.errorMessage = error.detail; + }); + } + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.category.component.html b/MyOffice.SPA/src/app/pages/settings/item/item.category.component.html new file mode 100644 index 0000000..f56ed7c --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/item.category.component.html @@ -0,0 +1,49 @@ +
+
+
+ + + +
+
+
+ + + + + + + + +
+ +
+ +
+ + + + + + + + +
{{item.name}} + done + + + +
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.category.component.scss b/MyOffice.SPA/src/app/pages/settings/item/item.category.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.category.component.ts b/MyOffice.SPA/src/app/pages/settings/item/item.category.component.ts new file mode 100644 index 0000000..fce7de7 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/item.category.component.ts @@ -0,0 +1,97 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; + +// libs +import Swal from 'sweetalert2'; +import { MatDialog } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { ItemCategoryModel } from '../../../model/item.category.model'; +import { SettingsItemCategoryEditComponent } from './edit.item.category.component'; + +@Component({ + templateUrl: './item.category.component.html', + styleUrls: ['./item.category.component.scss'], +}) +export class SettingsItemCategoryComponent { + public categories?: ItemCategoryModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private httpClient: HttpClient, + private dialogModel: MatDialog, + ) { + } + + ngOnInit(): void { + this.load(); + } + + private load() { + this.httpClient + .get(ApiRoutes.SettingsItemCategories) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.categories = data; + }); + } + + public add() { + this.dialogModel.open(SettingsItemCategoryEditComponent, { + width: '640px', + disableClose: true, + data: { }, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.load(); + } + }); + } + + public edit(category: ItemCategoryModel) { + this.dialogModel.open(SettingsItemCategoryEditComponent, { + width: '640px', + disableClose: true, + data: category, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + this.load(); + } + }); + } + + public remove(category: ItemCategoryModel) { + Swal.fire({ + title: 'Delete item category ' + category.name, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.delete(ApiRoutes.SettingsItemCategory.replace(':id', category.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + } +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.html b/MyOffice.SPA/src/app/pages/settings/item/item.component.html new file mode 100644 index 0000000..c93753c --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.html @@ -0,0 +1,77 @@ +
+
+
+ + + +
+
+
+
+
+
+
    + + Move to category + + + {{category.name}} + + + +
+ +
    + + Code + + +
+
    + +
+
+
+
+
+
+ + + + + Motions + + + + +
+
+ +
+ + + + + + + +
+ + {{item.model.name}} + + + +
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.scss b/MyOffice.SPA/src/app/pages/settings/item/item.component.scss new file mode 100644 index 0000000..72edd68 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.scss @@ -0,0 +1,11 @@ +.card-title { + display: flex; +} + +.left-content { + flex: 1; +} + +.right-content { + margin-left: auto; +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.ts b/MyOffice.SPA/src/app/pages/settings/item/item.component.ts new file mode 100644 index 0000000..f58012f --- /dev/null +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.ts @@ -0,0 +1,130 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; + +// libs +import Swal from 'sweetalert2'; +import { MatDialog } from '@angular/material/dialog'; + +// app +import { ApiRoutes } from '../../../api-routes'; +import { SettingsItemEditComponent } from './edit.item.component'; +import { ItemService } from '../../../services/item.service'; +import { ISelectableModel } from '../../../core/models/selectable.model'; +import { ItemModel } from '../../../model/item.model'; +import { ItemCategoryModel } from '../../../model/item.category.model'; + +@Component({ + templateUrl: './item.component.html', + styleUrls: ['./item.component.scss'], +}) +export class SettingsItemComponent { + public items?: ISelectableModel[]; + public categories?: ItemCategoryModel[]; + public selectedCategory?: ItemCategoryModel; + public selectedCount: number = 0; + public category?: ItemCategoryModel; + public categoryAddShow: boolean = false; + public categoryAddName?: string; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private dialogModel: MatDialog, + private httpClient: HttpClient, + private itemService: ItemService, + ) { + } + + ngOnInit(): void { + this.load(); + } + + private load(categoryId?: string) { + this.itemService.getCategories() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.categories = data; + var selected = data[0]; + if (categoryId) { + var categories = this.categories.filter(x => x.id === categoryId); + selected = categories.length === 0 + ? selected + : categories[0]; + } + + this.selectCategory(selected); + }); + } + + public selectCategory(category: ItemCategoryModel) { + this.selectedCategory = category; + this.itemService.getItems(category.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.items = data.map>(x => { return { model: x, selected: false } }); + }); + } + + public edit(item: ItemModel) { + this.dialogModel.open(SettingsItemEditComponent, { + width: '640px', + disableClose: true, + data: item, + }).afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + if (x && x.refresh) { + console.log(x.motion.categoryId); + this.load(x.motion.categoryId); + } + }); + } + + public remove(category: ItemModel) { + Swal.fire({ + title: 'Delete item from ' + category.name, + showCancelButton: true, + confirmButtonText: 'Delete', + showLoaderOnConfirm: true, + preConfirm: (name) => { + return new Promise((resolve, reject) => { + this.httpClient.delete(ApiRoutes.SettingsItemCategory.replace(':id', category.id!)) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + resolve(data); + }, error => { + Swal.showValidationMessage(error.detail); + reject(); + }); + + }).catch(x => { + return false; + }); + }, + allowOutsideClick: () => !Swal.isLoading(), + }).then((result) => { + this.load(); + }); + } + + onCheckboxChange(item: ISelectableModel) { + this.selectedCount += item.selected ? 1 : -1; + } + + onCategoryChange(item: any) { + var data = { + category: item.value, + items: this.items!.filter(x => x.selected).map(x => x.model.id) + } + this.httpClient.post(ApiRoutes.SettingsItems, data) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.category = undefined; + this.selectedCount = 0; + this.load(); + }, error => { + }); + } +} diff --git a/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.html b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.html new file mode 100644 index 0000000..e418b7c --- /dev/null +++ b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.html @@ -0,0 +1,129 @@ +
+
+
+ + + +
+
+
+
+
+

User Profile

+
+
+
+
+
+ + Email + + email + +
+
+
+
+ + First name + + face + + Only characters or numbers allowed + + +
+
+ + Last name + + face + + Only characters or numbers allowed + + +
+
+
+
+ + Full name + + face + + Full name is empty + + +
+
+
+
+ + Currency + + + ({{item.symbol}}) {{item.name}} + + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+

+ Providers +

+
+
+
+
+ link + email + mark_email_read +
+
+ {{provider.name}} +
+
+ + + + + + +
+
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.scss b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.spec.ts b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.spec.ts new file mode 100644 index 0000000..bc71b08 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { UserProfileComponent } from './user-profile.component'; + +describe('UserProfileComponent', + () => { + let component: UserProfileComponent; + let fixture: ComponentFixture; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [UserProfileComponent], + }).compileComponents(); + }) + ); + beforeEach(() => { + fixture = TestBed.createComponent(UserProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.ts b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.ts new file mode 100644 index 0000000..3fb03f0 --- /dev/null +++ b/MyOffice.SPA/src/app/pages/user-profile/user-profile.component.ts @@ -0,0 +1,178 @@ +// angular +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// libs +import Swal from 'sweetalert2'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { ExternalLoginConfig } from '../../config.external-login'; +import { AuthService } from '../../core/service/auth.service'; + +@Component({ + selector: 'app-blank', + templateUrl: './user-profile.component.html', + styleUrls: ['./user-profile.component.scss'], +}) +export class UserProfileComponent { + form: FormGroup; + providers?: ProviderModel[]; + profile?: ProfileModel; + currencies?: CurrencyModel[]; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + private fb: FormBuilder, + private httpClient: HttpClient, + private authService: AuthService, + ) { + this.form = this.fb.group({ + firstName: ['', [Validators.pattern('[a-zA-Z0-9]+')]], + lastName: ['', [Validators.pattern('[a-zA-Z0-9]+')]], + fullName: ['', [Validators.pattern('[a-zA-Z0-9 ]+')]], + email: [{ value: '', disabled: true }, []], + currency: [{ value: '' }, []], + }); + + this.providers = ExternalLoginConfig + .getConfiguredProviders() + .map(x => { + provider: x.provider, + name: x.name, + isConnected: false, + isEmailConfirmed: false + }); + } + + ngOnInit(): void { + this.load(); + } + + private updateForm(data: ProfileModel) { + this.form.patchValue(data); + } + + private load() { + this.loadProfile(); + } + + private loadProfile() { + this.httpClient + .get(ApiRoutes.UserProfile) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.profile = data; + this.loadCurrencies(); + + this.updateForm(data); + + this.providers?.map((provider) => { + var attached = data.providers?.find(x => x.provider === provider.provider); + provider.isConnected = false; + provider.isEmailConfirmed = false; + if (attached) { + provider.isConnected = true; + provider.isEmailConfirmed = attached.isEmailConfirmed; + } + }); + }); + } + + private loadCurrencies() { + this.httpClient + .get(ApiRoutes.GeneralCurrencies) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.currencies = data; + }); + } + + onSubmit() { + if (this.form.invalid) { + return; + } + + this.httpClient.post(ApiRoutes.UserProfile, this.form.value) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(data => { + this.updateForm(data); + console.log(this.form); + }); + } + + connect(provider: ProviderModel) { + provider.error = undefined; + provider.isLoading = true; + + if (provider.provider === ExternalLoginConfig.GOOGLE) { + this.authService.attachGoogle() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(r => { + this.load(); + provider.isLoading = false; + }); + } + if (provider.provider === ExternalLoginConfig.AUTH0) { + this.authService.attachAuth0() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(resp => { + provider.error = ''; + if (!resp.success) { + provider.error = resp.data || 'Connect failed'; + } + this.load(); + provider.isLoading = false; + }); + } + } + + disconnect(provider: string) { + Swal.fire({ + title: 'Are you sure to disconnect ' + provider + ' ?', + text: 'Some providers can be connected only with login!', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Yes, disconnect it!', + }).then((result) => { + if (result.value) { + this.authService.deattach(provider) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(x => { + this.load(); + }); + } + }); + } +} + +interface ProviderModel { + provider: string; + name: string; + isConnected: boolean; + isEmailConfirmed: boolean; + error?: string; + isLoading: boolean; +} + +interface ProfileModel { + firstName?: string, + lastName?: string, + fullName?: string, + currency?: string, + isEmailConfirmed?: boolean, + providers?: ProviderModel[], +} + +interface CurrencyModel { + id?: string, + name?: string, + symbol?: string, + rate?: number, + rateDate?: string, +} diff --git a/MyOffice.SPA/src/app/services/account.category.service.ts b/MyOffice.SPA/src/app/services/account.category.service.ts new file mode 100644 index 0000000..26a3ea7 --- /dev/null +++ b/MyOffice.SPA/src/app/services/account.category.service.ts @@ -0,0 +1,29 @@ +// angular +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +// libs +import { Observable } from 'rxjs'; + +// app +import { ApiRoutes } from '../api-routes'; +import { AccountCategoryModel } from '../model/account.category.model'; + +@Injectable() +export class AccountCategoryService { + + constructor( + private httpClient: HttpClient, + ) { + } + + public getCategories(): Observable { + return this.httpClient + .get(ApiRoutes.SettingsAccountCategories); + } + + public getCategory(id: string): Observable { + return this.httpClient + .get(ApiRoutes.SettingsAccountCategory.replace(':id', id)); + } +} diff --git a/MyOffice.SPA/src/app/services/account.service.ts b/MyOffice.SPA/src/app/services/account.service.ts new file mode 100644 index 0000000..06d4216 --- /dev/null +++ b/MyOffice.SPA/src/app/services/account.service.ts @@ -0,0 +1,28 @@ +// angular +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +// libs +import { Observable } from 'rxjs'; + +// app +import { ApiRoutes } from '../api-routes'; +import { AccountCategoryModel } from '../model/account.category.model'; + +@Injectable() +export class AccountService { + private types = new Map(); + + constructor( + private httpClient: HttpClient, + ) { + this.types.set('balance', 'Account with balance'); + this.types.set('credit', 'Account with loan funds'); + this.types.set('external', 'External account'); + this.types.set('other', 'Other account'); + } + + public getTypes(): Map { + return this.types; + } +} diff --git a/MyOffice.SPA/src/app/services/currency.service.ts b/MyOffice.SPA/src/app/services/currency.service.ts new file mode 100644 index 0000000..9733638 --- /dev/null +++ b/MyOffice.SPA/src/app/services/currency.service.ts @@ -0,0 +1,24 @@ +// angular +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +// libs +import { Observable } from 'rxjs'; + +// app +import { ApiRoutes } from '../api-routes'; +import { CurrencyModel } from '../model/currency.model'; + +@Injectable() +export class CurrencyService { + + constructor( + private httpClient: HttpClient, + ) { + } + + public getCurrencies(): Observable { + return this.httpClient + .get(ApiRoutes.SettingsCurrencies); + } +} diff --git a/MyOffice.SPA/src/app/services/item.service.ts b/MyOffice.SPA/src/app/services/item.service.ts new file mode 100644 index 0000000..32f804f --- /dev/null +++ b/MyOffice.SPA/src/app/services/item.service.ts @@ -0,0 +1,34 @@ +// angular +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +// libs +import { Observable } from 'rxjs'; + +// app +import { ApiRoutes } from '../api-routes'; +import { ItemModel } from '../model/item.model'; +import { ItemCategoryModel } from '../model/item.category.model'; + +@Injectable() +export class ItemService { + + constructor( + private httpClient: HttpClient, + ) { + } + + public getCategories(category?: string): Observable { + return this.httpClient + .get(ApiRoutes.SettingsItemCategories); + } + + public getItems(categoryId?: string): Observable { + var url = categoryId + ? ApiRoutes.SettingsItems + "?category=" + categoryId + : ApiRoutes.SettingsItems; + + return this.httpClient + .get(url); + } +} diff --git a/MyOffice.SPA/src/app/shared/TableElement.ts b/MyOffice.SPA/src/app/shared/TableElement.ts new file mode 100644 index 0000000..ad7a93a --- /dev/null +++ b/MyOffice.SPA/src/app/shared/TableElement.ts @@ -0,0 +1,3 @@ +export interface TableElement { + [key: string]: string | number; +} diff --git a/MyOffice.SPA/src/app/shared/UnsubscribeOnDestroyAdapter.ts b/MyOffice.SPA/src/app/shared/UnsubscribeOnDestroyAdapter.ts new file mode 100644 index 0000000..8fcd939 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/UnsubscribeOnDestroyAdapter.ts @@ -0,0 +1,20 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { SubSink } from './sub-sink'; + +/** + * A class that automatically unsubscribes all observables when the object gets destroyed + */ +@Injectable() +export class UnsubscribeOnDestroyAdapter implements OnDestroy { + /** + * The subscription sink object that stores all subscriptions + */ + subs = new SubSink(); + + /** + * The lifecycle hook that unsubscribes all subscriptions when the component / object gets destroyed + */ + ngOnDestroy(): void { + this.subs.unsubscribe(); + } +} diff --git a/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.html b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000..805084e --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.html @@ -0,0 +1,20 @@ + diff --git a/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.scss b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.spec.ts b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.spec.ts new file mode 100644 index 0000000..e8f9133 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BreadcrumbComponent } from './breadcrumb.component'; + +describe('BreadcrumbComponent', + () => { + let component: BreadcrumbComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BreadcrumbComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BreadcrumbComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.ts b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..5e67931 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-breadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.scss'], +}) +export class BreadcrumbComponent { + @Input() + title!: string; + @Input() + items!: string[]; + @Input() + active_item!: string; + + constructor() { + //constructor + } +} diff --git a/MyOffice.SPA/src/app/shared/components/components.module.ts b/MyOffice.SPA/src/app/shared/components/components.module.ts new file mode 100644 index 0000000..69a47bc --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/components.module.ts @@ -0,0 +1,33 @@ +// angular +import { NgModule } from '@angular/core'; + +// libs +import { TranslateModule } from '@ngx-translate/core'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FileUploadComponent } from './file-upload/file-upload.component'; +import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component'; +import { SharedModule } from '../shared.module'; +import { DatePeriodComponent } from './date-period/date-period.component'; +import { DialogChromeComponent } from './dialog-chrome/dialog-chrome.component'; + +@NgModule({ + declarations: [ + FileUploadComponent, + BreadcrumbComponent, + DatePeriodComponent, + DialogChromeComponent, + ], + imports: [ + SharedModule, + TranslateModule, + MatDialogModule, + ], + exports: [ + FileUploadComponent, + BreadcrumbComponent, + DatePeriodComponent, + DialogChromeComponent, + ], +}) +export class ComponentsModule { +} diff --git a/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.css b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.css new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.html b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.html new file mode 100644 index 0000000..d4a9468 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.html @@ -0,0 +1,28 @@ +
+
+
+ + From + + YYYY/MM/DD + + + + Please enter rate date + + +
+
+ + To + + YYYY/MM/DD + + + + Please enter rate date + + +
+
+
diff --git a/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.spec.ts b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.spec.ts new file mode 100644 index 0000000..81cf7ca --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatePeriodComponent } from './date-period.component'; + +describe('DatePeriodComponent', () => { + let component: DatePeriodComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DatePeriodComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DatePeriodComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.ts b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.ts new file mode 100644 index 0000000..0411193 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/date-period/date-period.component.ts @@ -0,0 +1,56 @@ +// app +import { Component } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Input } from '@angular/core'; +import { Output } from '@angular/core'; +import { EventEmitter } from '@angular/core'; + +// libs +import moment from 'moment'; + + +type DatePeriodForm = { + dateFrom: FormControl; + dateTo: FormControl; +}; + +@Component({ + selector: 'app-date-period', + templateUrl: './date-period.component.html', + styleUrls: ['./date-period.component.css'] +}) +export class DatePeriodComponent { + + @Input('from') dateFrom!: Date; + @Input('to') dateTo!: Date; + + @Output() onChange: EventEmitter = new EventEmitter(); + + form!: FormGroup; + + constructor( + private fb: FormBuilder + ) { + + } + + ngOnInit() { + this.form = this.fb.group({ + dateFrom: [ + this.dateFrom, + [Validators.required], + ], + dateTo: [ + this.dateTo, + [Validators.required], + ], + }); + } + + onSubmitClick() { + } + + onInputChange() { + this.onChange.emit([this.form.value.dateFrom!, this.form.value.dateTo!]); + } +} diff --git a/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.html b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.html new file mode 100644 index 0000000..a34b6d6 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.html @@ -0,0 +1,39 @@ +
+

{{ title }}

+
+ +
+
+ +
+ {{ errorMessage }} + +

{{ error.title }}

+
    +
  • {{ item.value }}
  • +
+
+
+ +
+ + + +
+
+ {{ errorMessage }} + +

{{ error.title }}

+
    +
  • {{ item.value }}
  • +
+
+
+
+
+
+ +
+
+
+
diff --git a/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.scss b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.scss new file mode 100644 index 0000000..19ab249 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.scss @@ -0,0 +1,24 @@ +.dialog-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.dialog-header [mat-dialog-title] { + flex: 1; + min-width: 0; +} + +.dialog-header-actions { + display: flex; + align-items: center; + flex-shrink: 0; + margin-top: 24px; + margin-right: 24px; + gap: 8px; +} + +.dialog-header-error { + margin: 0 24px 8px; +} diff --git a/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.ts b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.ts new file mode 100644 index 0000000..fb75106 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/dialog-chrome/dialog-chrome.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, TemplateRef } from '@angular/core'; +import { UI_CONFIG } from '../../../config/ui.config'; + +@Component({ + selector: 'app-dialog-chrome', + templateUrl: './dialog-chrome.component.html', + styleUrls: ['./dialog-chrome.component.scss'], +}) +export class DialogChromeComponent { + @Input({ required: true }) title!: string; + @Input({ required: true }) actions!: TemplateRef; + @Input() errorMessage?: string | null; + /** Structured validation error (e.g. currency rate dialog). */ + @Input() error?: { title?: string; errors?: Record } | null; + + readonly modalButtonsTop = UI_CONFIG.modal_buttons === 'top'; + + get hasError(): boolean { + return !!(this.errorMessage || this.error?.title || this.error?.errors); + } +} diff --git a/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.html b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.html new file mode 100644 index 0000000..1e97cd1 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.html @@ -0,0 +1 @@ + diff --git a/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.scss b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.spec.ts b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.spec.ts new file mode 100644 index 0000000..c1e8bbf --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FeatherIconsComponent } from './feather-icons.component'; + +describe('FeatherIconsComponent', + () => { + let component: FeatherIconsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FeatherIconsComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FeatherIconsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.ts b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.ts new file mode 100644 index 0000000..5f97c19 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-feather-icons', + templateUrl: './feather-icons.component.html', + styleUrls: ['./feather-icons.component.scss'], +}) +export class FeatherIconsComponent { + @Input() + icon?: string; + @Input() + class?: string; + + constructor() { + // constructor + } +} diff --git a/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.module.ts b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.module.ts new file mode 100644 index 0000000..0b3bbb2 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/feather-icons/feather-icons.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FeatherIconsComponent } from './feather-icons.component'; + +import { FeatherModule } from 'angular-feather'; +import { allIcons } from 'angular-feather/icons'; + +@NgModule({ + imports: [CommonModule, FeatherModule.pick(allIcons)], + exports: [FeatherIconsComponent, FeatherModule], + declarations: [FeatherIconsComponent], +}) +export class FeatherIconsModule { +} diff --git a/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.html b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.html new file mode 100644 index 0000000..8fb6747 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.html @@ -0,0 +1,5 @@ +
+ + {{file ? file.name : ' or drag and drop file here' }} + +
diff --git a/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.scss b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.scss new file mode 100644 index 0000000..d3cb0d4 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.scss @@ -0,0 +1,27 @@ +.file-drop-area { + border: 1px dashed #7c7db3; + border-radius: 3px; + position: relative; + max-width: 100%; + margin-top: 5px; + padding: 26px 20px 30px; + transition: 0.2s; +} + +.file-input { + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + cursor: pointer; + opacity: 0; +} + +.file-msg { + display: inline-block; + margin-left: 5px; + font-size: 12px; + font-weight: 500; + color: #5b5bff; +} diff --git a/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.spec.ts b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.spec.ts new file mode 100644 index 0000000..622f59d --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileUploadComponent } from './file-upload.component'; + +describe('FileUploadComponent', + () => { + let component: FileUploadComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [FileUploadComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FileUploadComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.ts b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.ts new file mode 100644 index 0000000..9dfd503 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/components/file-upload/file-upload.component.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/ban-types */ +import { Component, ElementRef, HostListener, Input } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'app-file-upload', + templateUrl: './file-upload.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: FileUploadComponent, + multi: true, + }, + ], + styleUrls: ['./file-upload.component.scss'], +}) +export class FileUploadComponent implements ControlValueAccessor { + onChange!: Function; + file: File | null = null; + + @HostListener('change', ['$event.target.files']) + emitFiles(event: FileList) { + const file = event && event.item(0); + this.onChange(file); + this.file = file; + } + + constructor(private host: ElementRef) {} + + writeValue(value: null) { + // clear file input + this.host.nativeElement.value = ''; + this.file = null; + } + + registerOnChange(fn: Function) { + this.onChange = fn; + } + + registerOnTouched(fn: Function) { + // add code here + } +} diff --git a/MyOffice.SPA/src/app/shared/feather-icons.module.ts b/MyOffice.SPA/src/app/shared/feather-icons.module.ts new file mode 100644 index 0000000..d3e4d0c --- /dev/null +++ b/MyOffice.SPA/src/app/shared/feather-icons.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; + +import { FeatherModule } from 'angular-feather'; +import { allIcons } from 'angular-feather/icons'; + +// Select some icons (use an object, not an array) + +@NgModule({ + imports: [FeatherModule.pick(allIcons)], + exports: [FeatherModule], +}) +export class FeatherIconsModule { +} diff --git a/MyOffice.SPA/src/app/shared/material.module.ts b/MyOffice.SPA/src/app/shared/material.module.ts new file mode 100644 index 0000000..dd3372f --- /dev/null +++ b/MyOffice.SPA/src/app/shared/material.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatNativeDateModule } from '@angular/material/core'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatListModule } from '@angular/material/list'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; + +const materialModules = [ + MatButtonModule, + MatInputModule, + MatListModule, + MatIconModule, + MatTooltipModule, + MatDatepickerModule, + MatNativeDateModule, + NgxMaskDirective, + NgxMaskPipe, + MatButtonToggleModule, + MatFormFieldModule, + MatMenuModule, + MatSlideToggleModule, +]; + +@NgModule({ + declarations: [], + imports: [...materialModules], + exports: [...materialModules], + providers: [provideNgxMask()], +}) +export class MaterialModule { +} diff --git a/MyOffice.SPA/src/app/shared/shared.module.ts b/MyOffice.SPA/src/app/shared/shared.module.ts new file mode 100644 index 0000000..4f1da2e --- /dev/null +++ b/MyOffice.SPA/src/app/shared/shared.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { MaterialModule } from './material.module'; +import { FeatherIconsModule } from './components/feather-icons/feather-icons.module'; + +@NgModule({ + declarations: [], + imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule], + exports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule, + MaterialModule, + FeatherIconsModule, + ], +}) +export class SharedModule { +} diff --git a/MyOffice.SPA/src/app/shared/sub-sink.ts b/MyOffice.SPA/src/app/shared/sub-sink.ts new file mode 100644 index 0000000..afb3d23 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/sub-sink.ts @@ -0,0 +1,61 @@ +import { SubscriptionLike } from 'rxjs'; + +// TODO: SubSink -> SubSync +/** + * Subscription sink that holds Observable subscriptions + * until you call unsubscribe on it in ngOnDestroy. + */ +export class SubSink { + protected _subs: SubscriptionLike[] = []; + + /** + * Subscription sink that holds Observable subscriptions + * until you call unsubscribe on it in ngOnDestroy. + * + * @example + * In Angular: + * ``` + * private subs = new SubSink(); + * ... + * this.subs.sink = observable$.subscribe( + * this.subs.add(observable$.subscribe(...)); + * ... + * ngOnDestroy() { + * this.subs.unsubscribe(); + * } + * ``` + */ + constructor() { + // constructor + } + + /** + * Add subscriptions to the tracked subscriptions + * @example + * this.subs.add(observable$.subscribe(...)); + */ + add(...subscriptions: SubscriptionLike[]) { + this._subs = this._subs.concat(subscriptions); + } + + /** + * Assign subscription to this sink to add it to the tracked subscriptions + * @example + * this.subs.sink = observable$.subscribe(...); + */ + set sink(subscription: SubscriptionLike) { + this._subs.push(subscription); + } + + /** + * Unsubscribe to all subscriptions in ngOnDestroy() + * @example + * ngOnDestroy() { + * this.subs.unsubscribe(); + * } + */ + unsubscribe() { + this._subs.forEach((sub) => sub && sub.unsubscribe()); + this._subs = []; + } +} diff --git a/MyOffice.SPA/src/app/shared/tableExportUtil.ts b/MyOffice.SPA/src/app/shared/tableExportUtil.ts new file mode 100644 index 0000000..e22b342 --- /dev/null +++ b/MyOffice.SPA/src/app/shared/tableExportUtil.ts @@ -0,0 +1,42 @@ +import * as XLSX from 'xlsx'; +import { TableElement } from './TableElement'; + +const getFileName = (name: string) => { + const timeSpan = new Date().toISOString(); + const sheetName = name || 'ExportResult'; + const fileName = `${sheetName}-${timeSpan}`; + return { + sheetName, + fileName, + }; +}; + +export class TableExportUtil { + static exportToExcel(arr: Partial[], name: string) { + const { sheetName, fileName } = getFileName(name); + + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(arr); + XLSX.utils.book_append_sheet(wb, ws, sheetName); + XLSX.writeFile(wb, `${fileName}.xlsx`); + } + + // static exportToPDF(exportData: any[]) { + // const doc = new jsPDF(); + // const dataValue: any = Object.keys(exportData).map(function ( + // personNamedIndex: any + // ) { + // return Object.values(exportData[personNamedIndex]); + // }); + // const keys: any = Object.keys(exportData[0]); + + // autoTable(doc, { + // head: [keys], + // body: dataValue, + // }); + + // const { fileName } = getFileName('pdf'); + + // doc.save(`${fileName}.pdf`); + // } +} diff --git a/MyOffice.SPA/src/assets/.gitkeep b/MyOffice.SPA/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.eot b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.eot new file mode 100644 index 0000000..f5c1fd4 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.eot differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.svg b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.svg new file mode 100644 index 0000000..e29de15 --- /dev/null +++ b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.svg @@ -0,0 +1,1181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.ttf b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.ttf new file mode 100644 index 0000000..218ff47 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.ttf differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff new file mode 100644 index 0000000..d24c3a9 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff2 b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff2 new file mode 100644 index 0000000..372c314 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-brands-400.woff2 differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.eot b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.eot new file mode 100644 index 0000000..5999593 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.eot differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.svg b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.svg new file mode 100644 index 0000000..0085843 --- /dev/null +++ b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.ttf b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.ttf new file mode 100644 index 0000000..4adbd66 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.ttf differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff new file mode 100644 index 0000000..94d86d1 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff2 b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff2 new file mode 100644 index 0000000..cff972c Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-regular-400.woff2 differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.eot b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.eot new file mode 100644 index 0000000..9def162 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.eot differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.svg b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.svg new file mode 100644 index 0000000..a229433 --- /dev/null +++ b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.svg @@ -0,0 +1,2567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.ttf b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.ttf new file mode 100644 index 0000000..c681849 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.ttf differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff new file mode 100644 index 0000000..891a580 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff differ diff --git a/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff2 b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff2 new file mode 100644 index 0000000..00d15c4 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/fontawesome/fa-solid-900.woff2 differ diff --git a/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.eot b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.eot new file mode 100644 index 0000000..ff32634 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.eot differ diff --git a/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.svg b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.svg new file mode 100644 index 0000000..c3d485c --- /dev/null +++ b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.svg @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.ttf b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.ttf new file mode 100644 index 0000000..5f2caab Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.ttf differ diff --git a/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff new file mode 100644 index 0000000..cfb9c3d Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff differ diff --git a/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff2 b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff2 new file mode 100644 index 0000000..19f70c6 Binary files /dev/null and b/MyOffice.SPA/src/assets/fonts/poppins/poppins-v5-latin-regular.woff2 differ diff --git a/MyOffice.SPA/src/assets/i18n/de.json b/MyOffice.SPA/src/assets/i18n/de.json new file mode 100644 index 0000000..6b93e47 --- /dev/null +++ b/MyOffice.SPA/src/assets/i18n/de.json @@ -0,0 +1,82 @@ +{ + "HEADER": { + "SEARCH": { + "TEXT": "Suche.." + } + }, + "MENUITEMS": { + "USER": { + "POST": "Manager" + }, + "MAIN": { + "TEXT": "Main" + }, + "DASHBOARD": { + "TEXT": "Zuhause", + "LIST": { + "DASHBOARD": "Dashboard" + } + }, + "ADVANCE-TABLE": { + "TEXT": "Vorab-Tabelle" + }, + "APPS": { + "TEXT": "Apps" + }, + "CALENDAR": { + "TEXT": "Kalender" + }, + "TASK": { + "TEXT": "Aufgabe" + }, + "CONTACTS": { + "TEXT": "Kontakte" + }, + "EMAIL": { + "TEXT": "Email", + "LIST": { + "INBOX": "Posteingang", + "COMPOSE": "Komponieren", + "READ": "E-Mail lesen" + } + }, + "MORE-APPS": { + "TEXT": "Mehr Apps", + "LIST": { + "CHAT": "Plaudern", + "SUPPORT": "Unterstützung", + "DRAG-DROP": "Ziehen und loslassen", + "CONTACT-GRID": "Kontakt Grid" + } + }, + "COMPONENTS": { + "TEXT": "Komponenten" + }, + "WIDGETS": { + "TEXT": "Widgets", + "LIST": { + "CHART-WIDGET": "Diagramm-Widget", + "DATA-WIDGET": "Daten-Widget" + } + }, + "FORMS": { + "TEXT": "Formen", + "LIST": { + "CONTROLS": "Form Controls", + "ADVANCE": "Advance Control", + "EXAMPLE": "Vorabkontrolle", + "VALIDATION": "Formularvalidierung", + "WIZARD": "Magier", + "EDITORS": "Redakteurinnen" + } + }, + "TABLES": { + "TEXT": "Tabellen", + "LIST": { + "BASIC": "Tablas básicas", + "MATERIAL": "Materialtabellen", + "NGX-DATATABLE": "NGX-datierbar" + } + } + } +} diff --git a/MyOffice.SPA/src/assets/i18n/en.json b/MyOffice.SPA/src/assets/i18n/en.json new file mode 100644 index 0000000..2f87228 --- /dev/null +++ b/MyOffice.SPA/src/assets/i18n/en.json @@ -0,0 +1,106 @@ +{ + "HEADER": { + "SEARCH": { + "TEXT": "Search.." + } + }, + "MENUITEMS": { + "USER": { + "POST": "Manager" + }, + "MAIN": { + "TEXT": "Main" + }, + "DASHBOARD": { + "TEXT": "Dashboard", + "LIST": { + "DASHBOARD": "Rests", + "INCOME": "Incomes", + "OUTCOME": "Outcomes" + } + }, + "ACCOUNTS": { + "TEXT": "Accounts" + }, + "SETTINGS": { + "TEXT": "Settings", + "LIST": { + "CURRENCIES": "Currencies", + "ACCOUNTCATEGORIES": "Account categories", + "ACCOUNTS": "Accounts", + "ITEMCATEGORIES": "Item categories", + "ITEMS": "Items" + } + }, + + "ADVANCE-TABLE": { + "TEXT": "Advance Table" + }, + "APPS": { + "TEXT": "Apps" + }, + "CALENDAR": { + "TEXT": "Calendar" + }, + "TASK": { + "TEXT": "Task" + }, + "CONTACTS": { + "TEXT": "Contacts" + }, + "EMAIL": { + "TEXT": "Email", + "LIST": { + "INBOX": "Inbox", + "COMPOSE": "Compose", + "READ": "Read Email" + } + }, + "MORE-APPS": { + "TEXT": "More Apps", + "LIST": { + "CHAT": "Chat", + "SUPPORT": "Support", + "DRAG-DROP": "Drag & Drop", + "CONTACT-GRID": "Contact Grid" + } + }, + "COMPONENTS": { + "TEXT": "Components" + }, + "WIDGETS": { + "TEXT": "Widgets", + "LIST": { + "CHART-WIDGET": "Chart-Widget", + "DATA-WIDGET": "Data-Widget" + } + }, + "FORMS": { + "TEXT": "Forms", + "LIST": { + "CONTROLS": "Form Controls", + "ADVANCE": "Advance Control", + "EXAMPLE": "Form Examples", + "VALIDATION": "Form Validation", + "WIZARD": "Wizard", + "EDITORS": "Editors" + } + }, + "TABLES": { + "TEXT": "Tables", + "LIST": { + "BASIC": "Basic Tables", + "MATERIAL": "Material Tables", + "NGX-DATATABLE": "NGX-Datatable" + } + } + }, + + "BALANCE": "Balance", + "DEBIT.BALANCE": "Debit balance", + "CREDIT.BALANCE": "Credit balance", + "CURRENT.BALANCE": "Current balance", + "HOME": "Home", + "OTHER": "Other", + "TOTAL": "Total" +} diff --git a/MyOffice.SPA/src/assets/i18n/es.json b/MyOffice.SPA/src/assets/i18n/es.json new file mode 100644 index 0000000..58966ef --- /dev/null +++ b/MyOffice.SPA/src/assets/i18n/es.json @@ -0,0 +1,82 @@ +{ + "HEADER": { + "SEARCH": { + "TEXT": "Buscar.." + } + }, + "MENUITEMS": { + "USER": { + "POST": "Gerente" + }, + "MAIN": { + "TEXT": "Principal" + }, + "DASHBOARD": { + "TEXT": "Casa", + "LIST": { + "DASHBOARD": "Tablero" + } + }, + "ADVANCE-TABLE": { + "TEXT": "Tabla de avance" + }, + "APPS": { + "TEXT": "Aplicaciones" + }, + "CALENDAR": { + "TEXT": "Calendario" + }, + "TASK": { + "TEXT": "Tarea" + }, + "CONTACTS": { + "TEXT": "Contactos" + }, + "EMAIL": { + "TEXT": "Email", + "LIST": { + "INBOX": "Bandeja de entrada", + "COMPOSE": "Componer", + "READ": "Leer el correo electrónico" + } + }, + "MORE-APPS": { + "TEXT": "Más aplicaciones", + "LIST": { + "CHAT": "Charla", + "SUPPORT": "Apoyo", + "DRAG-DROP": "Arrastrar y soltar", + "CONTACT-GRID": "Cuadrícula de contacto" + } + }, + "COMPONENTS": { + "TEXT": "Componentes" + }, + "WIDGETS": { + "TEXT": "Widgets", + "LIST": { + "CHART-WIDGET": "Widget de gráfico", + "DATA-WIDGET": "Widget de datos" + } + }, + "FORMS": { + "TEXT": "Formularios", + "LIST": { + "CONTROLS": "Controles de formulario", + "ADVANCE": "Control avanzado", + "EXAMPLE": "Ejemplos de formularios", + "VALIDATION": "Validación de formulario", + "WIZARD": "Mago", + "EDITORS": "Editoras" + } + }, + "TABLES": { + "TEXT": "Tabellen", + "LIST": { + "BASIC": "Tablas básicas", + "MATERIAL": "Tablas de materiales", + "NGX-DATATABLE": "NGX-Datatable" + } + } + } +} diff --git a/MyOffice.SPA/src/assets/i18n/ua.json b/MyOffice.SPA/src/assets/i18n/ua.json new file mode 100644 index 0000000..38167be --- /dev/null +++ b/MyOffice.SPA/src/assets/i18n/ua.json @@ -0,0 +1,108 @@ +{ + "HEADER": { + "SEARCH": { + "TEXT": "Шукати.." + } + }, + "MENUITEMS": { + "USER": { + "POST": "Manager" + }, + "MAIN": { + "TEXT": "Головне" + }, + "DASHBOARD": { + "TEXT": "Панелі", + "LIST": { + "DASHBOARD": "Залишки", + "INCOME": "Надходження", + "OUTCOME": "Витрати" + } + }, + "ACCOUNTS": { + "TEXT": "Рахунки" + }, + "SETTINGS": { + "TEXT": "Налаштування", + "LIST": { + "CURRENCIES": "Валюти", + "ACCOUNTCATEGORIES": "Категорії рахунків", + "ACCOUNTS": "Рахунки", + "ITEMCATEGORIES": "Категорії статей", + "ITEMS": "Статті" + } + }, + + "ADVANCE-TABLE": { + "TEXT": "Advance Table" + }, + "APPS": { + "TEXT": "Apps" + }, + "CALENDAR": { + "TEXT": "Calendar" + }, + "TASK": { + "TEXT": "Task" + }, + "CONTACTS": { + "TEXT": "Contacts" + }, + "EMAIL": { + "TEXT": "Email", + "LIST": { + "INBOX": "Inbox", + "COMPOSE": "Compose", + "READ": "Read Email" + } + }, + "MORE-APPS": { + "TEXT": "More Apps", + "LIST": { + "CHAT": "Chat", + "SUPPORT": "Support", + "DRAG-DROP": "Drag & Drop", + "CONTACT-GRID": "Contact Grid" + } + }, + "COMPONENTS": { + "TEXT": "Components" + }, + "WIDGETS": { + "TEXT": "Widgets", + "LIST": { + "CHART-WIDGET": "Chart-Widget", + "DATA-WIDGET": "Data-Widget" + } + }, + "FORMS": { + "TEXT": "Forms", + "LIST": { + "CONTROLS": "Form Controls", + "ADVANCE": "Advance Control", + "EXAMPLE": "Form Examples", + "VALIDATION": "Form Validation", + "WIZARD": "Wizard", + "EDITORS": "Editors" + } + }, + "TABLES": { + "TEXT": "Tables", + "LIST": { + "BASIC": "Basic Tables", + "MATERIAL": "Material Tables", + "NGX-DATATABLE": "NGX-Datatable" + } + } + }, + "BALANCE": "Баланс", + "DEBIT.BALANCE": "Остаток", + "CREDIT.BALANCE": "Кредиты", + "CURRENT.BALANCE": "Поточний баланс", + "HOME": "Головна", + "OTHER": "Інші", + "TOTAL": "Всього", + "INCOME": "Надходження", + "OUTCOME": "Витрати", + "PERIOD": "Період" +} diff --git a/MyOffice.SPA/src/assets/images/apps/calendar.png b/MyOffice.SPA/src/assets/images/apps/calendar.png new file mode 100644 index 0000000..2778d98 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/calendar.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/chat.png b/MyOffice.SPA/src/assets/images/apps/chat.png new file mode 100644 index 0000000..a67f315 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/chat.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/contact.png b/MyOffice.SPA/src/assets/images/apps/contact.png new file mode 100644 index 0000000..df66232 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/contact.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/gallery.png b/MyOffice.SPA/src/assets/images/apps/gallery.png new file mode 100644 index 0000000..7845170 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/gallery.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/mail.png b/MyOffice.SPA/src/assets/images/apps/mail.png new file mode 100644 index 0000000..dcdc777 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/mail.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/support.png b/MyOffice.SPA/src/assets/images/apps/support.png new file mode 100644 index 0000000..c5f5b76 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/support.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/table.png b/MyOffice.SPA/src/assets/images/apps/table.png new file mode 100644 index 0000000..b1d2feb Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/table.png differ diff --git a/MyOffice.SPA/src/assets/images/apps/task.png b/MyOffice.SPA/src/assets/images/apps/task.png new file mode 100644 index 0000000..bcbe59f Binary files /dev/null and b/MyOffice.SPA/src/assets/images/apps/task.png differ diff --git a/MyOffice.SPA/src/assets/images/banner/1.png b/MyOffice.SPA/src/assets/images/banner/1.png new file mode 100644 index 0000000..d90352c Binary files /dev/null and b/MyOffice.SPA/src/assets/images/banner/1.png differ diff --git a/MyOffice.SPA/src/assets/images/banner/2.png b/MyOffice.SPA/src/assets/images/banner/2.png new file mode 100644 index 0000000..d90352c Binary files /dev/null and b/MyOffice.SPA/src/assets/images/banner/2.png differ diff --git a/MyOffice.SPA/src/assets/images/banner/3.png b/MyOffice.SPA/src/assets/images/banner/3.png new file mode 100644 index 0000000..d90352c Binary files /dev/null and b/MyOffice.SPA/src/assets/images/banner/3.png differ diff --git a/MyOffice.SPA/src/assets/images/banner/4.png b/MyOffice.SPA/src/assets/images/banner/4.png new file mode 100644 index 0000000..d90352c Binary files /dev/null and b/MyOffice.SPA/src/assets/images/banner/4.png differ diff --git a/MyOffice.SPA/src/assets/images/dark.png b/MyOffice.SPA/src/assets/images/dark.png new file mode 100644 index 0000000..9698d4f Binary files /dev/null and b/MyOffice.SPA/src/assets/images/dark.png differ diff --git a/MyOffice.SPA/src/assets/images/details_close.png b/MyOffice.SPA/src/assets/images/details_close.png new file mode 100644 index 0000000..9c7d698 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/details_close.png differ diff --git a/MyOffice.SPA/src/assets/images/details_open.png b/MyOffice.SPA/src/assets/images/details_open.png new file mode 100644 index 0000000..c0edf44 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/details_open.png differ diff --git a/MyOffice.SPA/src/assets/images/favicon.ico b/MyOffice.SPA/src/assets/images/favicon.ico new file mode 100644 index 0000000..9ba9214 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/favicon.ico differ diff --git a/MyOffice.SPA/src/assets/images/flags/germany.jpg b/MyOffice.SPA/src/assets/images/flags/germany.jpg new file mode 100644 index 0000000..46147e4 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/flags/germany.jpg differ diff --git a/MyOffice.SPA/src/assets/images/flags/spain.jpg b/MyOffice.SPA/src/assets/images/flags/spain.jpg new file mode 100644 index 0000000..f81c83a Binary files /dev/null and b/MyOffice.SPA/src/assets/images/flags/spain.jpg differ diff --git a/MyOffice.SPA/src/assets/images/flags/ukraine.png b/MyOffice.SPA/src/assets/images/flags/ukraine.png new file mode 100644 index 0000000..3479ad3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/flags/ukraine.png differ diff --git a/MyOffice.SPA/src/assets/images/flags/us.jpg b/MyOffice.SPA/src/assets/images/flags/us.jpg new file mode 100644 index 0000000..851cd7b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/flags/us.jpg differ diff --git a/MyOffice.SPA/src/assets/images/icons/xlsx.png b/MyOffice.SPA/src/assets/images/icons/xlsx.png new file mode 100644 index 0000000..2362390 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/icons/xlsx.png differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/1.jpg b/MyOffice.SPA/src/assets/images/image-gallery/1.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/1.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/10.jpg b/MyOffice.SPA/src/assets/images/image-gallery/10.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/10.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/11.jpg b/MyOffice.SPA/src/assets/images/image-gallery/11.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/11.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/12.jpg b/MyOffice.SPA/src/assets/images/image-gallery/12.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/12.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/2.jpg b/MyOffice.SPA/src/assets/images/image-gallery/2.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/2.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/3.jpg b/MyOffice.SPA/src/assets/images/image-gallery/3.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/3.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/4.jpg b/MyOffice.SPA/src/assets/images/image-gallery/4.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/4.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/5.jpg b/MyOffice.SPA/src/assets/images/image-gallery/5.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/5.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/6.jpg b/MyOffice.SPA/src/assets/images/image-gallery/6.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/6.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/7.jpg b/MyOffice.SPA/src/assets/images/image-gallery/7.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/7.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/8.jpg b/MyOffice.SPA/src/assets/images/image-gallery/8.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/8.jpg differ diff --git a/MyOffice.SPA/src/assets/images/image-gallery/9.jpg b/MyOffice.SPA/src/assets/images/image-gallery/9.jpg new file mode 100644 index 0000000..25bdf2b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/image-gallery/9.jpg differ diff --git a/MyOffice.SPA/src/assets/images/invoice_logo.png b/MyOffice.SPA/src/assets/images/invoice_logo.png new file mode 100644 index 0000000..357b09e Binary files /dev/null and b/MyOffice.SPA/src/assets/images/invoice_logo.png differ diff --git a/MyOffice.SPA/src/assets/images/light.png b/MyOffice.SPA/src/assets/images/light.png new file mode 100644 index 0000000..46595db Binary files /dev/null and b/MyOffice.SPA/src/assets/images/light.png differ diff --git a/MyOffice.SPA/src/assets/images/logo.png b/MyOffice.SPA/src/assets/images/logo.png new file mode 100644 index 0000000..8396882 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/logo.png differ diff --git a/MyOffice.SPA/src/assets/images/media.png b/MyOffice.SPA/src/assets/images/media.png new file mode 100644 index 0000000..d889069 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/media.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/bg-01.png b/MyOffice.SPA/src/assets/images/pages/bg-01.png new file mode 100644 index 0000000..2847236 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/bg-01.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/bg-02.png b/MyOffice.SPA/src/assets/images/pages/bg-02.png new file mode 100644 index 0000000..2847236 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/bg-02.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/bg-03.png b/MyOffice.SPA/src/assets/images/pages/bg-03.png new file mode 100644 index 0000000..2847236 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/bg-03.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/bg-04.png b/MyOffice.SPA/src/assets/images/pages/bg-04.png new file mode 100644 index 0000000..2847236 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/bg-04.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/bg-05.png b/MyOffice.SPA/src/assets/images/pages/bg-05.png new file mode 100644 index 0000000..2847236 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/bg-05.png differ diff --git a/MyOffice.SPA/src/assets/images/pages/home-1.png b/MyOffice.SPA/src/assets/images/pages/home-1.png new file mode 100644 index 0000000..df6683d Binary files /dev/null and b/MyOffice.SPA/src/assets/images/pages/home-1.png differ diff --git a/MyOffice.SPA/src/assets/images/posts/post1.jpg b/MyOffice.SPA/src/assets/images/posts/post1.jpg new file mode 100644 index 0000000..d706d8b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post1.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post10.jpg b/MyOffice.SPA/src/assets/images/posts/post10.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post10.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post11.jpg b/MyOffice.SPA/src/assets/images/posts/post11.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post11.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post12.jpg b/MyOffice.SPA/src/assets/images/posts/post12.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post12.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post13.jpg b/MyOffice.SPA/src/assets/images/posts/post13.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post13.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post2.jpg b/MyOffice.SPA/src/assets/images/posts/post2.jpg new file mode 100644 index 0000000..d706d8b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post2.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post3.jpg b/MyOffice.SPA/src/assets/images/posts/post3.jpg new file mode 100644 index 0000000..d706d8b Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post3.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post4.jpg b/MyOffice.SPA/src/assets/images/posts/post4.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post4.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post5.jpg b/MyOffice.SPA/src/assets/images/posts/post5.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post5.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post6.jpg b/MyOffice.SPA/src/assets/images/posts/post6.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post6.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post7.jpg b/MyOffice.SPA/src/assets/images/posts/post7.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post7.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post8.jpg b/MyOffice.SPA/src/assets/images/posts/post8.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post8.jpg differ diff --git a/MyOffice.SPA/src/assets/images/posts/post9.jpg b/MyOffice.SPA/src/assets/images/posts/post9.jpg new file mode 100644 index 0000000..5d46edc Binary files /dev/null and b/MyOffice.SPA/src/assets/images/posts/post9.jpg differ diff --git a/MyOffice.SPA/src/assets/images/products/p-13.jpg b/MyOffice.SPA/src/assets/images/products/p-13.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/products/p-13.jpg differ diff --git a/MyOffice.SPA/src/assets/images/products/p-14.jpg b/MyOffice.SPA/src/assets/images/products/p-14.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/products/p-14.jpg differ diff --git a/MyOffice.SPA/src/assets/images/products/p-15.jpg b/MyOffice.SPA/src/assets/images/products/p-15.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/products/p-15.jpg differ diff --git a/MyOffice.SPA/src/assets/images/test.pdf b/MyOffice.SPA/src/assets/images/test.pdf new file mode 100644 index 0000000..fc3d58a Binary files /dev/null and b/MyOffice.SPA/src/assets/images/test.pdf differ diff --git a/MyOffice.SPA/src/assets/images/thumbs-up.png b/MyOffice.SPA/src/assets/images/thumbs-up.png new file mode 100644 index 0000000..8273711 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/thumbs-up.png differ diff --git a/MyOffice.SPA/src/assets/images/user/admin.jpg b/MyOffice.SPA/src/assets/images/user/admin.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/admin.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user1.jpg b/MyOffice.SPA/src/assets/images/user/user1.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user1.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user10.jpg b/MyOffice.SPA/src/assets/images/user/user10.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user10.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user11.jpg b/MyOffice.SPA/src/assets/images/user/user11.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user11.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user2.jpg b/MyOffice.SPA/src/assets/images/user/user2.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user2.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user3.jpg b/MyOffice.SPA/src/assets/images/user/user3.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user3.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user4.jpg b/MyOffice.SPA/src/assets/images/user/user4.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user4.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user5.jpg b/MyOffice.SPA/src/assets/images/user/user5.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user5.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user6.jpg b/MyOffice.SPA/src/assets/images/user/user6.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user6.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user7.jpg b/MyOffice.SPA/src/assets/images/user/user7.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user7.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user8.jpg b/MyOffice.SPA/src/assets/images/user/user8.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user8.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/user9.jpg b/MyOffice.SPA/src/assets/images/user/user9.jpg new file mode 100644 index 0000000..dd2b3b8 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/user9.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig1.jpg b/MyOffice.SPA/src/assets/images/user/usrbig1.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig1.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig10.jpg b/MyOffice.SPA/src/assets/images/user/usrbig10.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig10.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig11.jpg b/MyOffice.SPA/src/assets/images/user/usrbig11.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig11.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig2.jpg b/MyOffice.SPA/src/assets/images/user/usrbig2.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig2.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig3.jpg b/MyOffice.SPA/src/assets/images/user/usrbig3.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig3.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig4.jpg b/MyOffice.SPA/src/assets/images/user/usrbig4.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig4.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig5.jpg b/MyOffice.SPA/src/assets/images/user/usrbig5.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig5.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig6.jpg b/MyOffice.SPA/src/assets/images/user/usrbig6.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig6.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig7.jpg b/MyOffice.SPA/src/assets/images/user/usrbig7.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig7.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig8.jpg b/MyOffice.SPA/src/assets/images/user/usrbig8.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig8.jpg differ diff --git a/MyOffice.SPA/src/assets/images/user/usrbig9.jpg b/MyOffice.SPA/src/assets/images/user/usrbig9.jpg new file mode 100644 index 0000000..cbe59c3 Binary files /dev/null and b/MyOffice.SPA/src/assets/images/user/usrbig9.jpg differ diff --git a/MyOffice.SPA/src/assets/scss/apps/_calendar.scss b/MyOffice.SPA/src/assets/scss/apps/_calendar.scss new file mode 100644 index 0000000..0a416be --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_calendar.scss @@ -0,0 +1,340 @@ +/* + * Document : _calendar.scss + * Author : RedStar Template + * Description: This scss file for style related to calendar app + */ + +#event_title { + font-size: 1.2rem; +} + +#calendar { + float: right; + width: 100%; +} + +#external-events { + .fc-event { + padding: 5px 10px; + font-size: 14px; + margin-bottom: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + cursor: move; + } + + .form-check .form-check-label { + font-size: 14px; + } +} + +.cal-event { + display: inline-block !important; + height: 10px; + width: 10px; + border-radius: 50%; + padding: 0; +} + +.fc-state-active { + background: #a389d4 !important; + color: #fff; +} + +.fc-day-grid-event { + color: white !important; + text-align: center; +} + +.fc-event-primary { + border: none !important; + background-color: #007bff !important; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), + 0 7px 10px -5px rgba(0, 154, 255, 0.4); + color: #fff; +} + +.fc-event-warning { + border: none !important; + background-color: #ff9800 !important; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), + 0 7px 10px -5px rgba(255, 152, 0, 0.4); + color: #fff; +} + +.fc-event-success { + border: none !important; + position: relative; + background-color: #53b958 !important; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), + 0 7px 10px -5px rgba(76, 175, 80, 0.4); + font-weight: 400; + color: #fff; +} + +.fc-event-danger { + border: none !important; + background-color: #f9483b !important; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), + 0 7px 10px -5px rgba(244, 67, 54, 0.4); + color: #fff; +} + +.fc-event-info { + border: none !important; + position: relative; + background-color: #03c5de !important; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), + 0 7px 10px -5px rgba(0, 188, 212, 0.4); + font-weight: 400; + color: #fff; +} + +.fc-event-default { + background: #007bff; + border: 1px solid #007bff; +} + +.fc-state-default { + border: 1px solid #eee; + background: transparent !important; + color: #7e869d; + border-radius: 0; +} + +.catLbl { + font-weight: 600; + color: #34395e; + font-size: 12px; + letter-spacing: 0.5px; + margin-bottom: 0; +} + +.fc .fc-button-primary { + background-color: #ffffff !important; + border: 1px solid #eee !important; + color: #7e869d !important; + border-radius: 5px !important; + margin-right: 8px !important; + + &:disabled { + color: #fff !important; + background-color: #6777ef !important; + border-color: #6777ef !important; + } + + &:hover { + color: #868181 !important; + background-color: #e1e0e0 !important; + border-color: #e1e0e0 !important; + } + + &:focus { + box-shadow: none !important; + } + + &:not(:disabled) { + &.fc-button-active { + color: #fff !important; + background-color: #6777ef !important; + border-color: #6777ef !important; + } + + &:active { + color: #fff !important; + background-color: #6777ef !important; + border-color: #6777ef !important; + } + } +} + +.fc-color-picker { + list-style: none; + margin: 0; + padding: 0; + + > li { + float: left; + font-size: 30px; + margin-right: 5px; + line-height: 30px; + } +} + +.fc-content-skeleton thead { + border-bottom: none; +} + +.fc-toolbar h2 { + font-size: 16px; + margin-top: 4px; +} + +.fc-view { + border-color: #f2f2f2; + + > table { + border-color: #f2f2f2; + + th { + border-color: #ddd; + color: color(fontdark) !important; + font-weight: 500; + padding: 15px; + } + } + + color: color(fontdark) !important; + font-weight: 500; + padding: 10px; + + .fc-scrollgrid-section { + > th { + padding: 0px; + } + } +} + +.fc-view-container > .fc-view { + padding: 0; +} + +.fc-view { + color: #666; + text-align: right; + + > table td { + color: #666; + text-align: right; + } +} + +.fc button .fc-icon { + top: -0.09em; +} + +.fc-basic-view { + .fc-day-number, + .fc-week-number { + padding: 10px; + } +} + +.fc .fc-daygrid-event { + padding: 0px 10px; + box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.1); + color: #ffffff; + text-align: left; +} +.fc-daygrid-block-event .fc-event-time { + font-weight: 400 !important; + font-size: 12px; +} +.fc-daygrid-event-dot { + display: none; +} +.fc-daygrid-dot-event .fc-event-title { + font-weight: 400 !important; + text-align: left; + font-size: 12px; +} +.fc-daygrid-dot-event .fc-event-time { + font-weight: 400 !important; + text-align: left; + font-size: 12px; +} +.fc-event-title-container .fc-event-title { + font-weight: 400 !important; + text-align: left; + font-size: 12px; +} + +tr:first-child > td > .fc-day-grid-event { + margin-bottom: 10px; +} + +.fc-state-default { + border-radius: 3px; + background-color: #f2f2f2; + background-image: none; + border: none; + box-shadow: none; + text-transform: capitalize; + font-weight: 500; +} + +.fc button { + height: auto; + padding: 10px 15px; + text-shadow: none; + border-radius: 0; + + &.fc-state-active { + background-color: color(primary); + color: #fff; + } +} + +.fc .fc-axis { + vertical-align: middle; + padding: 0 4px; + white-space: nowrap; +} + +.ngx-mat-timepicker { + .ng-pristine { + td { + padding: 0px; + border-top: none; + border-bottom: none; + } + } +} + +.fc-time-grid-event .fc-content { + color: #ffffff; +} + +.fc-list-event { + color: #ffffff; +} + +.fc-list-event:hover td { + background-color: #cccaca !important; +} + +.fc-event-container:hover { + cursor: pointer; +} + +.fc-daygrid-day-top { + text-align: center; + display: block !important; + .fc-daygrid-day-number { + color: #666; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 4px 0; + font-size: 12px !important; + } +} +.fc-day-today .fc-daygrid-day-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + margin: 4px 0; + font-size: 12px; + border-radius: 50%; + background: #6777ef; + color: #fff; +} +.fc .fc-daygrid-day.fc-day-today { + background-color: transparent !important; +} +.fc .fc-col-header-cell-cushion { + color: #666; +} diff --git a/MyOffice.SPA/src/assets/scss/apps/_chat.scss b/MyOffice.SPA/src/assets/scss/apps/_chat.scss new file mode 100644 index 0000000..4cc8208 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_chat.scss @@ -0,0 +1,218 @@ +/* + * Document : _chat.scss + * Author : RedStar Template + * Description: This scss file for style related to chat app + */ +%font_extend { + font-size: 16px; + color: gray; + cursor: pointer; +} + +.chat-app { + height: 690px; + .people-list { + width: 280px; + position: absolute; + left: 0; + top: 0; + padding: 20px; + } + .chat { + // margin-left: 280px; + border-left: 1px solid #e8e8e8; + } + .list_btn { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 9999; + padding: 0; + width: 40px; + height: 40px; + text-align: center; + line-height: 40px; + display: none; + @include box-shadow(0 10px 25px 0 rgba(0, 0, 0, 0.3)); + @include border-radius(3px); + } +} +.people-list { + transition: 0.5s; + .chat-list { + li { + padding: 10px 15px; + list-style: none; + @include border-radius(3px); + &:hover { + background: #efefef; + cursor: pointer; + } + &.active { + background: #efefef; + } + .name { + font-size: 15px; + } + } + img { + width: 45px; + @include border-radius(50%); + } + } + img { + float: left; + border: 1px solid #fff; + @include box-shadow(0px 5px 25px 0px rgba(0, 0, 0, 0.2)); + @include border-radius(50%); + } + .about { + float: left; + padding-left: 8px; + } + .status { + color: #999; + font-size: 13px; + } +} +.chat { + .chat-header { + padding: 20px; + border-bottom: 1px solid #eee; + border-radius: 0 0.55rem 0 0; + img { + float: left; + @include border-radius(50%); + width: 45px; + } + .chat-about { + float: left; + padding-left: 10px; + } + .chat-with { + font-weight: bold; + font-size: 16px; + } + .chat-num-messages { + color: 434651; + } + } + .chat-history { + padding: 20px; + border-bottom: 2px solid #fff; + height: 450px; + ul { + padding: 0; + li { + list-style: none; + } + } + .message-data { + margin-bottom: 15px; + .message-data-name { + font-size: 13px; + font-weight: 700; + } + } + .message-data-time { + color: #434651; + padding-left: 6px; + } + .message { + color: #444; + padding: 18px 20px; + line-height: 26px; + font-size: 13px; + @include border-radius(7px); + margin-bottom: 30px; + width: 90%; + position: relative; + &:after { + bottom: 100%; + left: 7%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-bottom-color: #fff; + border-width: 10px; + margin-left: -10px; + } + } + .my-message { + background: #e8e8e8; + &:after { + bottom: 100%; + left: 7%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-bottom-color: #e8e8e8; + border-width: 10px; + margin-left: -10px; + } + } + .other-message { + background: #d9e7ea; + &:after { + border-bottom-color: #d9e7ea; + left: 93%; + } + } + } + .chat-message { + padding: 20px; + textarea { + width: 100%; + border: none; + padding: 10px 20px; + font: 14px/22px Lato, Arial, sans-serif; + margin-bottom: 10px; + @include border-radius(5px); + resize: none; + } + .fa-file-o { + @extend %font_extend; + } + .fa-file-image-o { + @extend %font_extend; + } + } +} +.online { + margin-right: 3px; + font-size: 10px; + color: #86bb71; +} +.offline { + margin-right: 3px; + font-size: 10px; + color: #e38968; +} +.me { + margin-right: 3px; + font-size: 10px; + color: #0498bd; +} +.float-end { + float: right; +} +.clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; +} +.slimScrollBar { + z-index: 0 !important; +} +.chat-upload { + float: right; +} diff --git a/MyOffice.SPA/src/assets/scss/apps/_contactgrid.scss b/MyOffice.SPA/src/assets/scss/apps/_contactgrid.scss new file mode 100644 index 0000000..5e7eea7 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_contactgrid.scss @@ -0,0 +1,36 @@ +/* + * Document : _contactgrid.scss + * Author : RedStar Template + * Description: This scss file for style related to contact grid app + */ +.contact-grid { + float: left; + width: 100%; + text-align: center; +} +.profile-header { + min-height: 150px; + color: #fff; +} +.user-name { + padding: 3px; + font-size: 22px; + text-align: center; + padding-top: 10px; +} +.user-img { + padding: 3px; + border-radius: 50% 50% 50% 50%; + max-width: 112px; + margin-top: -70px; + box-shadow: 0px 10px 25px 0px rgba(0, 0, 0, 0.3); + margin-bottom: 20px; +} +.profile-userbuttons { + text-align: center; + margin-top: 10px; +} +.contact-grid .phone .material-icons { + font-size: 16px; + padding: 4px 7px 40px 0px; +} diff --git a/MyOffice.SPA/src/assets/scss/apps/_contactlist.scss b/MyOffice.SPA/src/assets/scss/apps/_contactlist.scss new file mode 100644 index 0000000..18c17da --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_contactlist.scss @@ -0,0 +1,127 @@ +/* + * Document : _contactlist.scss + * Author : RedStar Template + * Description: This scss file for style related to contact list app + */ +.contact-detail { + .fa { + float: left; + width: 30px; + font-size: 20px; + margin-top: 5px; + } + span { + float: left; + width: calc(100% - 30px); + margin-bottom: 20px; + } + .fa-envelope { + font-size: 15px; + } + .fa-mobile { + font-size: 25px; + } +} +.contact-photo { + float: left; + width: 100%; + text-align: center; + padding-top: 20px; + img { + margin: 0 auto; + width: 130px; + padding: 3px; + border: 3px solid rgb(210, 214, 222); + border-radius: 50% 50% 50% 50%; + } +} +.contact-usertitle { + text-align: center; + margin-top: 5px; +} +.contact-usertitle-name { + font-size: 20px; + margin-bottom: 2px; + font-weight: bold; + color: #3a405b; +} +.contact-usertitle-job { + color: #777777; + font-size: 12px; + margin-bottom: 5px; +} +.newLabelBtn { + padding: 20px 0; + text-align: center; +} +.alert-dismissible .close { + text-indent: 0; +} +.alert.alert-dismissible { + color: #ffffff; +} +.contact_list { + .phone { + position: relative; + padding-left: 20px; + .material-icons { + position: absolute; + left: 0; + font-size: 16px; + top: 1px; + } + } +} +.list-group-unbordered { + .list-group-item { + border: 0px; + } +} +.contact-header { + display: flex; + + .contact-details-img img { + position: relative; + display: flex; + width: 80px; + height: 80px; + border-radius: 50%; + margin-right: 0px; + overflow: hidden; + } +} + +.contact-details-name { + font-size: 20px; + font-weight: 500; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + letter-spacing: 1px; +} + +.contact-details-field { + display: flex; + margin-top: 30px; + + .material-icons-two-tone { + margin: 0 20px 0 10px; + } + + .contact-detail-info { + white-space: pre-wrap; + } + + .color-icon { + filter: invert(14%) sepia(0%) saturate(5141%) hue-rotate(101deg) + brightness(95%) contrast(97%); + } +} + +.contact-form { + .modalHeader img { + border-radius: 50%; + height: 35px; + width: 35px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/apps/_dragdrop.scss b/MyOffice.SPA/src/assets/scss/apps/_dragdrop.scss new file mode 100644 index 0000000..cd4c6eb --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_dragdrop.scss @@ -0,0 +1,153 @@ +.example-box { + width: 200px; + height: 200px; + border: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + cursor: move; + display: inline-flex; + justify-content: center; + align-items: center; + text-align: center; + background: #fff; + border-radius: 4px; + margin-right: 25px; + position: relative; + z-index: 1; + box-sizing: border-box; + padding: 10px; + transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1); + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); +} + +.example-box:active { + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.example-boundary { + width: 400px; + height: 400px; + max-width: 100%; + border: dotted #ccc 2px; +} + +.example-container2 { + width: 400px; + max-width: 100%; + margin: 0 25px 25px 0; + display: inline-block; + vertical-align: top; +} + +.example-list { + border: solid 1px #ccc; + min-height: 60px; + background: white; + border-radius: 4px; + overflow: hidden; + display: block; +} + +.example-box-small { + padding: 20px 10px; + border-bottom: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + cursor: move; + background: white; + font-size: 14px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-box-small:last-child { + border: none; +} + +.example-list.cdk-drop-list-dragging + .example-box-small:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-list2 { + width: 500px; + max-width: 100%; + border: solid 1px #ccc; + min-height: 60px; + display: block; + background: white; + border-radius: 4px; + overflow: hidden; +} + +.example-box2 { + padding: 20px 10px; + border-bottom: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + cursor: move; + background: white; + font-size: 14px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-box2:last-child { + border: none; +} + +.example-list2.cdk-drop-list-dragging .example-box2:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-custom-placeholder { + background: #ccc; + border: dotted 3px #999; + min-height: 60px; + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-box-small.cdk-drag-disabled { + background: #ccc; + cursor: default; +} +.example-handle { + position: absolute; + top: 10px; + right: 10px; + color: #ccc; + cursor: move; + width: 24px; + height: 24px; +} diff --git a/MyOffice.SPA/src/assets/scss/apps/_task.scss b/MyOffice.SPA/src/assets/scss/apps/_task.scss new file mode 100644 index 0000000..28116f9 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/apps/_task.scss @@ -0,0 +1,250 @@ +.gu-mirror { + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + opacity: 0.8; + filter: alpha(opacity=80); +} + +.gu-hide { + display: none !important; +} + +.gu-unselectable { + //Instead of the line below you could use @include user-select($select) + user-select: none !important; +} + +.gu-transit { + opacity: 0.2; + filter: alpha(opacity=20); +} + +.taskbar { + .checked { + border: 3px solid #dda500 !important; + } + + .box { + padding-bottom: 7px; + + .task-name { + font-size: 18px; + font-weight: 600; + color: green; + } + + .task-desc { + font-size: 1.1em; + color: #555; + } + + .task-deadline { + margin-top: 5px; + margin-right: 5px; + font-size: 0.8em; + color: #555; + } + } + + .done div.box { + pointer-events: none; + + .tasks { + color: #fff !important; + } + } + + .avatar-image { + display: inline-block; + position: relative; + } + + .task-header { + text-align: center; + font-size: 25px; + padding: 20px; + color: #6b6a6a; + } + + .card-footer { + background: #fff !important; + border-radius: 0px 0px 10px 10px; + + .progress { + height: 5px; + } + } +} + +.pad { + padding-left: 0; + padding-right: 0; + display: flex; +} + +.move { + text-align: center; + background: #eee; + + .btn { + margin-bottom: 20px; + } +} + +.task-module { + display: flex; + flex-direction: column; + padding: 0 !important; + min-height: 100%; + + .mat-drawer.mat-drawer-end { + width: 500px; + min-width: 500px; + border: 1px solid #dcd6d6; + } + + .task-header { + display: flex; + flex: 0 1 auto; + align-items: center; + margin: 10px 0px 23px 0px; + } + + .header-button { + margin-left: auto; + } +} + +.task-container { + .header { + .header-title { + margin: 0px 0px 0px 5px; + } + + .header-close { + position: absolute; + top: 10px; + right: 15px; + list-style: none; + } + } +} + +.task-list { + width: 100%; + max-width: 100%; + border: solid 1px #ccc; + min-height: 60px; + display: block; + background: white; + border-radius: 4px; + overflow: hidden; +} + +.task-user-img { + height: 30px; + width: 30px; + position: relative; + margin-left: 30px; + border-radius: 50%; +} + +.task-date { + margin-left: 30px; +} + +.lbl-low { + display: inline-flex; + vertical-align: middle; +} + +.lbl-high { + display: inline-flex; + vertical-align: middle; +} + +.lbl-normal { + display: inline-flex; + vertical-align: middle; +} + +.task-low { + color: #0bc53a; + position: relative; + margin-left: auto; +} + +.task-high { + color: #f9683a; + position: relative; + margin-left: auto; +} + +.task-normal { + color: #868688; + position: relative; + margin-left: auto; +} + +.task-box { + padding: 10px 10px; + border-bottom: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + // justify-content: space-between; + box-sizing: border-box; + cursor: move; + background: white; + font-size: 14px; + + &:hover { + cursor: pointer; + } +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.task-box:last-child { + border: none; +} + +.task-list.cdk-drop-list-dragging .task-box:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.task-custom-placeholder { + background: #ccc; + border: dotted 3px #999; + min-height: 60px; + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.task-handle { + color: #ccc; + cursor: move; + width: 24px; + height: 24px; +} + +.done { + text-decoration: line-through; + opacity: 0.5; +} + +.mat-list-item-content { + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/MyOffice.SPA/src/assets/scss/browser/_ie10.scss b/MyOffice.SPA/src/assets/scss/browser/_ie10.scss new file mode 100644 index 0000000..698e265 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/browser/_ie10.scss @@ -0,0 +1,60 @@ +html { + &.ie10 { + .sidebar { + .menu { + .list { + li { + line-height: 30px; + } + + .ml-menu { + li { + &.active { + a { + &:not(.menu-toggle) { + &.toggled { + &:before { + top: 6px !important; + line-height: 20px !important; + } + } + } + } + } + } + } + } + } + + .user-info { + .info-container { + top: 15px; + } + } + } + + .search-bar { + input[type="text"] { + padding: 26px 60px 26px 56px; + } + } + + .dropdown-menu { + ul { + &.menu { + li { + a { + margin-top: -22px; + } + } + } + } + } + + .bs-searchbox { + .form-control { + width: 90%; + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/browser/_ie11.scss b/MyOffice.SPA/src/assets/scss/browser/_ie11.scss new file mode 100644 index 0000000..00779db --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/browser/_ie11.scss @@ -0,0 +1,56 @@ +html { + &.ie11 { + .sidebar { + .menu { + .list { + .ml-menu { + li { + &.active { + a { + &:not(.menu-toggle) { + &.toggled { + &:before { + top: 6px !important; + line-height: 20px !important; + } + } + } + } + } + } + } + } + } + + .user-info { + .info-container { + top: 15px; + } + } + } + + .search-bar { + input[type="text"] { + padding: 26px 60px 26px 56px; + } + } + + .dropdown-menu { + ul { + &.menu { + li { + a { + margin-top: -22px; + } + } + } + } + } + + .bs-searchbox { + .form-control { + width: 90%; + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_animation.scss b/MyOffice.SPA/src/assets/scss/common/_animation.scss new file mode 100644 index 0000000..6ed8cde --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_animation.scss @@ -0,0 +1,1046 @@ +/* + * Document : _animation.scss + * Author : RedStar Template + * Description: This scss file for animation css classes + */ +.waves-effect { + position: relative; + cursor: pointer; + display: inline-block; + overflow: hidden; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; +} +.waves-effect .waves-ripple { + position: absolute; + border-radius: 50%; + width: 100px; + height: 100px; + margin-top: -50px; + margin-left: -50px; + opacity: 0; + background: transparent; + // background:rgba(0,0,0,0.2); + // background:-webkit-radial-gradient(rgba(0,0,0,0.2) 0, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.5) 60%, rgba(255,255,255,0) 70%); + // background:-o-radial-gradient(rgba(0,0,0,0.2) 0, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.5) 60%, rgba(255,255,255,0) 70%); + // background:-moz-radial-gradient(rgba(0,0,0,0.2) 0, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.5) 60%, rgba(255,255,255,0) 70%); + // background:radial-gradient(rgba(0,0,0,0.2) 0, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.5) 60%, rgba(255,255,255,0) 70%); + -webkit-transition: all 0.5s ease-out; + -moz-transition: all 0.5s ease-out; + -o-transition: all 0.5s ease-out; + transition: all 0.5s ease-out; + -webkit-transition-property: -webkit-transform, opacity; + -moz-transition-property: -moz-transform, opacity; + -o-transition-property: -o-transform, opacity; + transition-property: transform, opacity; + -webkit-transform: scale(0) translate(0, 0); + -moz-transform: scale(0) translate(0, 0); + -ms-transform: scale(0) translate(0, 0); + -o-transform: scale(0) translate(0, 0); + transform: scale(0) translate(0, 0); + pointer-events: none; +} +.waves-effect.waves-light .waves-ripple { + background: rgba(255, 255, 255, 0.4); + background: -webkit-radial-gradient( + rgba(255, 255, 255, 0.2) 0, + rgba(255, 255, 255, 0.3) 40%, + rgba(255, 255, 255, 0.4) 50%, + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 70% + ); + background: -o-radial-gradient( + rgba(255, 255, 255, 0.2) 0, + rgba(255, 255, 255, 0.3) 40%, + rgba(255, 255, 255, 0.4) 50%, + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 70% + ); + background: -moz-radial-gradient( + rgba(255, 255, 255, 0.2) 0, + rgba(255, 255, 255, 0.3) 40%, + rgba(255, 255, 255, 0.4) 50%, + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 70% + ); + background: radial-gradient( + rgba(255, 255, 255, 0.2) 0, + rgba(255, 255, 255, 0.3) 40%, + rgba(255, 255, 255, 0.4) 50%, + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 70% + ); +} +.waves-effect.waves-classic .waves-ripple { + background: rgba(0, 0, 0, 0.2); +} +.waves-effect.waves-classic.waves-light .waves-ripple { + background: rgba(255, 255, 255, 0.4); +} +.waves-notransition { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + transition: none !important; +} +.waves-button, +.waves-circle { + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + transform: translateZ(0); + -webkit-mask-image: -webkit-radial-gradient(circle, #fff 100%, #000 100%); +} +.waves-button, +.waves-button:hover, +.waves-button:visited, +.waves-button-input { + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: none; + outline: none; + color: inherit; + background-color: transparent; + font-size: 1em; + line-height: 1em; + text-align: center; + text-decoration: none; + z-index: 1; +} +.waves-button { + padding: 0.85em 1.1em; + border-radius: 0.2em; +} +.waves-button-input { + margin: 0; + padding: 0.85em 1.1em; +} +.waves-input-wrapper { + border-radius: 0.2em; + vertical-align: bottom; +} +.waves-input-wrapper.waves-button { + padding: 0; +} +.waves-input-wrapper .waves-button-input { + position: relative; + top: 0; + left: 0; + z-index: 1; +} +.waves-circle { + text-align: center; + width: 2.5em; + height: 2.5em; + line-height: 2.5em; + border-radius: 50%; +} +.waves-float { + -webkit-mask-image: none; + -webkit-box-shadow: 0px 1px 1.5px 1px rgba(0, 0, 0, 0.12); + box-shadow: 0px 1px 1.5px 1px rgba(0, 0, 0, 0.12); + -webkit-transition: all 300ms; + -moz-transition: all 300ms; + -o-transition: all 300ms; + transition: all 300ms; +} +.waves-float:active { + -webkit-box-shadow: 0px 8px 20px 1px rgba(0, 0, 0, 0.3); + box-shadow: 0px 8px 20px 1px rgba(0, 0, 0, 0.3); +} +.waves-block { + display: block; +} +.slideDown { + animation-name: slideDown; + -webkit-animation-name: slideDown; + animation-duration: 1s; + -webkit-animation-duration: 1s; + animation-timing-function: ease; + -webkit-animation-timing-function: ease; + visibility: visible !important; +} +@keyframes slideDown { + 0% { + transform: translateY(-100%); + } + 50% { + transform: translateY(8%); + } + 65% { + transform: translateY(-4%); + } + 80% { + transform: translateY(4%); + } + 95% { + transform: translateY(-2%); + } + 100% { + transform: translateY(0%); + } +} +@-webkit-keyframes slideDown { + 0% { + -webkit-transform: translateY(-100%); + } + 50% { + -webkit-transform: translateY(8%); + } + 65% { + -webkit-transform: translateY(-4%); + } + 80% { + -webkit-transform: translateY(4%); + } + 95% { + -webkit-transform: translateY(-2%); + } + 100% { + -webkit-transform: translateY(0%); + } +} +.slideUp { + animation-name: slideUp; + -webkit-animation-name: slideUp; + animation-duration: 1s; + -webkit-animation-duration: 1s; + animation-timing-function: ease; + -webkit-animation-timing-function: ease; + visibility: visible !important; +} +@keyframes slideUp { + 0% { + transform: translateY(100%); + } + 50% { + transform: translateY(-8%); + } + 65% { + transform: translateY(4%); + } + 80% { + transform: translateY(-4%); + } + 95% { + transform: translateY(2%); + } + 100% { + transform: translateY(0%); + } +} +@-webkit-keyframes slideUp { + 0% { + -webkit-transform: translateY(100%); + } + 50% { + -webkit-transform: translateY(-8%); + } + 65% { + -webkit-transform: translateY(4%); + } + 80% { + -webkit-transform: translateY(-4%); + } + 95% { + -webkit-transform: translateY(2%); + } + 100% { + -webkit-transform: translateY(0%); + } +} +.slideLeft { + animation-name: slideLeft; + -webkit-animation-name: slideLeft; + animation-duration: 1s; + -webkit-animation-duration: 1s; + animation-timing-function: ease-in-out; + -webkit-animation-timing-function: ease-in-out; + visibility: visible !important; +} +@keyframes slideLeft { + 0% { + transform: translateX(150%); + } + 50% { + transform: translateX(-8%); + } + 65% { + transform: translateX(4%); + } + 80% { + transform: translateX(-4%); + } + 95% { + transform: translateX(2%); + } + 100% { + transform: translateX(0%); + } +} +@-webkit-keyframes slideLeft { + 0% { + -webkit-transform: translateX(150%); + } + 50% { + -webkit-transform: translateX(-8%); + } + 65% { + -webkit-transform: translateX(4%); + } + 80% { + -webkit-transform: translateX(-4%); + } + 95% { + -webkit-transform: translateX(2%); + } + 100% { + -webkit-transform: translateX(0%); + } +} +.slideRight { + animation-name: slideRight; + -webkit-animation-name: slideRight; + animation-duration: 1s; + -webkit-animation-duration: 1s; + animation-timing-function: ease-in-out; + -webkit-animation-timing-function: ease-in-out; + visibility: visible !important; +} +@keyframes slideRight { + 0% { + transform: translateX(-150%); + } + 50% { + transform: translateX(8%); + } + 65% { + transform: translateX(-4%); + } + 80% { + transform: translateX(4%); + } + 95% { + transform: translateX(-2%); + } + 100% { + transform: translateX(0%); + } +} +@-webkit-keyframes slideRight { + 0% { + -webkit-transform: translateX(-150%); + } + 50% { + -webkit-transform: translateX(8%); + } + 65% { + -webkit-transform: translateX(-4%); + } + 80% { + -webkit-transform: translateX(4%); + } + 95% { + -webkit-transform: translateX(-2%); + } + 100% { + -webkit-transform: translateX(0%); + } +} +.slideExpandUp { + animation-name: slideExpandUp; + -webkit-animation-name: slideExpandUp; + animation-duration: 1.6s; + -webkit-animation-duration: 1.6s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease -out; + visibility: visible !important; +} +@keyframes slideExpandUp { + 0% { + transform: translateY(100%) scaleX(0.5); + } + 30% { + transform: translateY(-8%) scaleX(0.5); + } + 40% { + transform: translateY(2%) scaleX(0.5); + } + 50% { + transform: translateY(0%) scaleX(1.1); + } + 60% { + transform: translateY(0%) scaleX(0.9); + } + 70% { + transform: translateY(0%) scaleX(1.05); + } + 80% { + transform: translateY(0%) scaleX(0.95); + } + 90% { + transform: translateY(0%) scaleX(1.02); + } + 100% { + transform: translateY(0%) scaleX(1); + } +} +@-webkit-keyframes slideExpandUp { + 0% { + -webkit-transform: translateY(100%) scaleX(0.5); + } + 30% { + -webkit-transform: translateY(-8%) scaleX(0.5); + } + 40% { + -webkit-transform: translateY(2%) scaleX(0.5); + } + 50% { + -webkit-transform: translateY(0%) scaleX(1.1); + } + 60% { + -webkit-transform: translateY(0%) scaleX(0.9); + } + 70% { + -webkit-transform: translateY(0%) scaleX(1.05); + } + 80% { + -webkit-transform: translateY(0%) scaleX(0.95); + } + 90% { + -webkit-transform: translateY(0%) scaleX(1.02); + } + 100% { + -webkit-transform: translateY(0%) scaleX(1); + } +} +.expandUp { + animation-name: expandUp; + -webkit-animation-name: expandUp; + animation-duration: 0.7s; + -webkit-animation-duration: 0.7s; + animation-timing-function: ease; + -webkit-animation-timing-function: ease; + visibility: visible !important; +} +@keyframes expandUp { + 0% { + transform: translateY(100%) scale(0.6) scaleY(0.5); + } + 60% { + transform: translateY(-7%) scaleY(1.12); + } + 75% { + transform: translateY(3%); + } + 100% { + transform: translateY(0%) scale(1) scaleY(1); + } +} +@-webkit-keyframes expandUp { + 0% { + -webkit-transform: translateY(100%) scale(0.6) scaleY(0.5); + } + 60% { + -webkit-transform: translateY(-7%) scaleY(1.12); + } + 75% { + -webkit-transform: translateY(3%); + } + 100% { + -webkit-transform: translateY(0%) scale(1) scaleY(1); + } +} +.fadeIn { + animation-name: fadeIn; + -webkit-animation-name: fadeIn; + animation-duration: 1.5s; + -webkit-animation-duration: 1.5s; + animation-timing-function: ease-in-out; + -webkit-animation-timing-function: ease-in-out; + visibility: visible !important; +} +@keyframes fadeIn { + 0% { + transform: scale(0); + opacity: 0; + } + 60% { + transform: scale(1.1); + } + 80% { + transform: scale(0.9); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} +@-webkit-keyframes fadeIn { + 0% { + -webkit-transform: scale(0); + opacity: 0; + } + 60% { + -webkit-transform: scale(1.1); + } + 80% { + -webkit-transform: scale(0.9); + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + opacity: 1; + } +} +.expandOpen { + animation-name: expandOpen; + -webkit-animation-name: expandOpen; + animation-duration: 1.2s; + -webkit-animation-duration: 1.2s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + visibility: visible !important; +} +@keyframes expandOpen { + 0% { + transform: scale(1.8); + } + 50% { + transform: scale(0.95); + } + 80% { + transform: scale(1.05); + } + 90% { + transform: scale(0.98); + } + 100% { + transform: scale(1); + } +} +@-webkit-keyframes expandOpen { + 0% { + -webkit-transform: scale(1.8); + } + 50% { + -webkit-transform: scale(0.95); + } + 80% { + -webkit-transform: scale(1.05); + } + 90% { + -webkit-transform: scale(0.98); + } + 100% { + -webkit-transform: scale(1); + } +} +.bigEntrance { + animation-name: bigEntrance; + -webkit-animation-name: bigEntrance; + animation-duration: 1.6s; + -webkit-animation-duration: 1.6s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + visibility: visible !important; +} +@keyframes bigEntrance { + 0% { + transform: scale(0.3) rotate(6deg) translateX(-30%) translateY(30%); + opacity: 0.2; + } + 30% { + transform: scale(1.03) rotate(-2deg) translateX(2%) translateY(-2%); + opacity: 1; + } + 45% { + transform: scale(0.98) rotate(1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 60% { + transform: scale(1.01) rotate(-1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 75% { + transform: scale(0.99) rotate(1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 90% { + transform: scale(1.01) rotate(0deg) translateX(0%) translateY(0%); + opacity: 1; + } + 100% { + transform: scale(1) rotate(0deg) translateX(0%) translateY(0%); + opacity: 1; + } +} +@-webkit-keyframes bigEntrance { + 0% { + -webkit-transform: scale(0.3) rotate(6deg) translateX(-30%) translateY(30%); + opacity: 0.2; + } + 30% { + -webkit-transform: scale(1.03) rotate(-2deg) translateX(2%) translateY(-2%); + opacity: 1; + } + 45% { + -webkit-transform: scale(0.98) rotate(1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 60% { + -webkit-transform: scale(1.01) rotate(-1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 75% { + -webkit-transform: scale(0.99) rotate(1deg) translateX(0%) translateY(0%); + opacity: 1; + } + 90% { + -webkit-transform: scale(1.01) rotate(0deg) translateX(0%) translateY(0%); + opacity: 1; + } + 100% { + -webkit-transform: scale(1) rotate(0deg) translateX(0%) translateY(0%); + opacity: 1; + } +} +.hatch { + animation-name: hatch; + -webkit-animation-name: hatch; + animation-duration: 2s; + -webkit-animation-duration: 2s; + animation-timing-function: ease-in-out; + -webkit-animation-timing-function: ease-in-out; + transform-origin: 50% 100%; + -ms-transform-origin: 50% 100%; + -webkit-transform-origin: 50% 100%; + visibility: visible !important; +} +@keyframes hatch { + 0% { + transform: rotate(0deg) scaleY(0.6); + } + 20% { + transform: rotate(-2deg) scaleY(1.05); + } + 35% { + transform: rotate(2deg) scaleY(1); + } + 50% { + transform: rotate(-2deg); + } + 65% { + transform: rotate(1deg); + } + 80% { + transform: rotate(-1deg); + } + 100% { + transform: rotate(0deg); + } +} +@-webkit-keyframes hatch { + 0% { + -webkit-transform: rotate(0deg) scaleY(0.6); + } + 20% { + -webkit-transform: rotate(-2deg) scaleY(1.05); + } + 35% { + -webkit-transform: rotate(2deg) scaleY(1); + } + 50% { + -webkit-transform: rotate(-2deg); + } + 65% { + -webkit-transform: rotate(1deg); + } + 80% { + -webkit-transform: rotate(-1deg); + } + 100% { + -webkit-transform: rotate(0deg); + } +} +.bounce { + animation-name: bounce; + -webkit-animation-name: bounce; + animation-duration: 1.6s; + -webkit-animation-duration: 1.6s; + animation-timing-function: ease; + -webkit-animation-timing-function: ease; + transform-origin: 50% 100%; + -ms-transform-origin: 50% 100%; + -webkit-transform-origin: 50% 100%; +} +@keyframes bounce { + 0% { + transform: translateY(0%) scaleY(0.6); + } + 60% { + transform: translateY(-100%) scaleY(1.1); + } + 70% { + transform: translateY(0%) scaleY(0.95) scaleX(1.05); + } + 80% { + transform: translateY(0%) scaleY(1.05) scaleX(1); + } + 90% { + transform: translateY(0%) scaleY(0.95) scaleX(1); + } + 100% { + transform: translateY(0%) scaleY(1) scaleX(1); + } +} +@-webkit-keyframes bounce { + 0% { + -webkit-transform: translateY(0%) scaleY(0.6); + } + 60% { + -webkit-transform: translateY(-100%) scaleY(1.1); + } + 70% { + -webkit-transform: translateY(0%) scaleY(0.95) scaleX(1.05); + } + 80% { + -webkit-transform: translateY(0%) scaleY(1.05) scaleX(1); + } + 90% { + -webkit-transform: translateY(0%) scaleY(0.95) scaleX(1); + } + 100% { + -webkit-transform: translateY(0%) scaleY(1) scaleX(1); + } +} +.pulse { + animation-name: pulse; + -webkit-animation-name: pulse; + animation-duration: 1.5s; + -webkit-animation-duration: 1.5s; + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; +} +@keyframes pulse { + 0% { + transform: scale(0.9); + opacity: 0.7; + } + 50% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(0.9); + opacity: 0.7; + } +} +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(0.95); + opacity: 0.7; + } + 50% { + -webkit-transform: scale(1); + opacity: 1; + } + 100% { + -webkit-transform: scale(0.95); + opacity: 0.7; + } +} +.floating { + animation-name: floating; + -webkit-animation-name: floating; + animation-duration: 1.5s; + -webkit-animation-duration: 1.5s; + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; +} +@keyframes floating { + 0% { + transform: translateY(0%); + } + 50% { + transform: translateY(8%); + } + 100% { + transform: translateY(0%); + } +} +@-webkit-keyframes floating { + 0% { + -webkit-transform: translateY(0%); + } + 50% { + -webkit-transform: translateY(8%); + } + 100% { + -webkit-transform: translateY(0%); + } +} +.tossing { + animation-name: tossing; + -webkit-animation-name: tossing; + animation-duration: 2.5s; + -webkit-animation-duration: 2.5s; + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; +} +@keyframes tossing { + 0% { + transform: rotate(-4deg); + } + 50% { + transform: rotate(4deg); + } + 100% { + transform: rotate(-4deg); + } +} +@-webkit-keyframes tossing { + 0% { + -webkit-transform: rotate(-4deg); + } + 50% { + -webkit-transform: rotate(4deg); + } + 100% { + -webkit-transform: rotate(-4deg); + } +} +.pullUp { + animation-name: pullUp; + -webkit-animation-name: pullUp; + animation-duration: 1.1s; + -webkit-animation-duration: 1.1s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + transform-origin: 50% 100%; + -ms-transform-origin: 50% 100%; + -webkit-transform-origin: 50% 100%; +} +@keyframes pullUp { + 0% { + transform: scaleY(0.1); + } + 40% { + transform: scaleY(1.02); + } + 60% { + transform: scaleY(0.98); + } + 80% { + transform: scaleY(1.01); + } + 100% { + transform: scaleY(0.98); + } + 80% { + transform: scaleY(1.01); + } + 100% { + transform: scaleY(1); + } +} +@-webkit-keyframes pullUp { + 0% { + -webkit-transform: scaleY(0.1); + } + 40% { + -webkit-transform: scaleY(1.02); + } + 60% { + -webkit-transform: scaleY(0.98); + } + 80% { + -webkit-transform: scaleY(1.01); + } + 100% { + -webkit-transform: scaleY(0.98); + } + 80% { + -webkit-transform: scaleY(1.01); + } + 100% { + -webkit-transform: scaleY(1); + } +} +.pullDown { + animation-name: pullDown; + -webkit-animation-name: pullDown; + animation-duration: 0.8s; + -webkit-animation-duration: 0.8s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + transform-origin: 50% 0%; + -ms-transform-origin: 50% 0%; + -webkit-transform-origin: 50% 0%; +} +@keyframes pullDown { + 0% { + transform: scaleY(0.1); + } + 40% { + transform: scaleY(1.02); + } + 60% { + transform: scaleY(0.98); + } + 80% { + transform: scaleY(1.01); + } + 100% { + transform: scaleY(0.98); + } + 80% { + transform: scaleY(1.01); + } + 100% { + transform: scaleY(1); + } +} +@-webkit-keyframes pullDown { + 0% { + -webkit-transform: scaleY(0.1); + } + 40% { + -webkit-transform: scaleY(1.02); + } + 60% { + -webkit-transform: scaleY(0.98); + } + 80% { + -webkit-transform: scaleY(1.01); + } + 100% { + -webkit-transform: scaleY(0.98); + } + 80% { + -webkit-transform: scaleY(1.01); + } + 100% { + -webkit-transform: scaleY(1); + } +} +.stretchLeft { + animation-name: stretchLeft; + -webkit-animation-name: stretchLeft; + animation-duration: 1.5s; + -webkit-animation-duration: 1.5s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + transform-origin: 100% 0%; + -ms-transform-origin: 100% 0%; + -webkit-transform-origin: 100% 0%; +} +@keyframes stretchLeft { + 0% { + transform: scaleX(0.3); + } + 40% { + transform: scaleX(1.02); + } + 60% { + transform: scaleX(0.98); + } + 80% { + transform: scaleX(1.01); + } + 100% { + transform: scaleX(0.98); + } + 80% { + transform: scaleX(1.01); + } + 100% { + transform: scaleX(1); + } +} +@-webkit-keyframes stretchLeft { + 0% { + -webkit-transform: scaleX(0.3); + } + 40% { + -webkit-transform: scaleX(1.02); + } + 60% { + -webkit-transform: scaleX(0.98); + } + 80% { + -webkit-transform: scaleX(1.01); + } + 100% { + -webkit-transform: scaleX(0.98); + } + 80% { + -webkit-transform: scaleX(1.01); + } + 100% { + -webkit-transform: scaleX(1); + } +} +.stretchRight { + animation-name: stretchRight; + -webkit-animation-name: stretchRight; + animation-duration: 1.5s; + -webkit-animation-duration: 1.5s; + animation-timing-function: ease-out; + -webkit-animation-timing-function: ease-out; + transform-origin: 0% 0%; + -ms-transform-origin: 0% 0%; + -webkit-transform-origin: 0% 0%; +} +@keyframes stretchRight { + 0% { + transform: scaleX(0.3); + } + 40% { + transform: scaleX(1.02); + } + 60% { + transform: scaleX(0.98); + } + 80% { + transform: scaleX(1.01); + } + 100% { + transform: scaleX(0.98); + } + 80% { + transform: scaleX(1.01); + } + 100% { + transform: scaleX(1); + } +} +@-webkit-keyframes stretchRight { + 0% { + -webkit-transform: scaleX(0.3); + } + 40% { + -webkit-transform: scaleX(1.02); + } + 60% { + -webkit-transform: scaleX(0.98); + } + 80% { + -webkit-transform: scaleX(1.01); + } + 100% { + -webkit-transform: scaleX(0.98); + } + 80% { + -webkit-transform: scaleX(1.01); + } + 100% { + -webkit-transform: scaleX(1); + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_customanimate.scss b/MyOffice.SPA/src/assets/scss/common/_customanimate.scss new file mode 100644 index 0000000..bd021ce --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_customanimate.scss @@ -0,0 +1,76 @@ +/* + * Document : _customanimate.scss + * Author : RedStar Template + * Description: This scss file for mixure of custom animation classes + */ +@-ms-keyframes spin { + from { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-moz-keyframes spin { + from { + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + from { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes spin { + from { + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_demo.scss b/MyOffice.SPA/src/assets/scss/common/_demo.scss new file mode 100644 index 0000000..74cbef7 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_demo.scss @@ -0,0 +1,372 @@ +/* + * Document : _demo.scss + * Author : RedStar Template + * Description: This scss file style classes of following components + + Structure (with shortcodes): + + 1. BUTTONS + 2. CHECKBOX & RADIO + 3. JQUERY KNOB-CHART + 4. SWITCH + 5. COLOR BOX + 6. IMAGES + 7. TAGS INPUT + 8. GOOGLE MATERIAL DESIGN ICON + 9. PRELOADER + 10. ION RANGE SLIDER + 11. RIGHT SIDEBAR + + */ + +/*********************************************************************** + + [1. BUTTONS ] + +***********************************************************************/ +.demo-button-sizes { + .btn { + margin-bottom: 5px; + } +} + +.icon-button-demo { + button { + margin-right: 5px; + margin-bottom: 12px; + } +} + +.icon-and-text-button-demo { + button { + margin-right: 5px; + margin-bottom: 12px; + width: 16.66666666666667%; + } +} + +.button-demo { + ul { + padding-left: 0; + + li { + list-style: none; + padding-left: 0; + display: inline-block; + margin-right: 7px; + + .btn { + display: block; + min-width: 175px; + } + } + } + + .btn { + margin-right: 8px; + margin-bottom: 13px; + min-width: 100px; + } +} + +.demo-button-groups { + .btn-group { + margin-right: 10px; + margin-bottom: 10px; + } +} + +.demo-button-toolbar { + .btn-toolbar { + float: left; + margin-right: 25px; + margin-bottom: 10px; + .btn-group { + margin-left: 5px; + } + } +} + +.demo-button-nesting { + > .btn-group { + margin-right: 15px; + } +} + +.demo-single-button-dropdowns { + > .btn-group { + margin-right: 10px; + } +} + +.demo-splite-button-dropdowns { + > .btn-group { + margin-right: 10px; + } +} + +.demo-dropup { + .dropup { + margin-right: 10px; + } +} + +/*********************************************************************** + + [2. CHECKBOX & RADIO ] + +***********************************************************************/ + +.demo-checkbox, +.demo-radio-button { + label { + min-width: 150px; + } +} +/*********************************************************************** + + [3. JQUERY KNOB-CHART ] + +***********************************************************************/ + +.demo-knob-chart { + div { + margin-right: 15px; + } +} +/*********************************************************************** + + [4. SWITCH ] + +***********************************************************************/ + +.demo-switch { + .switch { + display: inline-block; + min-width: 170px; + } + + .demo-switch-title { + min-width: 95px; + display: inline-block; + } +} +/*********************************************************************** + + [5. COLOR BOX ] + +***********************************************************************/ +.demo-color-box { + padding: 15px 0; + text-align: center; + margin-bottom: 20px; + @include border-radius(3px); + + .color-name { + font-size: 16px; + margin-bottom: 5px; + } + + .color-code, + .color-class-name { + font-size: 13px; + } +} +/*********************************************************************** + + [6. IMAGES ] + +***********************************************************************/ +.demo-image-copyright { + text-align: right; + font-style: italic; + font-size: 12px; + color: #777; + margin: 5px 0 10px 0; + + a { + font-weight: bold; + color: #555 !important; + } +} +/*********************************************************************** + + [7. TAGS INPUT ] + +***********************************************************************/ +.demo-tagsinput-area { + margin-bottom: 0px !important; + &.form-group .form-line:after { + border-bottom: 0px; + } +} +/*********************************************************************** + + [8 .GOOGLE MATERIAL DESIGN ICON ] + +***********************************************************************/ +.demo-icon-container { + .demo-google-material-icon { + margin-bottom: 5px; + text-align: left; + + .icon-name { + position: relative; + top: -8px; + left: 7px; + } + + .material-icons { + width: 24px; + } + } +} +/*********************************************************************** + + [9. PRELOADERS ] + +***********************************************************************/ +.demo-preloader { + .preloader { + margin-right: 10px; + } +} +/*********************************************************************** + + [10. ION RANGE SLIDER ] + +***********************************************************************/ +.irs-demo { + margin-bottom: 40px; + + .irs { + margin-top: 15px; + } +} +/*********************************************************************** + + [11. RIGHT SIDEBAR ] + +***********************************************************************/ +.right-sidebar { + .nav-tabs + .tab-content { + padding: 0; + } + + p { + margin: 20px 15px 15px 15px; + font-weight: bold; + text-align: center; + } + + #settings { + .setting-list { + list-style: none; + padding-left: 0; + margin-bottom: 20px; + + li { + padding: 15px; + position: relative; + border-top: 1px solid #eee; + + .switch { + position: absolute; + top: 15px; + right: 5px; + } + } + } + } + .progress { + -webkit-border-radius: 50px; + -moz-border-radius: 50px; + -ms-border-radius: 50px; + border-radius: 50px; + height: 7px; + margin: 10px 0px 0px 0px; + } +} +.right-sidebar .choose-theme li { + position: relative; + cursor: pointer; + display: inline-block; +} + +.demo-choose-skin, +.demo-choose-logoheader { + list-style: none; + padding-left: 0; + overflow-y: hidden; + + li { + padding: 10px 7px 4px 0px; + position: relative; + cursor: pointer; + float: left; + + &.actived { + &:after { + font-family: "Material Icons"; + position: absolute; + top: 10px; + right: 10px; + content: "\E876"; + font-size: 18px; + color: #ece6e6; + } + } + + div { + width: 24px; + height: 24px; + display: inline-block; + @include border-radius(3px); + } + + span { + position: relative; + bottom: 7px; + left: 5px; + } + } + .white-theme { + background-color: #fff; + } + .black-theme { + background-color: #3a3f51; + } + .purple-theme { + background-color: #813ae1; + } + .blue-theme { + background-color: #03a9f3; + } + .cyan-theme { + background-color: #13d1d3; + } + .green-theme { + background-color: #13b464; + } + .orange-theme { + background-color: #f06533; + } + + @each $key, $val in $colors { + .#{$key} { + background-color: $val; + } + } +} +.white-theme-border { + border: 1px solid #888888; +} + +.btn-sidebar-light, +.btn-theme-light { + background: 0 0 !important; + color: #888888 !important; + border: 1px solid #888888 !important; + margin: 10px; +} +//===================================================================================== diff --git a/MyOffice.SPA/src/assets/scss/common/_general.scss b/MyOffice.SPA/src/assets/scss/common/_general.scss new file mode 100644 index 0000000..80cfc33 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_general.scss @@ -0,0 +1,374 @@ +/* + * Document : _general.scss + * Author : RedStar Template + * Description: This scss file for all common style classes + */ + +body, +html { + @include transition(all 0.5s); + background-color: #f0eff3; + font-family: "Roboto", sans-serif; + font-size: 14px; +} + +button, +input, +select, +a { + outline: none !important; + font-size: 14px !important; + text-decoration: none; + + &:hover { + text-decoration: none; + } +} + +textarea { + font-size: 14px !important; +} + +ol, +ul, +dl { + padding-left: 0px; + list-style-type: none; +} + +.no-animate { + -o-transition-property: none !important; + -moz-transition-property: none !important; + -ms-transition-property: none !important; + -webkit-transition-property: none !important; + transition-property: none !important; + -o-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -webkit-transform: none !important; + transform: none !important; + -webkit-animation: none !important; + -moz-animation: none !important; + -o-animation: none !important; + -ms-animation: none !important; + animation: none !important; +} + +section { + &.content { + margin: 55px 27px 0 260px; + min-height: calc(100vh - 76px); + @include transition(0.5s); + .content-block { + padding: 25px 0px 0px 25px; + } + } +} + +.horizontal-layout { + section { + &.content { + margin: 170px 15px 0 15px; + float: left; + width: calc(100% - 30px); + } + } +} + +.pull-left { + float: left !important; +} + +.pull-right { + float: right !important; +} + +.msl-1 { + margin-left: 0.25rem !important; +} +.msl-2 { + margin-left: 0.5rem !important; +} +.msl-3 { + margin-left: 1rem !important; +} +.msl-4 { + margin-left: 1.5rem !important; +} +.msl-5 { + margin-left: 3rem !important; +} + +.msr-1 { + margin-right: 0.25rem !important; +} +.msr-2 { + margin-right: 0.5rem !important; +} +.msr-3 { + margin-right: 1rem !important; +} +.msr-4 { + margin-right: 1.5rem !important; +} +.msr-5 { + margin-right: 3rem !important; +} + +.psl-1 { + padding-left: 0.25rem !important; +} +.psl-2 { + padding-left: 0.5rem !important; +} +.psl-3 { + padding-left: 1rem !important; +} +.psl-4 { + padding-left: 1.5rem !important; +} +.psl-5 { + padding-left: 3rem !important; +} + +.psr-1 { + padding-right: 0.25rem !important; +} +.psr-2 { + padding-right: 0.5rem !important; +} +.psr-3 { + padding-right: 1rem !important; +} +.psr-4 { + padding-right: 1.5rem !important; +} +.psr-5 { + padding-right: 3rem !important; +} +.cursor-pointer { + cursor: pointer; +} + +.jqvmap-zoomin, +.jqvmap-zoomout { + width: 15px; + height: 15px; +} + +table { + .checkbox { + [type="checkbox"] + label { + margin: 0; + height: 20px; + padding-left: 20px; + vertical-align: middle; + } + } +} + +.loading-img-spin { + position: absolute; + top: 50%; + left: 50%; + width: 50px; + height: 50px; + margin: -60px 0 20px -20px; + -webkit-animation: spin 1.5s linear infinite; + -moz-animation: spin 1.5s linear infinite; + animation: spin 1.5s linear infinite; +} + +.shadow-style { + -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} + +.review-img { + padding-left: 20px; + padding-top: 5px; + width: 70px; + + img { + border-radius: 50%; + border: 2px solid #ffffff; + box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); + } +} + +.horizontal-layout { + .sidebar, + .nav-left-menu { + display: none; + } +} + +.bootstrap-notify-container { + max-width: 320px; + text-align: center; +} + +.map iframe { + width: 100%; +} + +.jqvmap-label { + position: absolute; + display: none; + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; + background: #eee; + color: black; + font-size: 14px; + font-family: sans-serif, Verdana; + padding: 10px; + pointer-events: none; +} + +.logo-white { + .navbar-toggle, + .bars:before, + .bars:after { + color: #000 !important; + } +} + +.logo-black { + .navbar-toggle, + .bars:before, + .bars:after { + color: #fff !important; + } +} + +.dark { + .sidemenu-collapse i { + color: #fff; + } + + .nav { + > li { + > a { + color: #fff; + } + } + } +} + +.light { + .sidemenu-collapse i { + color: #0d091d; + } + + .nav { + > li { + > a { + color: #0d091d; + } + } + } +} + +.border-apply { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2) !important; +} + +input::-webkit-input-placeholder { + font-size: 12px; + color: #adabab; +} + +input:-moz-placeholder { + /* Firefox 18- */ + font-size: 12px; + color: #adabab; +} + +input::-moz-placeholder { + /* Firefox 19+ */ + font-size: 12px; + color: #adabab; +} + +input:-ms-input-placeholder { + font-size: 12px; + color: #adabab; +} + +textarea::-webkit-input-placeholder { + font-size: 12px; + color: #adabab; +} + +textarea:-moz-placeholder { + /* Firefox 18- */ + font-size: 12px; + color: #adabab; +} + +textarea::-moz-placeholder { + /* Firefox 19+ */ + font-size: 12px; + color: #adabab; +} + +textarea:-ms-input-placeholder { + font-size: 12px; + color: #adabab; +} + +.profile-image img { + width: 100%; +} + +@media screen and (max-width: 1169px) { + .horizontal-layout { + .sidebar { + display: block; + } + + .top-sidebar { + display: none; + } + + section.content { + margin-top: 100px; + } + } +} + +.font-icon { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 20px; + padding: 10px; + transition: all 0.2s; + + .icon-preview { + font-size: 1.8rem; + margin-right: 10px; + line-height: 1; + color: #333439; + } +} + +.deshboard-echart-height { + height: 250px; +} +.pill-style { + font-size: 17px; + color: #a9a6a6; + padding-right: 5px; +} +.pill-timing { + width: 30%; +} + +@media screen and (min-width: 1400px) { + .boxed-layout .container { + width: 1370px; + max-width: 100%; + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_helpers.scss b/MyOffice.SPA/src/assets/scss/common/_helpers.scss new file mode 100644 index 0000000..12b1938 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_helpers.scss @@ -0,0 +1,164 @@ +@use "sass:math"; +@for $i from -25 through 25 { + .m-l-#{$i * 5} { + margin-left: #{$i * 5}px; + } + + .m-t-#{$i * 5} { + margin-top: #{$i * 5}px; + } + + .m-r-#{$i * 5} { + margin-right: #{$i * 5}px; + } + + .m-b-#{$i * 5} { + margin-bottom: #{$i * 5}px; + } +} + +.margin-0 { + margin: 0; +} + +@for $i from 0 through 25 { + .p-l-#{$i * 5} { + padding-left: #{$i * 5}px; + } + + .p-t-#{$i * 5} { + padding-top: #{$i * 5}px; + } + + .p-r-#{$i * 5} { + padding-right: #{$i * 5}px; + } + + .p-b-#{$i * 5} { + padding-bottom: #{$i * 5}px; + } +} +@for $i from 0 through 25 { + .margin-#{$i * 5} { + margin: #{$i * 5}px; + } + .padding-#{$i * 5} { + padding: #{$i * 5}px; + } +} + +.padding-0 { + padding: 0; +} + +@for $i from 5 through 49 { + .font-#{$i + 1} { + font-size: #{$i + 1}px; + } +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +.align-justify { + text-align: justify; +} + +.no-resize { + resize: none; +} + +.font-bold { + font-weight: bold; +} + +.font-italic { + font-style: italic; +} + +.font-underline { + text-decoration: underline; +} + +.font-line-through { + text-decoration: line-through; +} + +.font-overline { + text-decoration: overline; +} + +.block-header { + //margin-bottom: 15px; + + h2 { + margin: 0 !important; + color: #666 !important; + font-weight: normal; + font-size: 22px; + line-height: 46px; + + small { + display: block; + font-size: 12px; + margin-top: 8px; + color: #888; + + a { + font-weight: bold; + color: #777; + } + } + } +} + +@each $key, $val in $colors { + .bg-#{$key} { + background-color: $val !important; + color: #fff; + + .content { + .text, + .number { + color: #fff !important; + } + } + } +} + +@each $key, $val in $linear-colors { + .l-bg-#{$key} { + background: $val !important; + color: #fff; + + .content { + .text, + .number { + color: #fff !important; + } + } + } +} + +@each $key, $val in $colors { + .col-#{$key} { + color: $val !important; + } +} +@for $i from 0 through 100 { + .width-per-#{$i} { + width: round(percentage(math.div($i, 100))); + } + .tbl-col-width-per-#{$i} { + max-width: round(percentage(math.div($i, 100))); + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_media.scss b/MyOffice.SPA/src/assets/scss/common/_media.scss new file mode 100644 index 0000000..24868d0 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_media.scss @@ -0,0 +1,367 @@ +/* + * Document : _media.scss + * Author : RedStar Template + * Description: This scss file for media queries classes + */ +@media (max-width: 1169px) { + // .sidemenu-collapse { + // display: none !important; + // } + .ls-closed .sidebar { + margin-left: -300px; + } + section.content { + margin: 83px 15px 0 0px; + } + .search-box { + margin-left: 25px; + } + .side-closed.submenu-closed .navbar-header { + width: 260px !important; + .navbar-brand span { + display: inline-block !important; + } + } + nav.navbar { + width: calc(100% - 4rem) !important; + } + .navbar .collapse-menu-icon { + display: block !important; + } + .sidebar .sidemenu-collapse { + display: none !important; + } +} +@media (max-width: 800px) { + .responsive_table { + overflow-x: auto !important; + } + + .mat-table { + min-width: 800px; + } +} + +@media (max-width: 799px) { + .navbar { + .search-box { + margin-left: 20px; + } + // .nav > li > a { + // padding: 10px 10px; + // } + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + } + .navbar-toggle { + display: none; + } + .navbar-nav { + float: left; + margin: 0; + > li { + float: left; + > a { + padding-top: 15px; + padding-bottom: 15px; + } + } + } + + .container > { + .navbar-header { + margin-right: 0; + margin-left: 0; + } + .navbar-collapse { + margin-right: 0; + margin-left: 0; + } + } + .container-fluid > { + .navbar-header { + margin-right: 0; + margin-left: 0; + } + .navbar-collapse { + margin-right: 0; + margin-left: 0; + } + } + .navbar > { + .container .navbar-brand { + margin-left: -15px; + } + .container-fluid .navbar-brand { + margin-left: -15px; + } + } +} +.container > { + .navbar-header { + margin-right: -15px; + margin-left: -15px; + } + .navbar-collapse { + margin-right: -15px; + margin-left: -15px; + } +} +.container-fluid > { + .navbar-header { + margin-right: -15px; + margin-left: -15px; + border-right: 1px solid #f2f4f9; + } + .navbar-collapse { + margin-right: -15px; + margin-left: -15px; + } +} +@media (max-width: 767px) { + .navbar { + height: auto; + > .container, + > .container-fluid { + .navbar-brand { + margin-left: 30px; + width: 88%; + margin-right: 0; + } + } + + .navbar-toggle { + float: right; + } + + .nav-left-menu { + display: none; + } + + .navbar-right { + .fullscreen { + display: none; + } + } + + .navbar-header { + display: inline-block; + // margin-bottom: -25px; + width: calc(100% + 30px); + float: left; + } + + .nav { + > li { + display: inline-block; + > a { + // padding: 13px 15px 8px 15px; + &.js-right-sidebar { + padding: 9px 8px 8px 15px; + } + } + } + } + + .navbar-nav { + // margin-top: -10px; + margin-bottom: 1px; + margin-left: -7px; + + .open { + .dropdown-menu { + background-color: #fff; + position: absolute; + } + } + } + + .dropdown-menu { + margin-left: -50px; + } + + .navbar-collapse { + display: block; + } + } + .side-closed.submenu-closed .navbar-header { + width: calc(100% + 30px) !important; + } + .side-closed.submenu-closed .navbar-header .navbar-brand span { + display: inline-block !important; + } + .dt-buttons { + float: none !important; + text-align: center; + margin-bottom: 15px; + } + + .panel-switch-btn { + top: 12px; + right: 0 !important; + } + .rtl { + .navbar { + .navbar-toggle { + float: left; + margin-left: 15px; + margin-right: 0; + } + .navbar-header { + margin-bottom: 0px; + } + } + } + section.content, + body.ls-closed section.content { + margin-right: 0px; + margin-left: 0px; + } +} +@media (max-width: 600px) { + .navbar { + > .container, + > .container-fluid { + .navbar-brand { + /*width: 86%;*/ + } + } + .navbar-header { + margin-bottom: 0; + } + .navbar-toggle:before { + margin-top: 2px; + float: left; + } + // .nav-notification-icons { + // margin-top: 7px; + // } + } + .ls-closed .bars:after, + .ls-closed .bars:before { + margin-top: 2px; + } +} +@media (max-width: 500px) { + .navbar-nav { + &.nav { + .dropdown-menu { + width: 245px; + right: -80px; + &::before { + right: 95px; + } + &::after { + right: 96px; + } + } + .user_profile { + .dropdown-menu { + &::before { + right: 19px; + } + &::after { + right: 20px; + } + } + } + } + } + .navbar { + > .container, + > .container-fluid { + .navbar-brand { + margin-left: 28px; + width: 84%; + } + } + } + .breadcrumb-chart { + margin: 0 0 0 20px; + } + .search-box { + display: none; + } + full-calendar > div.fc-toolbar { + display: block !important; + } +} +@media (max-width: 420px) { + .navbar { + > .container, + > .container-fluid { + .navbar-brand { + width: 79%; + } + } + } + .btnAppList { + display: none; + } +} +@media (max-width: 350px) { + .navbar { + .nav { + > li { + > a { + padding: 13px 10px 8px 10px; + &.js-right-sidebar { + padding: 9px 10px 8px 10px; + } + } + } + .user_profile { + .dropdown-toggle { + padding: 7px 0px 9px 10px; + } + } + } + > .container-fluid { + .navbar-brand { + margin-left: 25px; + } + } + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .navbar { + > .container, + > .container-fluid { + .navbar-brand { + margin-left: 20px; + } + } + } +} + +@media (min-width: 992px) and (max-width: 1169px) { + .navbar { + > .container, + > .container-fluid { + .navbar-brand { + margin-left: 20px; + } + } + } +} + +@media (min-width: 1170px) and (max-width: 1999px) { +} + +@media (min-width: 1200px) { +} diff --git a/MyOffice.SPA/src/assets/scss/common/_mixins.scss b/MyOffice.SPA/src/assets/scss/common/_mixins.scss new file mode 100644 index 0000000..ba9a5d6 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_mixins.scss @@ -0,0 +1,70 @@ +@mixin border-radius($radius) { + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + -ms-border-radius: $radius; + border-radius: $radius; +} + +@mixin box-shadow($shadowinfo) { + -webkit-box-shadow: $shadowinfo; + -moz-box-shadow: $shadowinfo; + -ms-box-shadow: $shadowinfo; + box-shadow: $shadowinfo; +} +@mixin transform($transform) { + -moz-transform: $transform; + -ms-transform: $transform; + -o-transform: $transform; + -webkit-transform: $transform; + transform: $transform; +} + +@mixin transition($transition) { + -moz-transition: $transition; + -o-transition: $transition; + -webkit-transition: $transition; + transition: $transition; +} + +@mixin three-dots-overflow() { + white-space: nowrap; + -ms-text-overflow: ellipsis; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + overflow: hidden; +} + +@mixin navbar-link-color($textcolor, $navbarcolor, $opacity) { + .navbar-brand, + .navbar-brand:hover, + .navbar-brand:active, + .navbar-brand:focus { + color: $textcolor; + } + + .nav > li > a:hover, + .nav > li > a:focus, + .nav .open > a, + .nav .open > a:hover, + .nav .open > a:focus { + background-color: transparentize($navbarcolor, $opacity); + } + + .nav > li > a { + color: $textcolor; + } + + .bars { + float: left; + padding: 10px 20px; + font-size: 22px; + color: $textcolor; + margin-right: 10px; + margin-left: -10px; + margin-top: 4px; + } + + .bars:hover { + background-color: rgba(0, 0, 0, 0.08); + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_rtl.scss b/MyOffice.SPA/src/assets/scss/common/_rtl.scss new file mode 100644 index 0000000..a213c35 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_rtl.scss @@ -0,0 +1,816 @@ +.rtl { + direction: rtl; + text-align: right; + .navbar { + right: unset; + left: 0; + .navbar-toggle { + float: left; + margin-left: 15px; + margin-right: 0; + } + + .navbar-header { + margin-bottom: 0px; + } + .navbar-left { + padding-right: 0px; + } + } + + .navbar-nav { + &.navbar-right { + float: left !important; + margin-left: 0px; + margin-right: unset; + display: flex !important; + flex-direction: unset; + + .user_profile { + margin: 0px 0px 0px 6px; + .user_img { + border-radius: 50%; + } + } + + // .lang-dropdown { + // margin: 14px 10px 0px 20px; + // } + } + .dropdown-menu { + left: 0; + right: auto !important; + } + .nfc-dropdown { + left: 0; + right: auto !important; + } + .nfc-dropdown::before { + left: 19px; + right: unset; + } + .nfc-dropdown::after { + left: 20px; + right: unset; + } + > li .js-right-sidebar { + margin-left: 0px; + margin-right: 0; + } + .user_profile .user_dw_menu li i { + float: right; + padding-left: 5px; + } + .app-dropdown { + left: unset !important; + right: 0 !important; + + &::before { + right: 19px !important; + left: unset !important; + } + &::after { + right: 20px !important; + left: unset !important; + } + } + } + section { + &.content { + margin: 55px 260px 0px 27px; + min-height: calc(100vh - 76px); + transition: 0.5s; + .content-block { + padding: 25px 25px 0px 0px; + } + } + } + .breadcrumb-main { + .page-title { + border-right: none; + padding-right: 0px; + &:after { + display: none; + } + &:before { + content: "\f104"; + font-family: "Font Awesome 5 Free"; + font-size: 18px; + font-weight: 900; + padding: 0px 5px; + } + } + .breadcrumb-list { + float: left; + } + li a .fa-home { + float: right; + padding-right: 10px; + padding-left: unset; + } + .breadcrumb-item + .breadcrumb-item { + padding-right: 5px; + padding-left: unset; + &:after { + content: "\f104"; + font-family: "Font Awesome 5 Free"; + font-size: 14px; + font-weight: 900; + padding-left: 0.5rem; + } + &:before { + display: none; + } + } + } + .sidebar { + right: 0; + .menu { + clear: right; + .list { + .menu-toggle { + &:after { + left: 17px; + right: unset; + } + &:before { + content: "\f053"; + left: 17px; + right: unset; + } + } + a { + padding: 9px 9px 9px 9px; + span { + margin: 7px 12px 7px 0; + } + .mat-button-wrapper { + margin: 7px 0 7px 12px; + } + .material-icons-two-tone { + float: right; + } + .sidebarIcon { + float: right; + margin-right: 5px; + } + } + .ml-menu { + li a { + padding-right: 55px; + padding-top: 7px; + padding-bottom: 7px; + margin-right: 0; + } + .ml-menu-2 { + padding-right: 55px; + li a { + padding-right: 20px; + padding-top: 4px; + padding-bottom: 4px; + } + } + .ml-menu-3 { + padding-right: 20px; + li a { + padding-right: 20px; + padding-top: 4px; + padding-bottom: 4px; + } + } + } + .ml-sub-menu:after, + .ml-sub-menu:before { + left: 17px; + right: unset; + } + .header { + margin: 15px 35px 5px 0 !important; + } + .ml-sub-menu2:after, + .ml-sub-menu2:before { + left: 17px; + right: unset; + } + } + } + .sidebar-badge { + left: 35px; + right: unset; + } + .nav { + padding-right: 0px; + .logo { + margin-left: auto; + margin-right: unset; + } + } + } + .sidemenu-reorder { + float: right; + } + .form-check { + .form-check-label { + padding-right: 30px; + } + .form-check-sign { + left: unset; + right: 0px; + padding-right: 0px; + } + } + .mat-radio-button ~ .mat-radio-button { + margin-right: 16px; + } + @media (min-width: 768px) { + .navbar-header { + float: right; + } + } + + .msl-1 { + margin-right: 0.25rem !important; + margin-left: unset !important; + } + .msl-2 { + margin-right: 0.5rem !important; + margin-left: unset !important; + } + .msl-3 { + margin-right: 1rem !important; + margin-left: unset !important; + } + .msl-4 { + margin-right: 1.5rem !important; + margin-left: unset !important; + } + .msl-5 { + margin-right: 3rem !important; + margin-left: unset !important; + } + + .msr-1 { + margin-left: 0.25rem !important; + margin-right: unset !important; + } + .msr-2 { + margin-left: 0.5rem !important; + margin-right: unset !important; + } + .msr-3 { + margin-left: 1rem !important; + margin-right: unset !important; + } + .msr-4 { + margin-left: 1.5rem !important; + margin-right: unset !important; + } + .msr-5 { + margin-left: 3rem !important; + margin-right: unset !important; + } + + .psl-1 { + padding-right: 0.25rem !important; + padding-left: unset !important; + } + .psl-2 { + padding-right: 0.5rem !important; + padding-left: unset !important; + } + .psl-3 { + padding-right: 1rem !important; + padding-left: unset !important; + } + .psl-4 { + padding-right: 1.5rem !important; + padding-left: unset !important; + } + .psl-5 { + padding-right: 3rem !important; + padding-left: unset !important; + } + + .psr-1 { + padding-left: 0.25rem !important; + padding-right: unset !important; + } + .psr-2 { + padding-left: 0.5rem !important; + padding-right: unset !important; + } + .psr-3 { + padding-left: 1rem !important; + padding-right: unset !important; + } + .psr-4 { + padding-left: 1.5rem !important; + padding-right: unset !important; + } + .psr-5 { + padding-left: 3rem !important; + padding-right: unset !important; + } + + &.side-closed { + &.side-closed-hover { + section.content { + margin-right: 260px; + margin-left: 27px; + } + .sidebar .menu .list { + .ml-menu { + li a { + padding-right: 55px; + :before { + padding-right: 40px; + } + } + .ml-menu-2 { + padding-right: 55px; + li a { + padding-right: 20px; + } + } + } + li { + i { + float: right; + line-height: 2rem; + } + span { + display: block; + float: right; + } + .sidebarIcon { + float: right; + } + } + li .menu-toggle:before { + content: "\f053"; + } + } + } + section.content { + margin-right: 60px; + margin-left: 27px; + } + .navbar-brand { + margin-right: 5px; + } + a .sidebarIcon { + margin-right: 0px !important; + } + } + .card .header .header-dropdown { + left: 0px; + right: unset; + } + .dropdown-menu ul.menu { + .msg-user { + float: right; + } + .menu-info { + right: 10px; + float: right; + text-align: right; + .menu-desc .material-icons { + float: right; + margin-left: 3px; + } + } + li a { + float: right; + } + } + .right-sidebar { + &.open { + left: 0; + right: unset; + } + .rightSidebarClose { + right: 5px; + left: unset; + } + } + .collapse-menu-icon { + float: right !important; + } + .list-unstyled { + padding-right: 0; + } + .review-img { + padding-left: 0 !important; + padding-right: 20px; + } + .progress-percent { + float: left !important; + } + .todo-actionlist { + left: 0; + } + .ngxTableHeader { + .header-buttons-left { + right: -15px; + li { + margin-right: 10px; + } + .search-icon { + left: 0; + } + } + .header-buttons { + left: 35px; + right: unset; + } + } + #mail-nav { + #mail-folders { + padding-right: 0; + .badge { + float: left; + } + } + #mail-labels { + padding-right: 0; + } + #online-offline { + padding-right: 0; + } + } + .chat { + .chat-header { + img { + float: right; + } + .chat-about { + float: right; + padding-right: 10px; + } + } + .chat-history .my-message:after { + right: 7%; + } + } + .owl-carousel { + direction: ltr; + .owl-item { + direction: rtl; + } + } + .bx-wrapper { + direction: ltr; + } + .lg-outer { + direction: ltr; + } + .rightSetting p { + text-align: right; + } + .sidebar .menu .list { + padding-right: 0; + .active .ml-menu { + // padding-right: 30px; + margin-right: 0; + padding-left: 0px; + } + .ml-menu { + padding-right: 0; + li.active .ml-menu:before { + right: 30px; + left: unset; + content: "\f104"; + font-family: "Font Awesome 5 Free"; + } + .ml-menu-2 li.active a:not(.menu-toggle):before { + right: 0px; + left: unset; + } + } + .ml-menu-3 li.active .ml-menu3:before { + content: "\f104"; + } + } + .notice-board .notice-body { + padding: 0 10px 5px 0; + } + .ngx-datatable.material .datatable-footer .datatable-pager { + text-align: left; + } + .people-list img { + float: right; + } + .cd-timeline-img img { + right: 10%; + } + .navbar-header .bars:before { + right: 10px; + left: unset; + } + .navbar-brand { + margin-right: 60px; + span { + padding-left: unset; + padding-right: 10px; + } + } + .dropdown-menu ul.menu { + padding-right: 0; + } + .navbar-nav .user_profile .user_dw_menu { + padding-right: 0; + text-align: right; + } + .progress-list .status { + left: 0; + right: unset; + } + .to-do-list { + padding-right: 0px; + } + .materialTableHeader { + .tbl-export-btn { + left: 20px; + right: unset; + float: left; + } + .header-buttons-left { + padding-right: 0px; + .tbl-title { + margin-right: 20px; + margin-left: 0px; + } + + .tbl-search-box { + margin-right: 10px; + } + .search-icon { + padding-right: 10px; + padding-left: unset; + } + + input.search-field { + padding: 8px 50px 8px 8px; + } + } + } + #mail-nav #mail-labels li { + float: right; + } + #mail-nav #online-offline .material-icons { + padding: 0px 5px 2px 5px; + } + .chat-upload { + float: left; + } + .modal-header .close { + margin-left: 0px; + } + .modal-close-button { + left: 10px; + right: unset; + } + .owl-dt-container { + direction: rtl; + } + + .owl-dt-control-arrow-button svg { + transform: rotate(180deg); + } + .card-statistic-4 { + .card-spacing { + padding-right: 15px; + padding-top: 1rem; + } + } + .card-statistic-3 .card-icon { + left: -5px; + right: unset; + margin-left: 0px; + } + .card-statistic-2 .card-right { + float: left; + margin: 15px 0px 15px 15px; + } + .post-user { + .avtar-img { + float: right; + margin-left: 15px; + margin-right: unset; + } + } + .info-box6 { + .count-numbers { + left: 35px; + right: unset; + } + .count-name { + left: 35px; + right: unset; + } + } + + .feedBody { + border-right: 1px solid #d6d6d6; + border-left: unset; + margin-right: 30px; + margin-left: unset; + padding-right: 0px; + padding-left: unset; + li { + padding-left: unset; + padding-right: 30px; + .feed-user-img { + right: -20px; + left: unset; + } + } + } + .sl-item { + border-right: 1px solid #13b1e0; + border-left: unset; + padding-right: 15px; + padding-left: unset; + &::before { + right: -6.5px; + left: unset; + } + } + .task-module .header-button { + margin-right: auto; + margin-left: unset; + } + .task-list { + .task-low { + margin-right: auto; + margin-left: unset; + } + + .task-high { + margin-right: auto; + margin-left: unset; + } + + .task-normal { + margin-right: auto; + margin-left: unset; + } + } + .task-container .header .header-close { + left: 15px; + right: unset; + } + .form-check .form-check-sign .check:before { + margin-right: 10px; + margin-left: unset; + } + .top-sell { + .product-title { + margin-right: 20px; + } + .product-price { + margin-right: 20px; + } + .sell-price { + float: left; + } + } + .doc-file-type .media-cta { + margin-left: 15px; + } + .lang-dropdown { + margin-left: 0px; + .country-name { + margin-right: 5px; + } + } + .lang-item .lang-item-list { + text-align: right; + line-height: 15px; + display: block; + .flag-img { + float: right; + margin: 5px 0px 0px 5px; + } + } + &.ls-closed .sidebar { + margin-right: -300px; + margin-left: unset; + } + &.ls-closed section.content { + margin-right: 15px; + margin-left: 15px; + } + &.overlay-open.ls-closed .sidebar { + margin-right: 0; + margin-left: unset; + } + + .btn-space { + margin-left: 10px !important; + } + .ms-auto { + margin-right: auto !important; + margin-left: unset !important; + } + .float-end { + float: left !important; + } + .float-start { + float: right !important; + } + .text-end { + text-align: left !important; + } + .text-start { + text-align: right !important; + } + .settingSidebar { + left: -280px; + right: unset; + &.showSettingPanel { + left: 0; + right: unset; + } + + .settingPanelToggle { + background: #6777ef; + padding: 10px 15px; + color: #fff; + position: absolute; + top: 30%; + left: 280px; + width: 40px; + border-radius: 0px 10px 10px 0px; + } + } + .hiddenradio { + padding: 0px 0px 0px 20px; + margin: 10px 10px 0px 20px; + } + + .card-bnner { + img { + transform: scaleX(-1); + } + } + .user_img { + margin: 0px 0px 0px 5px; + } + &.side-closed .sidebar .menu .list li a { + padding-right: 9px; + } + + .nfc-menu { + right: auto; + left: unset; + .nfc-dropdown .menu { + .msg-user { + float: right; + } + .menu-info { + margin-right: 20px; + float: right; + text-align: right; + .menu-desc .material-icons { + float: right; + margin-left: 3px; + } + } + li a { + float: right; + } + } + } + + .mat-menu-item .user-menu-icons { + float: right; + } + .profile-menu .menu .user_dw_menu .mdc-list-item { + display: block; + .user-menu-icons { + float: right; + } + } + .order-list li + li { + margin-left: unset; + margin-right: -14px; + } + .info-card { + text-align: left; + } + .mdc-linear-progress__bar-inner { + left: 0px; + } + .ng-scrollbar-wrapper { + direction: rtl; + } +} + +@media (max-width: 1169px) { + .rtl { + .sidemenu-collapse { + display: none !important; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/common/_variables.scss b/MyOffice.SPA/src/assets/scss/common/_variables.scss new file mode 100644 index 0000000..3a7718c --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/common/_variables.scss @@ -0,0 +1,34 @@ +$colors: ( + "red": #f44336, + "pink": #e91e63, + "purple": #6f42c1, + "indigo": #3f51b5, + "blue": #2196f3, + "cyan": #00bcd4, + "teal": #009688, + "green": #4caf50, + "yellow": #ffe821, + "orange": #fd7e14, + "deep-orange": #ff5722, + "brown": #795548, + "grey": #9e9e9e, + "black": #000000, + "white": #ffffff, +) !default; + +$linear-colors: ( + "green": linear-gradient(45deg, #9ce89d, #cdfa7e), + "orange": linear-gradient(135deg, #ffc480, #ff763b), + "cyan": linear-gradient(45deg, #72c2ff, #86f0ff), + "red": linear-gradient(316deg, #fc5286, #fbaaa2), + "purple": linear-gradient(230deg, #759bff, #843cf6), + "purple-dark": linear-gradient(45deg, #a52dd8, #e29bf1), + "card1": linear-gradient(to left, #3a7bd5, #3a6073), + "card2": linear-gradient(to right, #c33764, #1d2671), + "card3": linear-gradient(to left, #134e5e, #71b280), + "card4": linear-gradient(to left, #d38312, #a83279), +); + +//Fonts Family +$navbar-font-family: "Roboto", sans-serif; +$sidebar-font-family: "Roboto", sans-serif; diff --git a/MyOffice.SPA/src/assets/scss/components/_breadcrumbs.scss b/MyOffice.SPA/src/assets/scss/components/_breadcrumbs.scss new file mode 100644 index 0000000..4721d45 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_breadcrumbs.scss @@ -0,0 +1,159 @@ +/* + * Document : _breadscrumbs.scss + * Author : RedStar Template + * Description: This scss file for breadscrumbs style classes + */ + +.breadcrumb-main { + // background-color: transparent; + // font-size: 15px; + // align-items: center; + // margin: 3rem 0 3rem; + // padding: 0px; + // color: #555; + + // &:before { + // content: ""; + // margin: 0; + // } + + .breadcrumb-title { + background-color: transparent; + font-size: 15px; + align-items: center; + margin: 30px 0px; + padding: 0px; + color: #555; + } + .breadcrumb-list { + background-color: transparent; + font-size: 15px; + align-items: center; + margin: 32px 0px; + float: right; + color: #555; + padding: 0px 5px; + } + + li { + display: inline-block; + a { + color: #444; + text-decoration: none; + + .fa-home { + font-size: 17px; + position: relative; + top: 0; + float: left; + padding-left: 10px; + color: #5798f7; + } + } + + .material-icons { + font-size: 18px; + position: relative; + top: 4px; + float: none; + } + &.active { + color: #444444; + } + } + + > li + li:before { + // content: "/" !important; + // color: #444444; + // padding-left: 5px; + } + .page-title { + // border-right: 1px solid #c5c5c5; + // padding-right: 10px; + font-size: 20px; + font-weight: 500; + color: #444444; + margin-bottom: 0px; + // &:after { + // content: "\f105"; + // font-family: "Font Awesome 5 Free"; + // font-size: 18px; + // font-weight: 900; + // padding: 0px 5px; + // } + i { + padding: 0px 5px; + &:before { + color: #717883; + font-size: 20px; + } + } + } + .breadcrumb-item + .breadcrumb-item { + padding-left: 5px; + &:before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 14px; + display: block; + font-weight: 900; + padding-right: 0.5rem; + } + } +} + +@each $key, $val in $colors { + .breadcrumb-col-#{$key} { + li { + a { + color: $val !important; + font-weight: bold; + } + } + } + + .breadcrumb-bg-#{$key} { + background-color: $val !important; + + li { + a { + color: #fff; + font-weight: bold; + + .material-icons { + padding-bottom: 8px; + } + } + + color: #fff !important; + } + + li + li:before { + color: #fff; + } + } +} +.breadcrumb-style { + border-radius: 30px; + /*background: #ffffff; + padding-left: 20px !important;*/ +} +.breadcrumb-chart { + display: inline-block; + .chart-info p { + font-size: 13px; + } +} +.breadcrumb-icon { + vertical-align: top; + height: 18px !important; + width: 18px !important; + margin: 0px 3px; + .feather { + width: 18px; + height: 18px; + color: #2c323f; + position: relative; + float: left; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_checkboxradio.scss b/MyOffice.SPA/src/assets/scss/components/_checkboxradio.scss new file mode 100644 index 0000000..55fbea2 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_checkboxradio.scss @@ -0,0 +1,139 @@ +/* + * Document : _checkboxradio.scss + * Author : RedStar Template + * Description: This scss file for checkbox and radio button style classes + */ +[type="checkbox"] { + +label { + padding-left: 26px; + height: 25px; + line-height: 21px; + font-size: 13px; + font-weight: normal; + vertical-align: middle; + } + + &:checked { + +label { + &:before { + top: -4px; + left: -2px; + width: 11px; + height: 19px; + } + } + } + + @each $key, + $val in $colors { + &:checked.chk-col-#{$key} { + +label { + &:before { + border-right: 2px solid $val; + border-bottom: 2px solid $val; + } + } + } + } +} + +@each $key, +$val in $colors { + :checked.chk-col-#{$key} { + +span { + &:after { + color: $val; + } + } + } +} + +[type="checkbox"].filled-in { + &:checked { + +label { + &:after { + top: 0; + width: 20px; + height: 20px; + border: 2px solid #26a69a; + background-color: #26a69a; + z-index: 0; + } + + &:before { + border-right: 2px solid #fff !important; + border-bottom: 2px solid #fff !important; + } + } + } + + @each $key, + $val in $colors { + &:checked.chk-col-#{$key} { + +label { + &:after { + border: 2px solid $val; + background-color: $val; + } + } + } + } +} + +[type="radio"] { + &:not(:checked) { + +label { + padding-left: 26px; + height: 25px; + line-height: 25px; + font-size: 13px; + font-weight: normal; + } + } + + &:checked { + +label { + padding-left: 26px; + height: 25px; + line-height: 25px; + font-size: 13px; + font-weight: normal; + } + } + + +label { + vertical-align: middle; + } +} + +@each $key, +$val in $colors { + [type="radio"].radio-col-#{$key} { + &:checked { + +label { + &:after { + background-color: $val; + border-color: $val; + } + } + } + } +} + +@each $key, +$val in $colors { + [type="radio"].with-gap.radio-col-#{$key} { + &:checked { + +label { + &:before { + border: 2px solid $val; + } + + &:after { + background-color: $val; + border: 2px solid $val; + } + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_dropdownmenu.scss b/MyOffice.SPA/src/assets/scss/components/_dropdownmenu.scss new file mode 100644 index 0000000..4f21eff --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_dropdownmenu.scss @@ -0,0 +1,338 @@ +/* + * Document : _dropdownmenu.scss + * Author : RedStar Template + * Description: This scss file for dropdown menu style classes + */ +.dropdown-menu { + @include border-radius(0); + margin-top: -35px !important; + margin-left: -15px; + -webkit-box-shadow: 0 5px 15px 2px rgba(64, 70, 74, 0.2) !important; + box-shadow: 0 5px 15px 2px rgba(64, 70, 74, 0.2) !important; + border-radius: 0px; + border: none; + padding: 0px; + + .divider { + margin: 5px 0; + } + + .header { + font-size: 13px; + font-weight: bold; + width: 100%; + border-bottom: 1px solid #eee; + text-align: center; + padding: 4px 0 6px 0; + } + + .footer { + a { + text-align: center; + border-top: 1px solid #eee; + padding: 10px 0 5px 0; + font-size: 13px; + margin-bottom: -5px; + color: #ff5e00; + font-weight: 500; + + &:hover { + background-color: transparent; + } + } + } + + > li { + > a { + padding: 7px 18px; + color: #666; + @include transition(all 0.5s); + font-size: 14px; + line-height: 25px; + display: block; + + &:hover { + background-color: rgba(0, 0, 0, 0.075); + } + + i.material-icons { + float: left; + margin-right: 7px; + margin-top: 2px; + font-size: 20px; + } + } + } +} + +.app-dropdown { + width: 350px; + max-width: 350px !important; + .nfc-header { + padding: 20px; + background-color: #7366ff; + border-radius: 5px 5px 0px 0px; + h5 { + color: #fff; + } + } + .app-icons { + display: block; + border-radius: 3px; + line-height: 34px; + text-align: center; + padding: 15px 0 9px; + border: 1px solid transparent; + color: black; + font-weight: 500; + &:hover { + background-color: #eef4fd; + } + } + .mat-mdc-menu-content { + padding: 0px; + } +} + +.dropdown-content li > a, +.dropdown-content li > span { + font-size: 13px; + color: #636262; +} + +.dropdown-animated { + -webkit-animation-duration: 0.3s !important; + -moz-animation-duration: 0.3s !important; + -o-animation-duration: 0.3s !important; + animation-duration: 0.3s !important; +} + +.dropdown-menu.pull-right.show { + position: absolute !important; + left: auto !important; + right: 0 !important; + top: 50px !important; + transform: none !important; +} +.nfc-menu { + transform-origin: left top; + width: 325px; + max-width: 100vw !important; + right: 10px; + left: auto; + position: absolute !important; + top: 0; + padding: 0; + border-radius: 5px; + .nfc-dropdown { + .menu { + padding-left: 0; + button { + padding: 11px 11px; + text-decoration: none; + @include transition(0.5s); + float: left; + width: 100%; + height: 65px; + // border-bottom: 1px solid #eee; + margin-bottom: 2px; + + &:hover { + background-color: #eef4fd; + } + .mdc-list-item__primary-text { + width: 100%; + } + } + + &.tasks { + h4 { + color: #333; + font-size: 13px; + margin: 0 0 8px 0; + + small { + float: right; + margin-top: 6px; + } + } + + .progress { + height: 7px; + margin-bottom: 7px; + } + } + + .icon-circle { + width: 36px; + height: 36px; + @include border-radius(50%); + color: #fff; + text-align: center; + display: inline-block; + float: left; + + i { + font-size: 18px; + line-height: 36px !important; + } + } + + .msg-user { + width: 44px; + height: 44px; + @include border-radius(50%); + color: #fff; + text-align: center; + display: inline-block; + vertical-align: top; + float: left; + + img { + float: left; + } + } + + li { + &:last-child { + border-bottom: none; + } + } + + .menu-info { + display: inline-block; + position: relative; + top: 3px; + left: 10px; + float: left; + width: calc(100% - 75px); + + h4, + .menu-title { + margin: 0; + font-size: 14px; + color: #121212; + float: left; + width: 100%; + line-height: 1; + } + + p, + .menu-desc { + margin: 0; + font-size: 11px; + color: rgba(0, 0, 0, 0.54); + float: left; + width: 100%; + line-height: 20px; + + .material-icons { + font-size: 13px; + color: rgba(0, 0, 0, 0.54); + position: relative; + top: 3px; + float: left; + margin-right: 3px; + height: 20px; + } + } + } + .nfc-close { + display: flex; + height: 40px; + line-height: 40px; + .feather { + color: #747474; + height: 12px !important; + width: 12px !important; + } + } + .nfc-type-icon { + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + &.nfc-green { + filter: invert(77%) sepia(4%) saturate(5247%) hue-rotate(85deg) + brightness(88%) contrast(95%); + } + &.nfc-blue { + filter: invert(67%) sepia(95%) saturate(5173%) hue-rotate(214deg) + brightness(101%) contrast(101%); + } + &.nfc-orange { + filter: invert(69%) sepia(81%) saturate(523%) hue-rotate(353deg) + brightness(101%) contrast(102%); + } + &.nfc-purple { + filter: invert(19%) sepia(98%) saturate(7474%) hue-rotate(284deg) + brightness(105%) contrast(117%); + } + &.nfc-red { + filter: invert(24%) sepia(94%) saturate(7102%) hue-rotate(356deg) + brightness(103%) contrast(100%); + } + } + .msg-unread { + background-color: #f5f9ff; + border-bottom: 1px solid #eee; + } + .msg-read { + border-bottom: 1px solid #eee; + } + } + } + .mat-menu-content { + padding-top: 0px !important; + padding-bottom: 0px !important; + } + .nfc-header { + padding: 20px; + background-color: #7366ff; + border-radius: 5px 5px 0px 0px; + display: flex; + h5 { + color: #fff; + } + } + + .nfc-mark-as-read { + display: block; + text-align: right; + color: #ffffff; + float: right; + width: 100%; + font-size: 12px !important; + } + .nfc-footer { + line-height: 50px; + cursor: pointer; + text-align: center; + border-top: 1px solid rgba(0, 0, 0, 0.15); + + .nfc-read-all { + color: rgb(106 106 106); + } + } + .mat-mdc-menu-content { + padding: 0px; + } +} +.profile-menu { + width: 200px; + max-width: 200px !important; + right: 10px; + + .user-menu-icons .feather { + height: 18px !important; + width: 18px !important; + vertical-align: middle; + } + .mat-mdc-menu-content { + padding: 0px; + } +} +.lang-item-menu .mat-mdc-menu-content { + padding: 0px; +} diff --git a/MyOffice.SPA/src/assets/scss/components/_feed.scss b/MyOffice.SPA/src/assets/scss/components/_feed.scss new file mode 100644 index 0000000..cdec4ea --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_feed.scss @@ -0,0 +1,189 @@ +/* + * Document : _feed.scss + * Author : RedStar Template + * Description: This scss file for feed style classes + */ +.feedBody { + border-left: 1px solid #d6d6d6; + margin-left: 30px; + padding-top: 10px; + padding-left: 0px; +} + +.col-auto { + padding-left: 15px; + padding-right: 15px; +} + +.feedLblStyle { + font-weight: bold; + padding: 0px 7px 0px 7px; + border-radius: 10px; +} + +.lblFileStyle { + color: red; + border: 1px solid red; +} + +.lblTaskStyle { + color: #2ed8b6; + border: 1px solid #2ed8b6; +} + +.lblCommentStyle { + color: #4099ff; + border: 1px solid #4099ff; +} + +.lblReplyStyle { + color: #f15317; + border: 1px solid #f15317; +} + +.feedBody li { + position: relative; + padding-left: 30px; + margin-bottom: 25px; +} + +.feedBody li .feed-user-img { + position: absolute; + left: -20px; + top: -10px; +} + +.feedBody li .feed-user-img img { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.feedBody li.active-feed .feed-user-img:after { + border-color: #2ed8b6; +} + +.feedBody li .feed-user-img:after { + content: ""; + position: absolute; + top: 3px; + right: 3px; + border: 3px solid transparent; + border-radius: 50%; +} + +.feedBody li h6 { + line-height: 1.5; + cursor: pointer; +} + +.text-muted { + color: #96a2b4 !important; + margin-bottom: 10px; +} + +.img-100 { + width: 100px; +} + +.feedBody li h6 { + line-height: 1.5; + cursor: pointer; +} + +.feedBody li.active-feed .feed-user-img:after { + border-color: #2ed8b6; +} + +.sl-item { + border-left: 1px solid #13b1e0; + padding-bottom: 1px; + padding-left: 15px; + position: relative; + + &:last-child::after { + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 100%; + bottom: 0; + content: ""; + height: 6px; + left: -3px; + position: absolute; + width: 6px; + } + + .sl-content { + i { + font-size: 12px; + } + + small { + position: relative; + top: -4px; + } + + p { + padding-bottom: 4px; + position: relative; + } + } +} + +.sl-item::before { + background-color: #13b1e0; + border-radius: 100%; + content: ""; + height: 12px; + left: -6.5px; + position: absolute; + top: 0; + width: 12px; +} + +.sl-primary { + border-left-color: #2196f3; + + &:last-child::after { + background-color: #2196f3; + } +} + +.sl-primary::before { + background-color: #2196f3; +} + +.sl-danger { + border-left-color: #f44336; + + &:last-child::after { + background-color: #f44336; + } +} + +.sl-danger::before { + background-color: #f44336; +} + +.sl-success { + border-left-color: #4caf50; + + &:last-child::after { + background-color: #4caf50; + } +} + +.sl-success::before { + background-color: #4caf50; +} + +.sl-warning { + border-left-color: #ff5722; + + &:last-child::after { + background-color: #ff5722; + } +} + +.sl-warning::before { + background-color: #ff5722; +} diff --git a/MyOffice.SPA/src/assets/scss/components/_formcomponents.scss b/MyOffice.SPA/src/assets/scss/components/_formcomponents.scss new file mode 100644 index 0000000..365fb1d --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_formcomponents.scss @@ -0,0 +1,59 @@ +.example-form { + min-width: 150px; + max-width: 500px; + width: 100%; +} + +.example-full-width { + width: 100%; +} +.example-h2 { + margin: 10px; +} + +.example-section { + display: flex; + align-content: center; + align-items: center; + height: 60px; +} + +.example-margin { + margin: 0 10px; +} + +.example-radio-group { + display: flex; + flex-direction: column; + margin: 15px 0; +} + +.example-radio-button { + margin: 5px; +} + +.advance-validation .error-msg { + margin-top: -15px; +} +.mat-radio-button ~ .mat-radio-button { + margin-left: 16px; +} +.mat-radio-label { + margin-bottom: 0px; +} +.mat-datepicker-content-touch .mat-calendar { + width: 50vh !important; + height: 57vh !important; +} +.mat-datepicker-toggle-default-icon { + width: 1.4em !important; +} +.mat-form-field { + .date-icon { + cursor: pointer; + } +} +.mat-form-field-prefix, +.mat-form-field-suffix { + color: rgba(0, 0, 0, 0.54); +} diff --git a/MyOffice.SPA/src/assets/scss/components/_infobox.scss b/MyOffice.SPA/src/assets/scss/components/_infobox.scss new file mode 100644 index 0000000..188ac5e --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_infobox.scss @@ -0,0 +1,690 @@ +/* + * Document : _infobox.scss + * Author : RedStar Template + * Description: This scss file for info box style classes + */ +.info-box { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + height: 80px; + display: flex; + cursor: default; + background-color: #fff; + position: relative; + overflow: hidden; + margin-bottom: 30px; + + .icon { + display: inline-block; + text-align: center; + background-color: rgba(0, 0, 0, 0.12); + width: 80px; + + i { + color: #fff; + font-size: 50px; + line-height: 80px; + } + + .chart.chart-bar { + height: 100%; + line-height: 100px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-pie { + height: 100%; + line-height: 123px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-line { + height: 100%; + line-height: 115px; + + canvas { + vertical-align: baseline !important; + } + } + } + + .content { + display: inline-block; + padding: 7px 10px; + + .text { + font-size: 13px; + margin-top: 11px; + color: #555; + } + + .number { + font-weight: normal; + font-size: 26px; + margin-top: -4px; + color: #555; + } + } +} + +.info-box.hover-zoom-effect { + .icon { + overflow: hidden; + + i { + @include transition(all 0.3s ease); + } + } + + &:hover { + .icon { + i { + opacity: 0.4; + @include transform(rotate(-32deg) scale(1.4)); + } + } + } +} + +.info-box.hover-expand-effect { + &:after { + background-color: rgba(0, 0, 0, 0.05); + content: "."; + position: absolute; + left: 80px; + top: 0; + width: 0; + height: 100%; + color: transparent; + @include transition(all 0.95s); + } + + &:hover { + &:after { + width: 100%; + } + } +} + +.info-box-2 { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + height: 80px; + display: flex; + cursor: default; + background-color: #fff; + position: relative; + overflow: hidden; + margin-bottom: 30px; + + .icon { + display: inline-block; + text-align: center; + width: 80px; + + i { + color: #fff; + font-size: 50px; + line-height: 80px; + } + } + + .chart.chart-bar { + height: 100%; + line-height: 105px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-pie { + height: 100%; + line-height: 123px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-line { + height: 100%; + line-height: 115px; + + canvas { + vertical-align: baseline !important; + } + } + + .content { + display: inline-block; + padding: 7px 10px; + + .text { + font-size: 13px; + margin-top: 11px; + color: #555; + } + + .number { + font-weight: normal; + font-size: 26px; + margin-top: -4px; + color: #555; + } + } +} + +.info-box-2.hover-zoom-effect { + .icon { + overflow: hidden; + + i { + @include transition(all 0.3s ease); + } + } + + &:hover { + .icon { + i { + opacity: 0.4; + @include transform(rotate(-32deg) scale(1.4)); + } + } + } +} + +.info-box-2.hover-expand-effect { + &:after { + background-color: rgba(0, 0, 0, 0.05); + content: "."; + position: absolute; + left: 0; + top: 0; + width: 0; + height: 100%; + color: transparent; + @include transition(all 0.95s); + } + + &:hover { + &:after { + width: 100%; + } + } +} + +.info-box-3 { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + height: 80px; + display: flex; + cursor: default; + background-color: #fff; + position: relative; + overflow: hidden; + margin-bottom: 30px; + + .icon { + position: absolute; + right: 10px; + bottom: 2px; + text-align: center; + + i { + color: rgba(0, 0, 0, 0.15); + font-size: 60px; + } + } + + .chart { + margin-right: 5px; + } + + .chart.chart-bar { + height: 100%; + line-height: 50px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-pie { + height: 100%; + line-height: 34px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-line { + height: 100%; + line-height: 40px; + + canvas { + vertical-align: baseline !important; + } + } + + .content { + display: inline-block; + padding: 7px 16px; + + .text { + font-size: 13px; + margin-top: 11px; + color: #555; + } + + .number { + font-weight: normal; + font-size: 26px; + margin-top: -4px; + color: #555; + } + } +} + +.info-box-3.hover-zoom-effect { + .icon { + i { + @include transition(all 0.3s ease); + } + } + + &:hover { + .icon { + i { + opacity: 0.4; + @include transform(rotate(-32deg) scale(1.4)); + } + } + } +} + +.info-box-3.hover-expand-effect { + &:after { + background-color: rgba(0, 0, 0, 0.05); + content: "."; + position: absolute; + left: 0; + top: 0; + width: 0; + height: 100%; + color: transparent; + @include transition(all 0.95s); + } + + &:hover { + &:after { + width: 100%; + } + } +} + +.info-box-4 { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + height: 80px; + display: flex; + cursor: default; + background-color: #fff; + position: relative; + overflow: hidden; + margin-bottom: 30px; + + .icon { + position: absolute; + right: 10px; + bottom: 2px; + text-align: center; + + i { + color: rgba(0, 0, 0, 0.15); + font-size: 60px; + } + } + + .chart { + margin-right: 5px; + } + + .chart.chart-bar { + height: 100%; + line-height: 50px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-pie { + height: 100%; + line-height: 34px; + + canvas { + vertical-align: baseline !important; + } + } + + .chart.chart-line { + height: 100%; + line-height: 40px; + + canvas { + vertical-align: baseline !important; + } + } + + .content { + display: inline-block; + padding: 7px 16px; + + .text { + font-size: 13px; + margin-top: 11px; + color: #555; + } + + .number { + font-weight: normal; + font-size: 26px; + margin-top: -4px; + color: #555; + } + } +} + +.info-box-4.hover-zoom-effect { + .icon { + i { + @include transition(all 0.3s ease); + } + } + + &:hover { + .icon { + i { + opacity: 0.4; + @include transform(rotate(-32deg) scale(1.4)); + } + } + } +} + +.info-box-4.hover-expand-effect { + &:after { + background-color: rgba(0, 0, 0, 0.05); + content: "."; + position: absolute; + left: 0; + top: 0; + width: 0; + height: 100%; + color: transparent; + @include transition(all 0.95s); + } + + &:hover { + &:after { + width: 100%; + } + } +} +.info-box-new { + background: #fff; + padding: 20px 20px 0px 20px; + color: #463f3f; + border-radius: 15px; + + .progress { + height: 10px; + border-radius: 20px; + } +} +.support-box { + padding: 15px; + color: #fff; + margin: 8px 0px 25px 0px; + border-radius: 10px; + min-height: 140px; +} +.counter-box { + padding: 15px; + color: #212529; + margin: 8px 0px 25px 0px; + border-radius: 10px; + min-height: 140px; + background-color: #ffffff; + box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1); +} +.info-box1 .text-end h2 { + color: #44e229; +} +.info-box2 .text-end h2 { + color: #e66c2f; +} +.info-box3 .text-end h2 { + color: #58b5f5; +} +.info-box4 .text-end h2 { + color: #ab92d4; +} + +.info-box5 { + width: 100%; + box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1); + margin-bottom: 25px; + padding: 0px 20px 0 20px; + border-radius: 25px; + height: 110px; + display: flex; + cursor: default; + background-color: #fff; + position: relative; + overflow: hidden; + small { + font-size: 14px; + } + .progress { + background: rgba(0, 0, 0, 0.2); + margin: 5px -10px 5px 0; + height: 8px; + background: #e3e3e3; + border-radius: 20px; + box-shadow: none; + overflow: visible; + .progress-bar { + border-radius: 20px; + background: #fff; + } + } + .knob-icon { + margin-top: 16px; + } + .info-box-content { + margin-top: 24px; + margin-left: 10px; + } + .progress-bar { + position: relative; + animation: animate-positive 4s; + line-height: 8px; + } +} +.info-box6 { + box-shadow: 2px 2px 10px #dadada; + margin: 8px 0px 25px 0px; + padding: 20px 10px; + background-color: #fff; + height: 100px; + border-radius: 5px; + transition: 0.3s linear all; + &:hover { + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 4px 4px 20px #dadada; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: 0.3s linear all; + } + &.primary { + background-color: #007bff; + color: #fff; + } + &.danger { + background-color: #ef5350; + color: #fff; + } + &.success { + background-color: #66bb6a; + color: #fff; + } + &.info { + background-color: #26c6da; + color: #fff; + } + i { + font-size: 5em; + opacity: 0.2; + } + .count-numbers { + position: absolute; + right: 35px; + top: 20px; + font-size: 32px; + display: block; + } + .count-name { + position: absolute; + right: 35px; + top: 65px; + font-style: italic; + text-transform: capitalize; + opacity: 0.5; + display: block; + font-size: 18px; + } +} +.bg-c-blue { + background: linear-gradient(to right, #5b73e8, #44c4fa); +} +.bg-c-green { + background: linear-gradient(to right, #1d976c, #2fd38a); +} +.bg-c-yellow { + background: linear-gradient(45deg, #ffb64d, #ffcb80); +} +.bg-c-pink { + background: linear-gradient(45deg, #ff5370, #ff869a); +} +.bg-c-purple { + background: linear-gradient(to right, #664dc9, #9884ea); +} +.bg-c-orange { + background: linear-gradient(to right, #fa5420, #f6a800); +} +.info-box7 { + border-radius: 5px; + color: #fff; + box-shadow: 0 1px 2.94px 0.06px rgba(4, 26, 55, 0.16); + border: none; + margin: 8px 0px 25px 0px; + transition: all 0.3s ease-in-out; + .info-box7-block { + padding: 25px; + } +} +.order-info-box7 i { + font-size: 30px; +} +.box-part { + background: #fff; + border-radius: 10px; + padding: 30px 15px; + margin: 5px 0 23px; +} +.box-part { + background: #fff; + border-radius: 10px; + padding: 30px 15px; + margin: 5px 0 23px; +} +.infobox-5 { + .card-icon { + i { + font-size: 4rem; + } + } +} +.card-statistic-4 { + position: relative; + color: #000000; + padding: 15px; + border-radius: 3px; + overflow: hidden; + + .card-icon-large { + font-size: 110px; + width: 110px; + height: 50px; + text-shadow: 3px 7px rgba(0, 0, 0, 0.3); + } + + .card-icon { + text-align: center; + line-height: 50px; + margin-left: 15px; + color: #000; + position: absolute; + right: -5px; + top: 20px; + opacity: 0.1; + } + + .banner-img img { + max-width: 100%; + float: right; + } +} + +.info-card { + margin-top: 20px; + text-align: right; + + .info-box8 { + padding: 15px 20px; + } + .card1-icon { + width: 50px; + height: 50px; + position: absolute; + top: -15px; + font-size: 35px; + border-radius: 8px; + display: flex; + color: #fff; + align-items: center; + justify-content: center; + transition: all 0.3s ease-in-out; + } + .card-block > span { + color: #919aa3; + } + :hover .card1-icon { + top: -25px; + } +} + +@-webkit-keyframes animate-positive { + 0% { + width: 0; + } +} +@keyframes animate-positive { + 0% { + width: 0; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_inputformgroup.scss b/MyOffice.SPA/src/assets/scss/components/_inputformgroup.scss new file mode 100644 index 0000000..1baa625 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_inputformgroup.scss @@ -0,0 +1,456 @@ +/* + * Document : _inputformgroup.scss + * Author : RedStar Template + * Description: This scss file for input form style classes + */ +%extend_check { + font-size: 13px; + line-height: 1.42857; + color: #414244; + font-weight: 400; +} + +.input-group { + width: 100%; + margin-bottom: 20px; + display: table; + + .form-line { + display: inline-block; + width: 100%; + //border-bottom: 1px solid #ddd; + position: relative; + + &:after { + content: ""; + position: absolute; + left: 0; + width: 100%; + bottom: 0px; + @include transform(scaleX(0)); + @include transition(0.25s ease-in); + border-bottom: 2px solid #1f91f3; + } + + +.input-group-addon { + padding-right: 0; + padding-left: 10px; + } + } + + .help-info { + float: right; + font-size: 12px; + margin-top: 5px; + color: #999; + } + + label.error { + font-size: 12px; + display: block; + margin-top: 5px; + font-weight: normal; + color: #f44336; + } + + .form-line.error { + &:after { + border-bottom: 2px solid #f44336; + } + } + + .form-line.success { + &:after { + border-bottom: 2px solid #4caf50; + } + } + + .form-line.warning { + &:after { + border-bottom: 2px solid #ffc107; + } + } + + .form-line.focused { + &:after { + @include transform(scaleX(1)); + } + + .form-label { + bottom: 25px; + left: 0; + font-size: 12px; + } + } + + .input-group-addon { + border: none; + background-color: transparent; + padding-left: 0; + font-weight: bold; + display: table-cell; + + .material-icons { + font-size: 18px; + color: #555; + } + } + + input[type="text"], + .form-control { + //border: none; + box-shadow: none; + padding-left: 0; + margin: 0; + font-size: 13px; + } + + .form-control { + &:focus { + @include box-shadow(none !important); + } + } +} + +.input-group.input-group-sm { + .input-group-addon { + i { + font-size: 14px; + } + } + + .form-control { + font-size: 12px !important; + } +} + +.input-group.input-group-lg { + .input-group-addon { + i { + font-size: 26px; + } + } + + .form-control { + font-size: 18px !important; + } +} + +.input-field { + >label:not(.label-icon).active { + font-size: 15px !important; + transform: translateY(-5px) scale(0.8); + } + + input, + textarea { + font-size: 13px !important; + } + + >label { + font-size: 13px !important; + } +} + +.form-control-label { + text-align: right; + + label { + margin-top: 8px; + } +} + +.form-horizontal { + .form-group { + margin-bottom: 0; + } +} + +.form-group { + width: 100%; + margin-bottom: 25px; + + .form-control { + width: 100%; + /* border: none; */ + box-shadow: none; + /* border-bottom: 1px solid #9e9e9e;*/ + @include border-radius(0); + padding-left: 0; + } + + input.form-control { + margin: 0; + font-size: 13px; + } + + .help-info { + float: right; + font-size: 12px; + margin-top: 5px; + color: #999; + } + + label.error { + font-size: 12px; + display: block; + margin-top: 5px; + font-weight: normal; + color: #f44336; + } + + .form-line { + width: 100%; + position: relative; + //border-bottom: 1px solid #ddd; + + &:after { + content: ""; + position: absolute; + left: 0; + width: 100%; + height: 0; + bottom: -1px; + @include transform(scaleX(0)); + @include transition(0.25s ease-in); + border-bottom: 2px solid #1f91f3; + } + + .form-label { + font-weight: normal; + color: #aaa; + position: absolute; + top: 10px; + left: 0; + cursor: text; + @include transition(0.2s); + } + } + + .form-line.error { + &:after { + border-bottom: 2px solid #f44336; + } + } + + .form-line.success { + &:after { + border-bottom: 2px solid #4caf50; + } + } + + .form-line.warning { + &:after { + border-bottom: 2px solid #ffc107; + } + } + + .form-line.focused { + &:after { + @include transform(scaleX(1)); + } + + .form-label { + top: -10px; + left: 0; + font-size: 12px; + } + } +} + +.form-group-sm { + .form-label { + font-size: 12px; + } + + .form-line.focused { + .form-label { + bottom: 20px; + font-size: 10px; + } + } +} + +.form-group-lg { + .form-label { + font-size: 18px; + } + + .form-line.focused { + .form-label { + bottom: 35px; + font-size: 12px; + } + } +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: transparent; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +.show>.dropdown-menu { + display: block; +} + +.spinner { + .input-group-addon { + /*position: relative;*/ + position: absolute; + top: 10px; + right: 0; + + .spin-up i { + position: absolute; + left: 0; + } + + .spin-down i { + position: absolute; + left: 0; + bottom: -15px; + } + } +} + +.bootstrap-select { + .btn { + color: #333; + } +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.form-check { + @extend %extend_check; + margin-bottom: 0.5rem; + padding-left: 0; + + .form-check-label { + cursor: pointer; + padding-left: 25px; + position: relative; + padding-right: 15px; + + span { + display: block; + position: absolute; + left: -1px; + top: -1px; + transition-duration: 0.2s; + padding-left: 0; + } + } + + .form-check-input { + opacity: 0; + height: 0; + width: 0; + overflow: hidden; + position: absolute; + margin: 0; + z-index: -1; + left: 0; + pointer-events: none; + + &:checked+.form-check-sign { + &:before { + animation: rippleOn 0.5s; + } + + .check { + background: #9c27b0; + + &:before { + color: #ffffff; + box-shadow: 0 0 0 10px, 10px -10px 0 10px, 32px 0 0 20px, + 0 32px 0 20px, -5px 5px 0 10px, 20px -12px 0 11px; + animation: checkboxOn 0.3s forwards; + } + } + } + } + + .form-check-sign { + vertical-align: middle; + position: relative; + top: -2px; + float: left; + padding-right: 10px; + display: inline-block; + + &:before { + display: block; + position: absolute; + left: 0; + content: ""; + background-color: rgba(0, 0, 0, 0.84); + height: 20px; + width: 20px; + border-radius: 100%; + z-index: 1; + opacity: 0; + margin: 0; + top: 0; + transform: scale3d(2.3, 2.3, 1); + } + + .check { + position: relative; + display: inline-block; + width: 20px; + height: 20px; + border: 1px solid rgba(0, 0, 0, 0.54); + overflow: hidden; + z-index: 1; + border-radius: 3px; + + &:before { + position: absolute; + content: ""; + transform: rotate(45deg); + display: block; + margin-top: -3px; + margin-left: 7px; + width: 0; + color: #ffffff; + height: 0; + animation: checkboxOff 0.3s forwards; + } + } + } +} + +label { + @extend %extend_check; +} + +.default-select { + select { + width: 100px; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + } + + .select-wrapper input.select-dropdown { + display: none; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_labels.scss b/MyOffice.SPA/src/assets/scss/components/_labels.scss new file mode 100644 index 0000000..8a2f508 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_labels.scss @@ -0,0 +1,35 @@ +/* + * Document : _labels.scss + * Author : RedStar Template + * Description: This scss file for label style classes + */ +.label { + @include border-radius(10px); + padding: 2px 10px; + color: #fff; + display: inline-block; +} + +.label-default { + background-color: #777; +} + +.label-primary { + background-color: #1f91f3; +} + +.label-success { + background-color: #2b982b; +} + +.label-info { + background-color: #00b0e4; +} + +.label-warning { + background-color: #ff9600; +} + +.label-danger { + background-color: #fb483a; +} diff --git a/MyOffice.SPA/src/assets/scss/components/_leftsidebaroverlay.scss b/MyOffice.SPA/src/assets/scss/components/_leftsidebaroverlay.scss new file mode 100644 index 0000000..3d898e0 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_leftsidebaroverlay.scss @@ -0,0 +1,952 @@ +/* + * Document : _leftsidebaroverlay.scss + * Author : RedStar Template + * Description: This scss file for left side bar style classes + */ +.overlay { + position: fixed; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +.overlay-open { + .sidebar { + margin-left: 0; + z-index: 99999999; + } +} + +.sidebar { + @include transition(all 0.5s); + font-family: $sidebar-font-family; + background: #ffffff; + width: 260px; + overflow: hidden; + display: inline-block; + height: calc(100vh - 0px); + position: fixed; + top: 0px; + left: 0; + border-right: 1px solid #f2f4f9; + -webkit-box-shadow: 0 8px 10px 0 rgba(183, 192, 206, 0.2); + box-shadow: 0 8px 10px 0 rgba(183, 192, 206, 0.2); + z-index: 999 !important; + + .user-info { + padding: 13px 15px 12px 15px; + white-space: nowrap; + position: relative; + border-bottom: 1px solid #e9e9e9; + height: 135px; + + .image { + margin-right: 12px; + display: inline-block; + + img { + @include border-radius(50%); + vertical-align: bottom !important; + } + } + + .info-container { + cursor: default; + display: block; + position: relative; + top: 25px; + + .name { + @include three-dots-overflow(); + font-size: 14px; + max-width: 200px; + color: #fff; + } + + .email { + @include three-dots-overflow(); + font-size: 12px; + max-width: 200px; + color: #fff; + } + + .user-helper-dropdown { + position: absolute; + right: -3px; + bottom: -12px; + @include box-shadow(none); + cursor: pointer; + color: #fff; + } + } + } + + .menu { + position: relative; + overflow-y: auto; + height: 100vh; + clear: left; + + .list { + list-style: none; + padding-left: 0; + + li { + &.active { + .menu-top { + background-color: #f0f3fb; + border-radius: 5px; + } + > :first-child { + span { + font-weight: 500; + } + } + } + &.active-top { + .menu-top { + background-color: #f0f3fb; + } + } + } + + .header { + font-size: 12px; + margin: 15px 0px 5px 23px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #000000; + } + + i.material-icons { + font-size: 15px; + } + + i.fas { + font-size: 15px; + } + + i.far { + font-size: 15px; + } + + i.fab { + font-size: 15px; + } + + i.fa { + font-size: 15px; + } + + .tooltips .sidebarQuickIcon { + font-size: 18px; + margin-top: 10px; + } + + .active { + .menu-toggle { + background: rgba(146, 144, 144, 0.28); + } + + .ml-menu { + display: block; + } + .ml-sub-menu { + display: block; + } + .ml-sub-menu2 { + display: block; + } + .ml-sub-menu3 { + display: block; + } + } + + .menu-toggle { + &:after, + &:before { + position: absolute; + top: calc(50% - 13px); + right: 17px; + font-size: 19px; + @include transform(scale(0)); + @include transition(all 0.3s); + } + + &:before { + content: "\f054"; + transform: scale(1); + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + @include transform(scale(1)); + } + + &:after { + content: "\f078"; + transform: scale(1); + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + @include transform(scale(0)); + } + } + + .material-icons-two-tone { + vertical-align: middle; + filter: invert(43%) sepia(4%) saturate(19%) hue-rotate(342deg) + brightness(94%) contrast(88%); + } + + .active .menu-toggle { + &:before { + @include transform(scale(0)); + } + + &:after { + @include transform(scale(1)); + } + } + + .ml-sub-menu { + &:after, + &:before { + position: absolute; + top: calc(50% - 13px); + right: 17px; + font-size: 19px; + @include transition(all 0.3s); + } + + &:before { + content: "\f054"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // @include transform(scale(1)); + } + + &:after { + content: "\f078"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // content: "\2013"; + // @include transform(scale(0)); + } + } + .ml-sub-menu2 { + &:after, + &:before { + position: absolute; + top: calc(50% - 13px); + right: 17px; + font-size: 19px; + // @include transform(scale(0)); + @include transition(all 0.3s); + } + + &:before { + content: "\f054"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // @include transform(scale(1)); + } + + &:after { + content: "\f078"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // @include transform(scale(0)); + } + } + .ml-sub-menu3 { + &:after, + &:before { + position: absolute; + top: calc(50% - 13px); + right: 17px; + font-size: 19px; + // @include transform(scale(0)); + @include transition(all 0.3s); + } + + &:before { + content: "\f054"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // @include transform(scale(1)); + } + + &:after { + content: "\f078"; + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + // @include transform(scale(0)); + } + } + + a { + color: #000000; + position: relative; + font-size: 15px !important; + display: block; + overflow: hidden; + line-height: 2rem; + padding: 9px 9px 9px 9px; + margin: 8px 11px 0px 11px; + // Parent items (menu-toggle) have no routerLink/href — force pointer on hover. + cursor: pointer; + &:hover, + &:active { + text-decoration: none !important; + background-color: #f0f3fb; + } + + small { + position: absolute; + top: calc(50% - 7.5px); + right: 15px; + } + + span { + margin: 7px 0 7px 7px; + color: rgba(0, 0, 0, 0.85); + // font-weight: bold; + font-size: 16px; + overflow: hidden; + } + } + + .ml-menu { + list-style: none; + display: none; + padding-left: 0; + + span { + font-weight: normal; + font-size: 14px; + margin: 3px 0 1px 6px; + } + + li { + a { + padding-left: 40px; + padding-top: 4px; + padding-bottom: 4px; + } + + &.active { + a.toggled:not(.menu-toggle) { + &:before { + content: ""; + display: block; + width: 7px; + height: 7px; + border-radius: 50%; + position: absolute; + left: 18%; + top: 50%; + transform: translate(-10px, -50%); + opacity: 1; + transition: all 0.2s ease; + } + } + + .ml-menu { + &:before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 13px; + display: block; + width: 7px; + height: 7px; + position: absolute; + transition: 0.5s; + left: 6%; + font-weight: 900; + top: calc(50% - 15px); + } + // &:hover:before { + // font-weight: 600; + // left: 25px; + // } + } + .ml-menu-2 { + display: block; + } + .ml-menu { + color: #5668f3; + font-weight: 500; + } + .ml-sub-sub-menu { + color: #5668f3; + font-weight: 500; + } + } + a { + &.ml-sub-sub-menu { + &:before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 13px; + display: block; + width: 7px; + height: 7px; + position: absolute; + transition: 0.5s; + left: 18%; + font-weight: 900; + top: calc(50% - 15px); + } + &:hover:before { + font-weight: 600; + left: 50px; + } + } + } + + .ml-menu { + li { + a { + padding-left: 80px; + } + } + + .ml-menu { + li { + a { + padding-left: 95px; + } + } + } + } + } + .activeSub { + .ml-menu-2 { + display: block; + } + .ml-menu-3 { + display: block; + } + } + } + .ml-menu-2 { + list-style: none; + display: none; + padding-left: 0; + + span { + font-weight: normal; + font-size: 14px; + margin: 3px 0 1px 6px; + } + + li { + a { + padding-left: 55px; + padding-top: 4px; + padding-bottom: 4px; + } + } + li.active .ml-menu-3 { + display: block; + } + li.active .ml-menu2:before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 13px; + display: block; + width: 7px; + height: 7px; + position: absolute; + left: 17%; + font-weight: 900; + top: calc(50% - 15px); + } + } + .ml-menu-3 { + list-style: none; + display: none; + padding-left: 0; + + span { + font-weight: normal; + font-size: 14px; + margin: 3px 0 1px 6px; + } + + li { + a { + padding-left: 70px; + padding-top: 4px; + padding-bottom: 4px; + } + } + li.active .ml-menu3:before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 13px; + display: block; + width: 7px; + height: 7px; + position: absolute; + left: 24%; + font-weight: 900; + top: calc(50% - 15px); + } + } + } + } + .sidebar-badge { + position: absolute; + right: 35px; + padding: 3px 6px; + margin-top: 5px !important; + border-radius: 15px; + color: #ffffff !important; + font-size: 12px !important; + border: none; + font-weight: 300 !important; + } +} +.ml-menu { + .ml-sub-menu { + &:before { + @include transform(scale(1)); + } + &:after { + @include transform(scale(0)); + } + } + + .ml-sub-menu2 { + &:before { + @include transform(scale(1)); + } + &:after { + @include transform(scale(0)); + } + } + + .ml-sub-menu3 { + &:before { + @include transform(scale(1)); + } + &:after { + @include transform(scale(0)); + } + } + + .active .ml-sub-menu { + &:after { + @include transform(scale(1)); + } + + &:before { + @include transform(scale(0)); + } + } + + .ml-menu-2 { + li.active .ml-sub-menu2 { + &:after { + @include transform(scale(1)); + } + + &:before { + @include transform(scale(0)); + } + } + } + + .ml-menu-3 { + li.active .ml-sub-menu3 { + &:after { + @include transform(scale(1)); + } + + &:before { + @include transform(scale(0)); + } + } + } +} +.right-sidebar { + width: 280px; + height: calc(100vh - 60px); + position: fixed; + right: -300px; + top: 60px; + background: #fdfdfd; + z-index: 999 !important; + @include box-shadow(-2px 2px 5px rgba(0, 0, 0, 0.1)); + overflow: hidden; + @include transition(0.5s); + + &.open { + right: 0; + } + + .nav-tabs { + font-weight: 600; + font-size: 13px; + width: 100%; + margin-left: 2px; + + li { + text-align: center; + + > a { + margin-right: 0; + } + + &:first-child { + width: 45%; + } + + &:last-child { + width: 55%; + } + } + } + + .rightSidebarClose { + position: absolute; + top: 8px; + left: 8px; + z-index: 99; + cursor: pointer; + } +} + +.boxed-layout { + .sidebar { + left: auto; + } + + &.side-closed .sidebar { + left: 0; + } +} + +.horizontal-menu { + width: 100%; + text-align: center; +} + +.top-sidebar { + float: left; + width: 100%; + position: fixed; + z-index: 9; + top: 60px; + background-color: #ffffff; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + -ms-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + + ul.horizontal-list { + overflow: visible !important; + margin: 0; + display: inline-block; + height: 60px; + + a { + color: #747474; + float: left; + width: 100%; + } + + li { + float: left; + position: relative; + padding: 20px 20px; + + &:hover > ul { + display: block !important; + position: absolute; + top: 63px; + background-color: #fff; + border-radius: 5px; + z-index: 9; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + -ms-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); + + &:after { + position: absolute; + bottom: 100%; + left: 1.8rem; + width: 0; + height: 0; + margin-left: -16px; + content: " "; + pointer-events: none; + border: solid transparent; + border-width: 9px; + border-color: rgba(136, 183, 213, 0); + border-bottom-color: #fff; + } + + &:before { + position: absolute; + bottom: 100%; + left: 1.8rem; + width: 0; + height: 0; + margin-left: -16px; + content: " "; + pointer-events: none; + border: solid transparent; + border-width: 9px; + border-color: rgba(136, 183, 213, 0); + border-bottom-color: #ad5454; + } + + &.mega-ml-menu { + position: relative; + top: 0; + } + } + + ul li { + width: 100%; + padding: 10px 15px; + text-align: left; + } + } + + ul.ml-menu { + display: none !important; + width: 200px; + + li:hover ul { + top: 0; + left: 100%; + } + + .menu-toggle { + position: relative; + + &:after { + content: "\2023"; + position: absolute; + right: 0; + font-size: 25px; + top: 0; + line-height: 22px; + } + } + } + + i.material-icons { + margin-top: 1px; + margin-right: 3px; + float: left; + font-size: 20px; + } + + // .menu-toggle::after{ + // content: "\2304"; + // position: absolute; + // top: 15px; + // right: 0; + // } + } + + .slimScrollDiv { + overflow: visible !important; + } +} + +// Dark left sidebar style +.menu_dark .sidebar { + background: #1a202e; + border-right: 1px solid #2f3a44; + font-weight: 500; + + .menu .list { + a { + color: #cfd8e3; + span { + color: #cfd8e3; + } + } + + a:hover { + background-color: rgba(0, 0, 0, 0.2); + } + + .header { + background: transparent; + color: #9babf1; + font-size: 12px; + margin: 15px 0px 5px 23px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .active { + .menu-toggle { + background-color: rgba(0, 0, 0, 0.4); + } + } + + .menu-toggle::after { + color: #b8babb; + } + + .menu-toggle::before { + color: #b8babb; + } + + .material-icons-two-tone { + filter: invert(86%) sepia(3%) saturate(2995%) hue-rotate(185deg) + brightness(79%) contrast(78%); + } + + .ml-menu { + li a { + color: #dadada; + } + } + + li { + &.active { + .menu-top { + background-color: rgba(0, 0, 0, 0.4); + } + } + &.active-top { + .menu-top { + background-color: rgba(0, 0, 0, 0.4); + } + } + } + } + + .menu .list .ml-menu .active a::before { + content: "\f105"; + font-family: "Font Awesome 5 Free"; + font-size: 13px; + display: block; + width: 7px; + height: 7px; + position: absolute; + left: 10%; + font-weight: 900; + } +} + +.menu_dark { + .sidebar-userpic-name { + color: #e6e6e6; + } + + .profile-usertitle-job { + color: #e6e6e6; + } +} + +.user-panel { + float: left; + width: 100%; + color: #ccc; + padding: 25px 0px 10px 0; + .image { + width: 35%; + max-width: 75px; + margin: 0 auto; + + img { + max-width: 100%; + } + } +} + +.user-img-circle { + background: #fff; + z-index: 1000; + position: inherit; + box-shadow: 10px 10px 13px 0px rgba(78, 78, 78, 0.15); +} + +.img-circle { + border-radius: 15%; +} + +.profile-usertitle { + text-align: center; + color: #060606; +} + +.profile-usertitle-job { + font-size: 11px; + color: #000000; +} + +.sidebar-userpic-btn { + display: flex; + place-content: space-around; + margin: auto; + line-height: 2rem; + padding: 9px 9px 9px 9px; + margin: 8px 13px 0px 13px; + + a { + padding: 0px !important; + margin: 0px !important; + height: 30px; + width: 30px; + + &:hover { + cursor: pointer; + } + + .mat-button-wrapper { + margin-left: 0px !important; + } + + &:hover { + background-color: transparent !important; + } + } +} + +.collapse.in { + display: block; + list-style-type: none; +} +.sidebarIcon { + height: 18px !important; + width: 18px !important; + text-align: center; + fill: rgba(75, 75, 90, 0.12) !important; +} +.headerShadow { + width: 100%; + height: 100px; + position: fixed; + top: 0; + z-index: 1; + background: linear-gradient(180deg, #f8f8f8e6 44%, #f7f7f780 73%, #ffffff00); +} diff --git a/MyOffice.SPA/src/assets/scss/components/_navbar.scss b/MyOffice.SPA/src/assets/scss/components/_navbar.scss new file mode 100644 index 0000000..35ed72f --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_navbar.scss @@ -0,0 +1,654 @@ +/* + * Document : _navbar.scss + * Author : RedStar Template + * Description: This scss file for navbar style classes + */ +.navbar { + font-family: $navbar-font-family; + @include border-radius(0); + box-shadow: none; + // @include box-shadow(0px 0px 20px 0px rgba(0,0,0,0.15)); + border: none; + position: fixed; + top: 0; + left: 0; + z-index: 999; + width: 100%; + padding: 0; + + .navbar-brand { + @include three-dots-overflow(); + } + + .navbar-custom-right-menu { + float: right; + } + + .navbar-toggle { + text-decoration: none; + color: #fff; + width: 20px; + height: 20px; + margin-top: -7px; + line-height: 60px; + margin-right: 17px; + + &:before { + content: "\E8D5"; + font-family: "Material Icons"; + font-size: 26px; + } + } + + .navbar-collapse { + &.in { + overflow: visible; + } + } + .nav.navbar-nav { + display: block; + } + .container-fluid, + .container { + display: block; + box-shadow: 0 4px 24px 0 rgb(35 42 48 / 10%); + } + .dropdown-menu ul.menu li { + width: 100%; + } + .nav-left-menu { + margin: 3px 15px; + } + .collapse-menu-icon { + margin-bottom: 0px; + line-height: 60px; + display: none; + } + .header-icon { + vertical-align: top !important; + .feather { + height: 20px; + width: 20px; + } + } +} +.ls-closed { + .sidebar { + margin-left: -300px; + } + + section.content { + margin-left: 15px; + } + + .bars { + &:after, + &:before { + font-family: "Material Icons"; + font-size: 24px; + position: absolute; + left: 10px; + top: 0px; + line-height: 60px; + margin-right: 10px; + @include transform(scale(0)); + @include transition(all 0.3s); + } + + &:before { + content: "\E5D2"; + @include transform(scale(1)); + } + + // &:after { + // content: "\E5C4"; + // @include transform(scale(0)); + // } + } + + .navbar-brand { + margin-left: 30px; + } +} + +.overlay-open { + .bars { + &:before { + @include transform(scale(0)); + } + + &:after { + @include transform(scale(1)); + } + } + &.ls-closed { + .sidebar { + margin-left: 0; + } + } +} + +.navbar-header { + padding: 8px; + background-color: #000; + width: 260px; + float: left; + @include transition(all 0.5s); + + .bars { + float: left; + text-decoration: none; + } +} +.navbar-icon { + list-style-type: none; + padding-left: 20px; +} +.logo-name { + color: white; + font-size: 24px; + font-weight: 400; +} +.navbar-nav { + > li { + > a { + padding: 7px 7px 2px 7px; + } + .js-right-sidebar { + margin-right: 5px; + } + } + &.navbar-left { + margin-right: 10px; + line-height: 60px; + } + + &.navbar-right { + float: right !important; + margin-right: 10px; + line-height: 60px; + // .nav-item { + // margin: 0px 4px; + // } + + .user_profile { + .dropdown-toggle { + cursor: pointer; + } + .user_img { + float: right; + margin: 13px 0px 0px 10px; + border-radius: 50%; + } + span { + font-weight: 500; + color: #fff; + } + } + } + .langSelItem .dropdown-menu { + margin-top: 35px !important; + } + + .app-dropdown { + right: unset !important; + left: 0 !important; + &::before { + left: 19px !important; + right: unset !important; + } + &::after { + left: 20px !important; + right: unset !important; + } + } + .dropdown-menu { + margin-top: 60px !important; + width: 325px; + right: 0; + left: auto; + position: absolute !important; + top: 0; + padding: 0; + border-radius: 5px; + &::before { + content: ""; + position: absolute; + top: -7px; + right: 19px; + display: inline-block !important; + border-right: 7px solid transparent; + border-bottom: 7px solid #eee; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + } + &::after { + content: ""; + position: absolute; + top: -6px; + right: 20px; + display: inline-block !important; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + } + ul.menu .menu-info p { + line-height: 1; + .material-icons { + display: inline-block; + } + } + li.footer { + width: 100%; + height: 45px; + } + .header { + line-height: 2; + } + } + .material-icons { + line-height: 1; + height: 24px; + font-size: 27px; + } + .fas { + line-height: 1; + height: 24px; + font-size: 18px; + } + .far { + line-height: 1; + height: 24px; + font-size: 18px; + } + .fab { + line-height: 1; + height: 24px; + font-size: 18px; + } + .fa { + line-height: 1; + height: 24px; + font-size: 18px; + } + .user_profile { + .user_dw_menu { + list-style-type: none; + padding-left: 0px; + li { + width: 100%; + border-bottom: 1px solid #eee; + height: 45px; + a { + line-height: 24px; + color: #333333; + display: inline-flex; + } + i { + float: left; + padding-right: 5px; + } + &:last-child { + border-bottom: 0; + } + } + } + .dropdown-menu { + width: 200px; + right: -10px; + } + } +} +.nav-notification-icons { + min-width: 0 !important; + flex-shrink: 0; + line-height: 40px !important; + border-radius: 50% !important; + margin: 0px 4px; + &:after { + display: none; + } + .material-icons-outlined { + display: flex; + vertical-align: middle; + } +} +.lang-dropdown { + cursor: pointer; + // margin: 14px 20px 0px 10px; + .country-name { + margin-left: 5px; + vertical-align: middle; + } + &::after { + display: none; + } + img { + height: 17px; + border-radius: 3px; + } +} +.lang-item { + width: 175px !important; + .lang-item-list { + line-height: 30px; + &.active { + background-color: #eef1f9; + color: #000000; + } + &:active { + background-color: #eef1f9; + color: #000000; + } + .flag-img { + margin: 0px 5px; + } + } +} + +.label-count { + position: absolute; + top: 5px; + right: 10px; + font-size: 10px; + line-height: 15px; + background-color: #000; + padding: 3px 3px; + border-radius: 3px; +} +.navbar-brand { + float: left; + height: 45px; + padding: 6px 25px; + margin-left: 20px; + font-size: 18px; + line-height: 20px; + text-align: center; + width: 100%; + img { + /*float: left;*/ + vertical-align: top; + } + span { + line-height: 32px; + padding-left: 10px; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} +.collapse { + display: none; +} +.navbar-nav { + float: none; + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; + } +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; + > li { + position: relative; + display: block; + > a { + position: relative; + display: block; + padding: 5px 0px; + } + } + .fullscreen { + // margin: 0px 10px; + } + .langSelItem { + margin: 15px 10px 0px 10px; + } + .btnNotification { + margin: 0px 10px; + } + .btnUserDropdown { + margin: 0px 8px; + } + .btnHeaderSetting { + margin: 0px 8px; + .settingBtn { + vertical-align: top; + } + } + .user-menu-icons { + margin: 0px 5px; + .feather { + height: 18px !important; + width: 18px !important; + vertical-align: middle; + } + } + + .menuIcon { + line-height: 20px !important; + height: 20px; + font-size: 20px; + } + .logo { + margin-right: auto; + } +} +nav.navbar { + min-height: 60px; + position: fixed; + width: calc(100% - 4rem - 257px); + z-index: 12; + right: 0; + left: unset; + margin: 1.3rem 2rem 0; + border-radius: 0.428rem; + box-shadow: 0 -18px 1px 5px rgb(241 240 244); + top: 0; +} + +@each $key, $val in $colors { + .col-#{$key} { + .navbar { + @include navbar-link-color(#fff, #000, 0.95); + // @include navbar-link-color(rgba(0,0,0,0.85), #000, .95); + } + } +} +.side-closed { + .sidebar { + /*margin-left: -300px;*/ + width: 60px; + .menu { + .list { + li { + .menu-toggle { + &:before, + &:after { + content: ""; + } + } + span { + display: none; + } + a { + line-height: 1rem; + padding-left: 9px; + &:after { + top: calc(50% - 7px); + } + } + } + } + } + } + &.submenu-closed { + .sidebar .menu .list { + li .ml-menu, + .header { + display: none !important; + } + } + .navbar-header { + width: 65px; + .navbar-brand { + padding-right: 0; + padding-left: 0; + // margin: 0; + span { + display: none; + } + } + .navbar-nav { + .sidemenu-collapse { + display: none; + } + } + } + .sidebar-user-panel { + display: none; + } + } + + section.content { + margin-left: 58px; + transition: all 1s, width 1s; + } + nav.navbar { + width: calc(100% - 4rem - 55px); + transition: all 1s, width 1s; + } + + .navbar-brand { + margin-left: 5px; + } + &.side-closed-hover { + .sidebar { + width: 260px; + transition: all 1s, width 1s; + .menu { + .list { + li { + .menu-toggle { + &:before { + content: "\f054"; + transform: scale(1); + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + top: calc(50% - 7px); + @include transform(scale(1)); + } + &:after { + content: "\f078"; + transform: scale(1); + font-family: "Font Awesome 5 Free"; + font-weight: 600; + font-size: 12px; + color: gray; + @include transform(scale(0)); + } + } + span { + display: block; + float: left; + } + a { + padding: 9px 9px 9px 9px; + } + i { + float: left; + line-height: 2rem; + } + .sidebarIcon { + float: left; + line-height: 2rem; + } + } + .ml-menu { + li { + a { + padding-left: 45px; + padding-top: 7px; + padding-bottom: 7px; + line-height: 2rem; + } + } + .ml-menu-2 { + li { + a { + padding-left: 65px; + padding-top: 4px; + padding-bottom: 4px; + } + } + } + .ml-menu-3 { + li { + a { + padding-left: 85px; + padding-top: 4px; + padding-bottom: 4px; + } + } + } + } + .active .menu-toggle { + &:before { + @include transform(scale(0)); + } + + &:after { + @include transform(scale(1)); + } + } + } + } + } + section.content { + margin-left: 260px; + transition: all 1s, width 1s; + } + nav.navbar { + width: calc(100% - 4rem - 257px); + transition: all 1s, width 1s; + } + } +} +.sidemenu-collapse { + height: 50px; + padding: 0px 20px 0px 44px; + &:hover { + text-decoration: none; + color: #fff; + background-color: transparent; + } + .fas { + line-height: 1; + height: 24px; + font-size: 18px; + color: #3a2c70; + } +} +nav, +nav .nav-wrapper i, +nav a.button-collapse, +nav a.button-collapse i { + @media screen and (min-width: 601px) { + // height: 60px; + // line-height: 60px; + } +} +.boxed-layout { + .container > .navbar-header { + margin-left: 0; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_navtabs.scss b/MyOffice.SPA/src/assets/scss/components/_navtabs.scss new file mode 100644 index 0000000..1e52768 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_navtabs.scss @@ -0,0 +1,76 @@ +/* + * Document : _navtabs.scss + * Author : RedStar Template + * Description: This scss file for navbar tabs style classes + */ +.nav-tabs { + border-bottom: 2px solid #eee; + + > li { + position: relative; + top: 3px; + left: -2px; + + > a { + border: none !important; + color: #999 !important; + @include border-radius(0); + + &:hover, + &:active, + &:focus { + background-color: transparent !important; + } + + &:before { + content: ""; + position: absolute; + left: 0; + width: 100%; + height: 0; + border-bottom: 2px solid #2196f3; + bottom: 2px; + @include transform(scaleX(0)); + @include transition(0.1s ease-in); + } + + .material-icons { + position: relative; + top: 7px; + margin-bottom: 8px; + } + } + } + + li { + a.active { + color: #222 !important; + + &:hover, + &:active, + &:focus { + background-color: transparent !important; + } + + &:before { + @include transform(scaleX(1)); + } + } + } + + + .tab-content { + padding: 15px 0; + } +} + +@each $key, $val in $colors { + .nav-tabs.tab-col-#{$key} { + > li { + > a { + &:before { + border-bottom: 2px solid $val; + } + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_noticeboard.scss b/MyOffice.SPA/src/assets/scss/components/_noticeboard.scss new file mode 100644 index 0000000..cf4603c --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_noticeboard.scss @@ -0,0 +1,18 @@ +.notice-board { + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-align: start; + -webkit-box-align: start; + align-items: flex-start; + .notice-body { + font-size: 13px; + padding: 0px 0px 5px 10px; + p { + margin-bottom: 0px; + } + .notice-heading { + margin: 5px 0px 0px 0px; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_rightsidebar.scss b/MyOffice.SPA/src/assets/scss/components/_rightsidebar.scss new file mode 100644 index 0000000..e2a555d --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_rightsidebar.scss @@ -0,0 +1,30 @@ +.rightSetting { + padding: 20px 25px 0px 25px; + p { + font-weight: bold; + margin: 0; + border-bottom: 1px solid #eee; + font-size: 12px; + text-align: left; + } + .mat-button-toggle-checked { + background-color: #6e68c1; + color: #ffffff !important; + } + .mat-button-toggle { + width: 70px; + height: 30px; + font-size: 14px; + } + .mat-button-toggle-button { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 30px !important; + padding: 0 12px !important; + text-align: center; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_searchbar.scss b/MyOffice.SPA/src/assets/scss/components/_searchbar.scss new file mode 100644 index 0000000..26c5770 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_searchbar.scss @@ -0,0 +1,70 @@ +/* + * Document : _noUISlider.scss + * Author : RedStar Template + * Description: This scss file for noUISlider component style classes + */ +%extend_search { + outline: none; + opacity: 1; + margin-left: -43px; +} + +.search-box { + overflow: hidden; + width: 270px; + vertical-align: middle; + white-space: nowrap; + input { + &#search { + width: 250px; + height: 40px; + background: rgb(255, 255, 255); + border: none; + font-size: 10pt; + float: left; + color: #fff; + padding-left: 15px; + margin-bottom: 0; + margin-top: 10px; + border-radius: 50px; + &:-moz-placeholder { + color: #65737e; + } + &:-ms-input-placeholder { + color: #65737e; + } + } + &#search::-webkit-input-placeholder { + color: #65737e; + } + &#search::-moz-placeholder { + color: #65737e; + } + } + button.icon { + border: none; + background: transparent; + height: 50px; + width: 0px; + color: #4f5b66; + opacity: 0; + font-size: 10pt; + transition: all 0.55s ease; + + .fa { + font-size: 18px; + } + } + &:hover button.icon { + @extend %extend_search; + &:hover { + background: transparent; + } + } + &:active button.icon { + @extend %extend_search; + } + &:focus button.icon { + @extend %extend_search; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_settingSidebar.scss b/MyOffice.SPA/src/assets/scss/components/_settingSidebar.scss new file mode 100644 index 0000000..a90e1d1 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_settingSidebar.scss @@ -0,0 +1,197 @@ +.settingSidebar { + background: #fff; + position: fixed; + height: 100%; + width: 280px; + top: 62px; + right: -280px; + z-index: 999; + transition: 0.3s ease-in; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.16), 0 2px 10px rgba(0, 0, 0, 0.12); + + .settingPanelToggle { + background: #6777ef; + padding: 13px 13px; + color: #fff; + position: absolute; + top: 30%; + left: -40px; + width: 40px; + border-radius: 10px 0 0 10px; + .setting-sidebar-icon .feather { + height: 17px; + width: 17px; + } + } + + &.showSettingPanel { + right: 0; + } + + .settingSidebar-body { + position: relative; + height: 100%; + } + + .settingSidebar-tab { + display: flex; + + .nav-item { + width: 33.33%; + text-align: center; + + .nav-link { + padding: 15px 12px; + color: #6a7a8c; + border-bottom: 3px solid transparent; + + &.active { + border-bottom: 3px solid #2962ff; + color: #2962ff; + } + + &:hover { + border-bottom: 3px solid #2962ff; + color: #2962ff; + } + } + } + } + + ul.choose-theme li { + display: inline-block; + + &:hover { + cursor: pointer; + } + } + + ul.choose-theme li div { + border-radius: 15px; + display: inline-block; + vertical-align: middle; + height: 25px; + width: 25px; + overflow: hidden; + position: relative; + margin: 4px; + } + + ul.choose-theme li div.purple { + background: #6777ef; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.orange { + background: #ffa117; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.cyan { + background: #3dc7be; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.green { + background: #4caf4f; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.red { + background: #ea5455; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.white { + background: #ece8e8; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li div.black { + background: #343a40; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + ul.choose-theme li div.blue { + background: #03a9f3; + -webkit-box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + + ul.choose-theme li.active div::after { + content: "\f00c"; + color: #fff; + top: 4px; + left: 7px; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + font-size: 12px; + position: absolute; + -webkit-transition: 0.5s; + transition: 0.5s; + } + + .setting-panel-header { + display: block; + padding: 15px 20px; + color: #212529; + font-size: 15px; + border: 1px solid #eae9e9; + background: #e9ecef; + } + + .disk-server-setting { + .progress { + height: 8px; + } + + p { + font-weight: bold; + margin: 0; + border-bottom: 1px solid #eee; + font-size: 14px; + text-align: left; + padding-bottom: 5px; + } + } + + .rt-sidebar-last-ele { + margin-bottom: 70px !important; + } +} +.hiddenradio { + padding: 0px 20px 0px 0px; + margin: 0px 20px 0px 0px; +} +.hiddenradio [type="radio"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +/* IMAGE STYLES */ +.hiddenradio [type="radio"] + img { + cursor: pointer; + height: auto; + width: 100%; + border: 3px solid #d5e0ec; +} + +/* CHECKED STYLES */ +.hiddenradio [type="radio"]:checked + img, +.hiddenradio label.layout-selected img { + outline: 2px solid #6777ef; +} + +@media only screen and (max-width: 1024px) { + .settingSidebar { + display: none; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_switch.scss b/MyOffice.SPA/src/assets/scss/components/_switch.scss new file mode 100644 index 0000000..6324216 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_switch.scss @@ -0,0 +1,40 @@ +/* + * Document : _switch.scss + * Author : RedStar Template + * Description: This scss file switch button style classes + */ +.switch { + label { + font-weight: normal; + font-size: 13px; + + .lever { + margin: 0 14px; + } + + input[type="checkbox"] { + &:checked { + @each $key, $val in $colors { + &:not(:disabled) { + ~ .lever.switch-col-#{$key} { + &:active { + &:after { + box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.4), + 0 0 0 15px rgba($val, 0.1); + } + } + } + } + + + .lever.switch-col-#{$key} { + background-color: rgba($val, 0.5); + + &:after { + background-color: $val; + } + } + } + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_thumbnails.scss b/MyOffice.SPA/src/assets/scss/components/_thumbnails.scss new file mode 100644 index 0000000..b229ed0 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_thumbnails.scss @@ -0,0 +1,18 @@ +/* + * Document : _thumbnail.scss + * Author : RedStar Template + * Description: This scss file for thumbnail style classes + */ +.thumbnail { + @include border-radius(0); + + p:not(button) { + color: #999999; + font-size: 14px; + } + + h3 { + font-weight: bold; + font-size: 17px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/components/_todo.scss b/MyOffice.SPA/src/assets/scss/components/_todo.scss new file mode 100644 index 0000000..fa5536f --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/components/_todo.scss @@ -0,0 +1,85 @@ +/* + * Document : _todo.scss + * Author : RedStar Template + * Description: This scss file for todo style classes + */ +.to-do-list { + padding-left: 0; + margin-top: -10px; + font-size: 12px; + min-height: 311px; + float: left; + width: 100%; + li { + padding: 15px 0; + border-radius: 3px; + position: relative; + cursor: move; + list-style: none; + font-size: 14px; + background: #fff; + border-bottom: 1px dotted rgba(0, 0, 0, 0.2); + p { + margin: 0; + padding-left: 50px; + } + .todo-check input[type="checkbox"] { + visibility: hidden; + } + } +} +.todo-actionlist { + position: absolute; + right: -5px; + top: 22px; + a { + height: 24px; + width: 24px; + display: inline-block; + float: left; + i { + height: 24px; + width: 24px; + display: inline-block; + text-align: center; + line-height: 24px; + color: #ccc; + } + &:hover i { + color: #666; + } + } +} +.line-through { + text-decoration: line-through; +} +.todo-action-bar { + margin-top: 20px; +} +.todo-check { + width: 20px; + position: relative; + margin-right: 10px; + margin-left: 10px; + input[type="checkbox"] { + visibility: hidden; + } + label { + cursor: pointer; + position: absolute; + width: 20px; + height: 20px; + top: 0; + left: 0; + border-radius: 2px; + } +} +.todo-done i { + font-size: 14px; +} +.todo-remove i { + font-size: 10px; +} +.inbox-small-cells .todo-check input[type="checkbox"] { + visibility: hidden; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_animated.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_animated.scss new file mode 100644 index 0000000..7c7c0e1 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_animated.scss @@ -0,0 +1,20 @@ +// Animated Icons +// -------------------------- + +.#{$fa-css-prefix}-spin { + animation: fa-spin 2s infinite linear; +} + +.#{$fa-css-prefix}-pulse { + animation: fa-spin 1s infinite steps(8); +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_bordered-pulled.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_bordered-pulled.scss new file mode 100644 index 0000000..c8c4274 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_bordered-pulled.scss @@ -0,0 +1,20 @@ +// Bordered & Pulled +// ------------------------- + +.#{$fa-css-prefix}-border { + border: solid .08em $fa-border-color; + border-radius: .1em; + padding: .2em .25em .15em; +} + +.#{$fa-css-prefix}-pull-left { float: left; } +.#{$fa-css-prefix}-pull-right { float: right; } + +.#{$fa-css-prefix}, +.fas, +.far, +.fal, +.fab { + &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } + &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_core.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_core.scss new file mode 100644 index 0000000..cbd4cf7 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_core.scss @@ -0,0 +1,21 @@ +// Base Class Definition +// ------------------------- + +.#{$fa-css-prefix}, +.fas, +.far, +.fal, +.fad, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; +} + +%fa-icon { + @include fa-icon; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_fixed-width.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_fixed-width.scss new file mode 100644 index 0000000..970641f --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_fixed-width.scss @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.#{$fa-css-prefix}-fw { + text-align: center; + width: $fa-fw-width; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_icons.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_icons.scss new file mode 100644 index 0000000..fa3ac07 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_icons.scss @@ -0,0 +1,1451 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ + +.#{$fa-css-prefix}-500px:before { content: fa-content($fa-var-500px); } +.#{$fa-css-prefix}-accessible-icon:before { content: fa-content($fa-var-accessible-icon); } +.#{$fa-css-prefix}-accusoft:before { content: fa-content($fa-var-accusoft); } +.#{$fa-css-prefix}-acquisitions-incorporated:before { content: fa-content($fa-var-acquisitions-incorporated); } +.#{$fa-css-prefix}-ad:before { content: fa-content($fa-var-ad); } +.#{$fa-css-prefix}-address-book:before { content: fa-content($fa-var-address-book); } +.#{$fa-css-prefix}-address-card:before { content: fa-content($fa-var-address-card); } +.#{$fa-css-prefix}-adjust:before { content: fa-content($fa-var-adjust); } +.#{$fa-css-prefix}-adn:before { content: fa-content($fa-var-adn); } +.#{$fa-css-prefix}-adobe:before { content: fa-content($fa-var-adobe); } +.#{$fa-css-prefix}-adversal:before { content: fa-content($fa-var-adversal); } +.#{$fa-css-prefix}-affiliatetheme:before { content: fa-content($fa-var-affiliatetheme); } +.#{$fa-css-prefix}-air-freshener:before { content: fa-content($fa-var-air-freshener); } +.#{$fa-css-prefix}-airbnb:before { content: fa-content($fa-var-airbnb); } +.#{$fa-css-prefix}-algolia:before { content: fa-content($fa-var-algolia); } +.#{$fa-css-prefix}-align-center:before { content: fa-content($fa-var-align-center); } +.#{$fa-css-prefix}-align-justify:before { content: fa-content($fa-var-align-justify); } +.#{$fa-css-prefix}-align-left:before { content: fa-content($fa-var-align-left); } +.#{$fa-css-prefix}-align-right:before { content: fa-content($fa-var-align-right); } +.#{$fa-css-prefix}-alipay:before { content: fa-content($fa-var-alipay); } +.#{$fa-css-prefix}-allergies:before { content: fa-content($fa-var-allergies); } +.#{$fa-css-prefix}-amazon:before { content: fa-content($fa-var-amazon); } +.#{$fa-css-prefix}-amazon-pay:before { content: fa-content($fa-var-amazon-pay); } +.#{$fa-css-prefix}-ambulance:before { content: fa-content($fa-var-ambulance); } +.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: fa-content($fa-var-american-sign-language-interpreting); } +.#{$fa-css-prefix}-amilia:before { content: fa-content($fa-var-amilia); } +.#{$fa-css-prefix}-anchor:before { content: fa-content($fa-var-anchor); } +.#{$fa-css-prefix}-android:before { content: fa-content($fa-var-android); } +.#{$fa-css-prefix}-angellist:before { content: fa-content($fa-var-angellist); } +.#{$fa-css-prefix}-angle-double-down:before { content: fa-content($fa-var-angle-double-down); } +.#{$fa-css-prefix}-angle-double-left:before { content: fa-content($fa-var-angle-double-left); } +.#{$fa-css-prefix}-angle-double-right:before { content: fa-content($fa-var-angle-double-right); } +.#{$fa-css-prefix}-angle-double-up:before { content: fa-content($fa-var-angle-double-up); } +.#{$fa-css-prefix}-angle-down:before { content: fa-content($fa-var-angle-down); } +.#{$fa-css-prefix}-angle-left:before { content: fa-content($fa-var-angle-left); } +.#{$fa-css-prefix}-angle-right:before { content: fa-content($fa-var-angle-right); } +.#{$fa-css-prefix}-angle-up:before { content: fa-content($fa-var-angle-up); } +.#{$fa-css-prefix}-angry:before { content: fa-content($fa-var-angry); } +.#{$fa-css-prefix}-angrycreative:before { content: fa-content($fa-var-angrycreative); } +.#{$fa-css-prefix}-angular:before { content: fa-content($fa-var-angular); } +.#{$fa-css-prefix}-ankh:before { content: fa-content($fa-var-ankh); } +.#{$fa-css-prefix}-app-store:before { content: fa-content($fa-var-app-store); } +.#{$fa-css-prefix}-app-store-ios:before { content: fa-content($fa-var-app-store-ios); } +.#{$fa-css-prefix}-apper:before { content: fa-content($fa-var-apper); } +.#{$fa-css-prefix}-apple:before { content: fa-content($fa-var-apple); } +.#{$fa-css-prefix}-apple-alt:before { content: fa-content($fa-var-apple-alt); } +.#{$fa-css-prefix}-apple-pay:before { content: fa-content($fa-var-apple-pay); } +.#{$fa-css-prefix}-archive:before { content: fa-content($fa-var-archive); } +.#{$fa-css-prefix}-archway:before { content: fa-content($fa-var-archway); } +.#{$fa-css-prefix}-arrow-alt-circle-down:before { content: fa-content($fa-var-arrow-alt-circle-down); } +.#{$fa-css-prefix}-arrow-alt-circle-left:before { content: fa-content($fa-var-arrow-alt-circle-left); } +.#{$fa-css-prefix}-arrow-alt-circle-right:before { content: fa-content($fa-var-arrow-alt-circle-right); } +.#{$fa-css-prefix}-arrow-alt-circle-up:before { content: fa-content($fa-var-arrow-alt-circle-up); } +.#{$fa-css-prefix}-arrow-circle-down:before { content: fa-content($fa-var-arrow-circle-down); } +.#{$fa-css-prefix}-arrow-circle-left:before { content: fa-content($fa-var-arrow-circle-left); } +.#{$fa-css-prefix}-arrow-circle-right:before { content: fa-content($fa-var-arrow-circle-right); } +.#{$fa-css-prefix}-arrow-circle-up:before { content: fa-content($fa-var-arrow-circle-up); } +.#{$fa-css-prefix}-arrow-down:before { content: fa-content($fa-var-arrow-down); } +.#{$fa-css-prefix}-arrow-left:before { content: fa-content($fa-var-arrow-left); } +.#{$fa-css-prefix}-arrow-right:before { content: fa-content($fa-var-arrow-right); } +.#{$fa-css-prefix}-arrow-up:before { content: fa-content($fa-var-arrow-up); } +.#{$fa-css-prefix}-arrows-alt:before { content: fa-content($fa-var-arrows-alt); } +.#{$fa-css-prefix}-arrows-alt-h:before { content: fa-content($fa-var-arrows-alt-h); } +.#{$fa-css-prefix}-arrows-alt-v:before { content: fa-content($fa-var-arrows-alt-v); } +.#{$fa-css-prefix}-artstation:before { content: fa-content($fa-var-artstation); } +.#{$fa-css-prefix}-assistive-listening-systems:before { content: fa-content($fa-var-assistive-listening-systems); } +.#{$fa-css-prefix}-asterisk:before { content: fa-content($fa-var-asterisk); } +.#{$fa-css-prefix}-asymmetrik:before { content: fa-content($fa-var-asymmetrik); } +.#{$fa-css-prefix}-at:before { content: fa-content($fa-var-at); } +.#{$fa-css-prefix}-atlas:before { content: fa-content($fa-var-atlas); } +.#{$fa-css-prefix}-atlassian:before { content: fa-content($fa-var-atlassian); } +.#{$fa-css-prefix}-atom:before { content: fa-content($fa-var-atom); } +.#{$fa-css-prefix}-audible:before { content: fa-content($fa-var-audible); } +.#{$fa-css-prefix}-audio-description:before { content: fa-content($fa-var-audio-description); } +.#{$fa-css-prefix}-autoprefixer:before { content: fa-content($fa-var-autoprefixer); } +.#{$fa-css-prefix}-avianex:before { content: fa-content($fa-var-avianex); } +.#{$fa-css-prefix}-aviato:before { content: fa-content($fa-var-aviato); } +.#{$fa-css-prefix}-award:before { content: fa-content($fa-var-award); } +.#{$fa-css-prefix}-aws:before { content: fa-content($fa-var-aws); } +.#{$fa-css-prefix}-baby:before { content: fa-content($fa-var-baby); } +.#{$fa-css-prefix}-baby-carriage:before { content: fa-content($fa-var-baby-carriage); } +.#{$fa-css-prefix}-backspace:before { content: fa-content($fa-var-backspace); } +.#{$fa-css-prefix}-backward:before { content: fa-content($fa-var-backward); } +.#{$fa-css-prefix}-bacon:before { content: fa-content($fa-var-bacon); } +.#{$fa-css-prefix}-bacteria:before { content: fa-content($fa-var-bacteria); } +.#{$fa-css-prefix}-bacterium:before { content: fa-content($fa-var-bacterium); } +.#{$fa-css-prefix}-bahai:before { content: fa-content($fa-var-bahai); } +.#{$fa-css-prefix}-balance-scale:before { content: fa-content($fa-var-balance-scale); } +.#{$fa-css-prefix}-balance-scale-left:before { content: fa-content($fa-var-balance-scale-left); } +.#{$fa-css-prefix}-balance-scale-right:before { content: fa-content($fa-var-balance-scale-right); } +.#{$fa-css-prefix}-ban:before { content: fa-content($fa-var-ban); } +.#{$fa-css-prefix}-band-aid:before { content: fa-content($fa-var-band-aid); } +.#{$fa-css-prefix}-bandcamp:before { content: fa-content($fa-var-bandcamp); } +.#{$fa-css-prefix}-barcode:before { content: fa-content($fa-var-barcode); } +.#{$fa-css-prefix}-bars:before { content: fa-content($fa-var-bars); } +.#{$fa-css-prefix}-baseball-ball:before { content: fa-content($fa-var-baseball-ball); } +.#{$fa-css-prefix}-basketball-ball:before { content: fa-content($fa-var-basketball-ball); } +.#{$fa-css-prefix}-bath:before { content: fa-content($fa-var-bath); } +.#{$fa-css-prefix}-battery-empty:before { content: fa-content($fa-var-battery-empty); } +.#{$fa-css-prefix}-battery-full:before { content: fa-content($fa-var-battery-full); } +.#{$fa-css-prefix}-battery-half:before { content: fa-content($fa-var-battery-half); } +.#{$fa-css-prefix}-battery-quarter:before { content: fa-content($fa-var-battery-quarter); } +.#{$fa-css-prefix}-battery-three-quarters:before { content: fa-content($fa-var-battery-three-quarters); } +.#{$fa-css-prefix}-battle-net:before { content: fa-content($fa-var-battle-net); } +.#{$fa-css-prefix}-bed:before { content: fa-content($fa-var-bed); } +.#{$fa-css-prefix}-beer:before { content: fa-content($fa-var-beer); } +.#{$fa-css-prefix}-behance:before { content: fa-content($fa-var-behance); } +.#{$fa-css-prefix}-behance-square:before { content: fa-content($fa-var-behance-square); } +.#{$fa-css-prefix}-bell:before { content: fa-content($fa-var-bell); } +.#{$fa-css-prefix}-bell-slash:before { content: fa-content($fa-var-bell-slash); } +.#{$fa-css-prefix}-bezier-curve:before { content: fa-content($fa-var-bezier-curve); } +.#{$fa-css-prefix}-bible:before { content: fa-content($fa-var-bible); } +.#{$fa-css-prefix}-bicycle:before { content: fa-content($fa-var-bicycle); } +.#{$fa-css-prefix}-biking:before { content: fa-content($fa-var-biking); } +.#{$fa-css-prefix}-bimobject:before { content: fa-content($fa-var-bimobject); } +.#{$fa-css-prefix}-binoculars:before { content: fa-content($fa-var-binoculars); } +.#{$fa-css-prefix}-biohazard:before { content: fa-content($fa-var-biohazard); } +.#{$fa-css-prefix}-birthday-cake:before { content: fa-content($fa-var-birthday-cake); } +.#{$fa-css-prefix}-bitbucket:before { content: fa-content($fa-var-bitbucket); } +.#{$fa-css-prefix}-bitcoin:before { content: fa-content($fa-var-bitcoin); } +.#{$fa-css-prefix}-bity:before { content: fa-content($fa-var-bity); } +.#{$fa-css-prefix}-black-tie:before { content: fa-content($fa-var-black-tie); } +.#{$fa-css-prefix}-blackberry:before { content: fa-content($fa-var-blackberry); } +.#{$fa-css-prefix}-blender:before { content: fa-content($fa-var-blender); } +.#{$fa-css-prefix}-blender-phone:before { content: fa-content($fa-var-blender-phone); } +.#{$fa-css-prefix}-blind:before { content: fa-content($fa-var-blind); } +.#{$fa-css-prefix}-blog:before { content: fa-content($fa-var-blog); } +.#{$fa-css-prefix}-blogger:before { content: fa-content($fa-var-blogger); } +.#{$fa-css-prefix}-blogger-b:before { content: fa-content($fa-var-blogger-b); } +.#{$fa-css-prefix}-bluetooth:before { content: fa-content($fa-var-bluetooth); } +.#{$fa-css-prefix}-bluetooth-b:before { content: fa-content($fa-var-bluetooth-b); } +.#{$fa-css-prefix}-bold:before { content: fa-content($fa-var-bold); } +.#{$fa-css-prefix}-bolt:before { content: fa-content($fa-var-bolt); } +.#{$fa-css-prefix}-bomb:before { content: fa-content($fa-var-bomb); } +.#{$fa-css-prefix}-bone:before { content: fa-content($fa-var-bone); } +.#{$fa-css-prefix}-bong:before { content: fa-content($fa-var-bong); } +.#{$fa-css-prefix}-book:before { content: fa-content($fa-var-book); } +.#{$fa-css-prefix}-book-dead:before { content: fa-content($fa-var-book-dead); } +.#{$fa-css-prefix}-book-medical:before { content: fa-content($fa-var-book-medical); } +.#{$fa-css-prefix}-book-open:before { content: fa-content($fa-var-book-open); } +.#{$fa-css-prefix}-book-reader:before { content: fa-content($fa-var-book-reader); } +.#{$fa-css-prefix}-bookmark:before { content: fa-content($fa-var-bookmark); } +.#{$fa-css-prefix}-bootstrap:before { content: fa-content($fa-var-bootstrap); } +.#{$fa-css-prefix}-border-all:before { content: fa-content($fa-var-border-all); } +.#{$fa-css-prefix}-border-none:before { content: fa-content($fa-var-border-none); } +.#{$fa-css-prefix}-border-style:before { content: fa-content($fa-var-border-style); } +.#{$fa-css-prefix}-bowling-ball:before { content: fa-content($fa-var-bowling-ball); } +.#{$fa-css-prefix}-box:before { content: fa-content($fa-var-box); } +.#{$fa-css-prefix}-box-open:before { content: fa-content($fa-var-box-open); } +.#{$fa-css-prefix}-box-tissue:before { content: fa-content($fa-var-box-tissue); } +.#{$fa-css-prefix}-boxes:before { content: fa-content($fa-var-boxes); } +.#{$fa-css-prefix}-braille:before { content: fa-content($fa-var-braille); } +.#{$fa-css-prefix}-brain:before { content: fa-content($fa-var-brain); } +.#{$fa-css-prefix}-bread-slice:before { content: fa-content($fa-var-bread-slice); } +.#{$fa-css-prefix}-briefcase:before { content: fa-content($fa-var-briefcase); } +.#{$fa-css-prefix}-briefcase-medical:before { content: fa-content($fa-var-briefcase-medical); } +.#{$fa-css-prefix}-broadcast-tower:before { content: fa-content($fa-var-broadcast-tower); } +.#{$fa-css-prefix}-broom:before { content: fa-content($fa-var-broom); } +.#{$fa-css-prefix}-brush:before { content: fa-content($fa-var-brush); } +.#{$fa-css-prefix}-btc:before { content: fa-content($fa-var-btc); } +.#{$fa-css-prefix}-buffer:before { content: fa-content($fa-var-buffer); } +.#{$fa-css-prefix}-bug:before { content: fa-content($fa-var-bug); } +.#{$fa-css-prefix}-building:before { content: fa-content($fa-var-building); } +.#{$fa-css-prefix}-bullhorn:before { content: fa-content($fa-var-bullhorn); } +.#{$fa-css-prefix}-bullseye:before { content: fa-content($fa-var-bullseye); } +.#{$fa-css-prefix}-burn:before { content: fa-content($fa-var-burn); } +.#{$fa-css-prefix}-buromobelexperte:before { content: fa-content($fa-var-buromobelexperte); } +.#{$fa-css-prefix}-bus:before { content: fa-content($fa-var-bus); } +.#{$fa-css-prefix}-bus-alt:before { content: fa-content($fa-var-bus-alt); } +.#{$fa-css-prefix}-business-time:before { content: fa-content($fa-var-business-time); } +.#{$fa-css-prefix}-buy-n-large:before { content: fa-content($fa-var-buy-n-large); } +.#{$fa-css-prefix}-buysellads:before { content: fa-content($fa-var-buysellads); } +.#{$fa-css-prefix}-calculator:before { content: fa-content($fa-var-calculator); } +.#{$fa-css-prefix}-calendar:before { content: fa-content($fa-var-calendar); } +.#{$fa-css-prefix}-calendar-alt:before { content: fa-content($fa-var-calendar-alt); } +.#{$fa-css-prefix}-calendar-check:before { content: fa-content($fa-var-calendar-check); } +.#{$fa-css-prefix}-calendar-day:before { content: fa-content($fa-var-calendar-day); } +.#{$fa-css-prefix}-calendar-minus:before { content: fa-content($fa-var-calendar-minus); } +.#{$fa-css-prefix}-calendar-plus:before { content: fa-content($fa-var-calendar-plus); } +.#{$fa-css-prefix}-calendar-times:before { content: fa-content($fa-var-calendar-times); } +.#{$fa-css-prefix}-calendar-week:before { content: fa-content($fa-var-calendar-week); } +.#{$fa-css-prefix}-camera:before { content: fa-content($fa-var-camera); } +.#{$fa-css-prefix}-camera-retro:before { content: fa-content($fa-var-camera-retro); } +.#{$fa-css-prefix}-campground:before { content: fa-content($fa-var-campground); } +.#{$fa-css-prefix}-canadian-maple-leaf:before { content: fa-content($fa-var-canadian-maple-leaf); } +.#{$fa-css-prefix}-candy-cane:before { content: fa-content($fa-var-candy-cane); } +.#{$fa-css-prefix}-cannabis:before { content: fa-content($fa-var-cannabis); } +.#{$fa-css-prefix}-capsules:before { content: fa-content($fa-var-capsules); } +.#{$fa-css-prefix}-car:before { content: fa-content($fa-var-car); } +.#{$fa-css-prefix}-car-alt:before { content: fa-content($fa-var-car-alt); } +.#{$fa-css-prefix}-car-battery:before { content: fa-content($fa-var-car-battery); } +.#{$fa-css-prefix}-car-crash:before { content: fa-content($fa-var-car-crash); } +.#{$fa-css-prefix}-car-side:before { content: fa-content($fa-var-car-side); } +.#{$fa-css-prefix}-caravan:before { content: fa-content($fa-var-caravan); } +.#{$fa-css-prefix}-caret-down:before { content: fa-content($fa-var-caret-down); } +.#{$fa-css-prefix}-caret-left:before { content: fa-content($fa-var-caret-left); } +.#{$fa-css-prefix}-caret-right:before { content: fa-content($fa-var-caret-right); } +.#{$fa-css-prefix}-caret-square-down:before { content: fa-content($fa-var-caret-square-down); } +.#{$fa-css-prefix}-caret-square-left:before { content: fa-content($fa-var-caret-square-left); } +.#{$fa-css-prefix}-caret-square-right:before { content: fa-content($fa-var-caret-square-right); } +.#{$fa-css-prefix}-caret-square-up:before { content: fa-content($fa-var-caret-square-up); } +.#{$fa-css-prefix}-caret-up:before { content: fa-content($fa-var-caret-up); } +.#{$fa-css-prefix}-carrot:before { content: fa-content($fa-var-carrot); } +.#{$fa-css-prefix}-cart-arrow-down:before { content: fa-content($fa-var-cart-arrow-down); } +.#{$fa-css-prefix}-cart-plus:before { content: fa-content($fa-var-cart-plus); } +.#{$fa-css-prefix}-cash-register:before { content: fa-content($fa-var-cash-register); } +.#{$fa-css-prefix}-cat:before { content: fa-content($fa-var-cat); } +.#{$fa-css-prefix}-cc-amazon-pay:before { content: fa-content($fa-var-cc-amazon-pay); } +.#{$fa-css-prefix}-cc-amex:before { content: fa-content($fa-var-cc-amex); } +.#{$fa-css-prefix}-cc-apple-pay:before { content: fa-content($fa-var-cc-apple-pay); } +.#{$fa-css-prefix}-cc-diners-club:before { content: fa-content($fa-var-cc-diners-club); } +.#{$fa-css-prefix}-cc-discover:before { content: fa-content($fa-var-cc-discover); } +.#{$fa-css-prefix}-cc-jcb:before { content: fa-content($fa-var-cc-jcb); } +.#{$fa-css-prefix}-cc-mastercard:before { content: fa-content($fa-var-cc-mastercard); } +.#{$fa-css-prefix}-cc-paypal:before { content: fa-content($fa-var-cc-paypal); } +.#{$fa-css-prefix}-cc-stripe:before { content: fa-content($fa-var-cc-stripe); } +.#{$fa-css-prefix}-cc-visa:before { content: fa-content($fa-var-cc-visa); } +.#{$fa-css-prefix}-centercode:before { content: fa-content($fa-var-centercode); } +.#{$fa-css-prefix}-centos:before { content: fa-content($fa-var-centos); } +.#{$fa-css-prefix}-certificate:before { content: fa-content($fa-var-certificate); } +.#{$fa-css-prefix}-chair:before { content: fa-content($fa-var-chair); } +.#{$fa-css-prefix}-chalkboard:before { content: fa-content($fa-var-chalkboard); } +.#{$fa-css-prefix}-chalkboard-teacher:before { content: fa-content($fa-var-chalkboard-teacher); } +.#{$fa-css-prefix}-charging-station:before { content: fa-content($fa-var-charging-station); } +.#{$fa-css-prefix}-chart-area:before { content: fa-content($fa-var-chart-area); } +.#{$fa-css-prefix}-chart-bar:before { content: fa-content($fa-var-chart-bar); } +.#{$fa-css-prefix}-chart-line:before { content: fa-content($fa-var-chart-line); } +.#{$fa-css-prefix}-chart-pie:before { content: fa-content($fa-var-chart-pie); } +.#{$fa-css-prefix}-check:before { content: fa-content($fa-var-check); } +.#{$fa-css-prefix}-check-circle:before { content: fa-content($fa-var-check-circle); } +.#{$fa-css-prefix}-check-double:before { content: fa-content($fa-var-check-double); } +.#{$fa-css-prefix}-check-square:before { content: fa-content($fa-var-check-square); } +.#{$fa-css-prefix}-cheese:before { content: fa-content($fa-var-cheese); } +.#{$fa-css-prefix}-chess:before { content: fa-content($fa-var-chess); } +.#{$fa-css-prefix}-chess-bishop:before { content: fa-content($fa-var-chess-bishop); } +.#{$fa-css-prefix}-chess-board:before { content: fa-content($fa-var-chess-board); } +.#{$fa-css-prefix}-chess-king:before { content: fa-content($fa-var-chess-king); } +.#{$fa-css-prefix}-chess-knight:before { content: fa-content($fa-var-chess-knight); } +.#{$fa-css-prefix}-chess-pawn:before { content: fa-content($fa-var-chess-pawn); } +.#{$fa-css-prefix}-chess-queen:before { content: fa-content($fa-var-chess-queen); } +.#{$fa-css-prefix}-chess-rook:before { content: fa-content($fa-var-chess-rook); } +.#{$fa-css-prefix}-chevron-circle-down:before { content: fa-content($fa-var-chevron-circle-down); } +.#{$fa-css-prefix}-chevron-circle-left:before { content: fa-content($fa-var-chevron-circle-left); } +.#{$fa-css-prefix}-chevron-circle-right:before { content: fa-content($fa-var-chevron-circle-right); } +.#{$fa-css-prefix}-chevron-circle-up:before { content: fa-content($fa-var-chevron-circle-up); } +.#{$fa-css-prefix}-chevron-down:before { content: fa-content($fa-var-chevron-down); } +.#{$fa-css-prefix}-chevron-left:before { content: fa-content($fa-var-chevron-left); } +.#{$fa-css-prefix}-chevron-right:before { content: fa-content($fa-var-chevron-right); } +.#{$fa-css-prefix}-chevron-up:before { content: fa-content($fa-var-chevron-up); } +.#{$fa-css-prefix}-child:before { content: fa-content($fa-var-child); } +.#{$fa-css-prefix}-chrome:before { content: fa-content($fa-var-chrome); } +.#{$fa-css-prefix}-chromecast:before { content: fa-content($fa-var-chromecast); } +.#{$fa-css-prefix}-church:before { content: fa-content($fa-var-church); } +.#{$fa-css-prefix}-circle:before { content: fa-content($fa-var-circle); } +.#{$fa-css-prefix}-circle-notch:before { content: fa-content($fa-var-circle-notch); } +.#{$fa-css-prefix}-city:before { content: fa-content($fa-var-city); } +.#{$fa-css-prefix}-clinic-medical:before { content: fa-content($fa-var-clinic-medical); } +.#{$fa-css-prefix}-clipboard:before { content: fa-content($fa-var-clipboard); } +.#{$fa-css-prefix}-clipboard-check:before { content: fa-content($fa-var-clipboard-check); } +.#{$fa-css-prefix}-clipboard-list:before { content: fa-content($fa-var-clipboard-list); } +.#{$fa-css-prefix}-clock:before { content: fa-content($fa-var-clock); } +.#{$fa-css-prefix}-clone:before { content: fa-content($fa-var-clone); } +.#{$fa-css-prefix}-closed-captioning:before { content: fa-content($fa-var-closed-captioning); } +.#{$fa-css-prefix}-cloud:before { content: fa-content($fa-var-cloud); } +.#{$fa-css-prefix}-cloud-download-alt:before { content: fa-content($fa-var-cloud-download-alt); } +.#{$fa-css-prefix}-cloud-meatball:before { content: fa-content($fa-var-cloud-meatball); } +.#{$fa-css-prefix}-cloud-moon:before { content: fa-content($fa-var-cloud-moon); } +.#{$fa-css-prefix}-cloud-moon-rain:before { content: fa-content($fa-var-cloud-moon-rain); } +.#{$fa-css-prefix}-cloud-rain:before { content: fa-content($fa-var-cloud-rain); } +.#{$fa-css-prefix}-cloud-showers-heavy:before { content: fa-content($fa-var-cloud-showers-heavy); } +.#{$fa-css-prefix}-cloud-sun:before { content: fa-content($fa-var-cloud-sun); } +.#{$fa-css-prefix}-cloud-sun-rain:before { content: fa-content($fa-var-cloud-sun-rain); } +.#{$fa-css-prefix}-cloud-upload-alt:before { content: fa-content($fa-var-cloud-upload-alt); } +.#{$fa-css-prefix}-cloudscale:before { content: fa-content($fa-var-cloudscale); } +.#{$fa-css-prefix}-cloudsmith:before { content: fa-content($fa-var-cloudsmith); } +.#{$fa-css-prefix}-cloudversify:before { content: fa-content($fa-var-cloudversify); } +.#{$fa-css-prefix}-cocktail:before { content: fa-content($fa-var-cocktail); } +.#{$fa-css-prefix}-code:before { content: fa-content($fa-var-code); } +.#{$fa-css-prefix}-code-branch:before { content: fa-content($fa-var-code-branch); } +.#{$fa-css-prefix}-codepen:before { content: fa-content($fa-var-codepen); } +.#{$fa-css-prefix}-codiepie:before { content: fa-content($fa-var-codiepie); } +.#{$fa-css-prefix}-coffee:before { content: fa-content($fa-var-coffee); } +.#{$fa-css-prefix}-cog:before { content: fa-content($fa-var-cog); } +.#{$fa-css-prefix}-cogs:before { content: fa-content($fa-var-cogs); } +.#{$fa-css-prefix}-coins:before { content: fa-content($fa-var-coins); } +.#{$fa-css-prefix}-columns:before { content: fa-content($fa-var-columns); } +.#{$fa-css-prefix}-comment:before { content: fa-content($fa-var-comment); } +.#{$fa-css-prefix}-comment-alt:before { content: fa-content($fa-var-comment-alt); } +.#{$fa-css-prefix}-comment-dollar:before { content: fa-content($fa-var-comment-dollar); } +.#{$fa-css-prefix}-comment-dots:before { content: fa-content($fa-var-comment-dots); } +.#{$fa-css-prefix}-comment-medical:before { content: fa-content($fa-var-comment-medical); } +.#{$fa-css-prefix}-comment-slash:before { content: fa-content($fa-var-comment-slash); } +.#{$fa-css-prefix}-comments:before { content: fa-content($fa-var-comments); } +.#{$fa-css-prefix}-comments-dollar:before { content: fa-content($fa-var-comments-dollar); } +.#{$fa-css-prefix}-compact-disc:before { content: fa-content($fa-var-compact-disc); } +.#{$fa-css-prefix}-compass:before { content: fa-content($fa-var-compass); } +.#{$fa-css-prefix}-compress:before { content: fa-content($fa-var-compress); } +.#{$fa-css-prefix}-compress-alt:before { content: fa-content($fa-var-compress-alt); } +.#{$fa-css-prefix}-compress-arrows-alt:before { content: fa-content($fa-var-compress-arrows-alt); } +.#{$fa-css-prefix}-concierge-bell:before { content: fa-content($fa-var-concierge-bell); } +.#{$fa-css-prefix}-confluence:before { content: fa-content($fa-var-confluence); } +.#{$fa-css-prefix}-connectdevelop:before { content: fa-content($fa-var-connectdevelop); } +.#{$fa-css-prefix}-contao:before { content: fa-content($fa-var-contao); } +.#{$fa-css-prefix}-cookie:before { content: fa-content($fa-var-cookie); } +.#{$fa-css-prefix}-cookie-bite:before { content: fa-content($fa-var-cookie-bite); } +.#{$fa-css-prefix}-copy:before { content: fa-content($fa-var-copy); } +.#{$fa-css-prefix}-copyright:before { content: fa-content($fa-var-copyright); } +.#{$fa-css-prefix}-cotton-bureau:before { content: fa-content($fa-var-cotton-bureau); } +.#{$fa-css-prefix}-couch:before { content: fa-content($fa-var-couch); } +.#{$fa-css-prefix}-cpanel:before { content: fa-content($fa-var-cpanel); } +.#{$fa-css-prefix}-creative-commons:before { content: fa-content($fa-var-creative-commons); } +.#{$fa-css-prefix}-creative-commons-by:before { content: fa-content($fa-var-creative-commons-by); } +.#{$fa-css-prefix}-creative-commons-nc:before { content: fa-content($fa-var-creative-commons-nc); } +.#{$fa-css-prefix}-creative-commons-nc-eu:before { content: fa-content($fa-var-creative-commons-nc-eu); } +.#{$fa-css-prefix}-creative-commons-nc-jp:before { content: fa-content($fa-var-creative-commons-nc-jp); } +.#{$fa-css-prefix}-creative-commons-nd:before { content: fa-content($fa-var-creative-commons-nd); } +.#{$fa-css-prefix}-creative-commons-pd:before { content: fa-content($fa-var-creative-commons-pd); } +.#{$fa-css-prefix}-creative-commons-pd-alt:before { content: fa-content($fa-var-creative-commons-pd-alt); } +.#{$fa-css-prefix}-creative-commons-remix:before { content: fa-content($fa-var-creative-commons-remix); } +.#{$fa-css-prefix}-creative-commons-sa:before { content: fa-content($fa-var-creative-commons-sa); } +.#{$fa-css-prefix}-creative-commons-sampling:before { content: fa-content($fa-var-creative-commons-sampling); } +.#{$fa-css-prefix}-creative-commons-sampling-plus:before { content: fa-content($fa-var-creative-commons-sampling-plus); } +.#{$fa-css-prefix}-creative-commons-share:before { content: fa-content($fa-var-creative-commons-share); } +.#{$fa-css-prefix}-creative-commons-zero:before { content: fa-content($fa-var-creative-commons-zero); } +.#{$fa-css-prefix}-credit-card:before { content: fa-content($fa-var-credit-card); } +.#{$fa-css-prefix}-critical-role:before { content: fa-content($fa-var-critical-role); } +.#{$fa-css-prefix}-crop:before { content: fa-content($fa-var-crop); } +.#{$fa-css-prefix}-crop-alt:before { content: fa-content($fa-var-crop-alt); } +.#{$fa-css-prefix}-cross:before { content: fa-content($fa-var-cross); } +.#{$fa-css-prefix}-crosshairs:before { content: fa-content($fa-var-crosshairs); } +.#{$fa-css-prefix}-crow:before { content: fa-content($fa-var-crow); } +.#{$fa-css-prefix}-crown:before { content: fa-content($fa-var-crown); } +.#{$fa-css-prefix}-crutch:before { content: fa-content($fa-var-crutch); } +.#{$fa-css-prefix}-css3:before { content: fa-content($fa-var-css3); } +.#{$fa-css-prefix}-css3-alt:before { content: fa-content($fa-var-css3-alt); } +.#{$fa-css-prefix}-cube:before { content: fa-content($fa-var-cube); } +.#{$fa-css-prefix}-cubes:before { content: fa-content($fa-var-cubes); } +.#{$fa-css-prefix}-cut:before { content: fa-content($fa-var-cut); } +.#{$fa-css-prefix}-cuttlefish:before { content: fa-content($fa-var-cuttlefish); } +.#{$fa-css-prefix}-d-and-d:before { content: fa-content($fa-var-d-and-d); } +.#{$fa-css-prefix}-d-and-d-beyond:before { content: fa-content($fa-var-d-and-d-beyond); } +.#{$fa-css-prefix}-dailymotion:before { content: fa-content($fa-var-dailymotion); } +.#{$fa-css-prefix}-dashcube:before { content: fa-content($fa-var-dashcube); } +.#{$fa-css-prefix}-database:before { content: fa-content($fa-var-database); } +.#{$fa-css-prefix}-deaf:before { content: fa-content($fa-var-deaf); } +.#{$fa-css-prefix}-deezer:before { content: fa-content($fa-var-deezer); } +.#{$fa-css-prefix}-delicious:before { content: fa-content($fa-var-delicious); } +.#{$fa-css-prefix}-democrat:before { content: fa-content($fa-var-democrat); } +.#{$fa-css-prefix}-deploydog:before { content: fa-content($fa-var-deploydog); } +.#{$fa-css-prefix}-deskpro:before { content: fa-content($fa-var-deskpro); } +.#{$fa-css-prefix}-desktop:before { content: fa-content($fa-var-desktop); } +.#{$fa-css-prefix}-dev:before { content: fa-content($fa-var-dev); } +.#{$fa-css-prefix}-deviantart:before { content: fa-content($fa-var-deviantart); } +.#{$fa-css-prefix}-dharmachakra:before { content: fa-content($fa-var-dharmachakra); } +.#{$fa-css-prefix}-dhl:before { content: fa-content($fa-var-dhl); } +.#{$fa-css-prefix}-diagnoses:before { content: fa-content($fa-var-diagnoses); } +.#{$fa-css-prefix}-diaspora:before { content: fa-content($fa-var-diaspora); } +.#{$fa-css-prefix}-dice:before { content: fa-content($fa-var-dice); } +.#{$fa-css-prefix}-dice-d20:before { content: fa-content($fa-var-dice-d20); } +.#{$fa-css-prefix}-dice-d6:before { content: fa-content($fa-var-dice-d6); } +.#{$fa-css-prefix}-dice-five:before { content: fa-content($fa-var-dice-five); } +.#{$fa-css-prefix}-dice-four:before { content: fa-content($fa-var-dice-four); } +.#{$fa-css-prefix}-dice-one:before { content: fa-content($fa-var-dice-one); } +.#{$fa-css-prefix}-dice-six:before { content: fa-content($fa-var-dice-six); } +.#{$fa-css-prefix}-dice-three:before { content: fa-content($fa-var-dice-three); } +.#{$fa-css-prefix}-dice-two:before { content: fa-content($fa-var-dice-two); } +.#{$fa-css-prefix}-digg:before { content: fa-content($fa-var-digg); } +.#{$fa-css-prefix}-digital-ocean:before { content: fa-content($fa-var-digital-ocean); } +.#{$fa-css-prefix}-digital-tachograph:before { content: fa-content($fa-var-digital-tachograph); } +.#{$fa-css-prefix}-directions:before { content: fa-content($fa-var-directions); } +.#{$fa-css-prefix}-discord:before { content: fa-content($fa-var-discord); } +.#{$fa-css-prefix}-discourse:before { content: fa-content($fa-var-discourse); } +.#{$fa-css-prefix}-disease:before { content: fa-content($fa-var-disease); } +.#{$fa-css-prefix}-divide:before { content: fa-content($fa-var-divide); } +.#{$fa-css-prefix}-dizzy:before { content: fa-content($fa-var-dizzy); } +.#{$fa-css-prefix}-dna:before { content: fa-content($fa-var-dna); } +.#{$fa-css-prefix}-dochub:before { content: fa-content($fa-var-dochub); } +.#{$fa-css-prefix}-docker:before { content: fa-content($fa-var-docker); } +.#{$fa-css-prefix}-dog:before { content: fa-content($fa-var-dog); } +.#{$fa-css-prefix}-dollar-sign:before { content: fa-content($fa-var-dollar-sign); } +.#{$fa-css-prefix}-dolly:before { content: fa-content($fa-var-dolly); } +.#{$fa-css-prefix}-dolly-flatbed:before { content: fa-content($fa-var-dolly-flatbed); } +.#{$fa-css-prefix}-donate:before { content: fa-content($fa-var-donate); } +.#{$fa-css-prefix}-door-closed:before { content: fa-content($fa-var-door-closed); } +.#{$fa-css-prefix}-door-open:before { content: fa-content($fa-var-door-open); } +.#{$fa-css-prefix}-dot-circle:before { content: fa-content($fa-var-dot-circle); } +.#{$fa-css-prefix}-dove:before { content: fa-content($fa-var-dove); } +.#{$fa-css-prefix}-download:before { content: fa-content($fa-var-download); } +.#{$fa-css-prefix}-draft2digital:before { content: fa-content($fa-var-draft2digital); } +.#{$fa-css-prefix}-drafting-compass:before { content: fa-content($fa-var-drafting-compass); } +.#{$fa-css-prefix}-dragon:before { content: fa-content($fa-var-dragon); } +.#{$fa-css-prefix}-draw-polygon:before { content: fa-content($fa-var-draw-polygon); } +.#{$fa-css-prefix}-dribbble:before { content: fa-content($fa-var-dribbble); } +.#{$fa-css-prefix}-dribbble-square:before { content: fa-content($fa-var-dribbble-square); } +.#{$fa-css-prefix}-dropbox:before { content: fa-content($fa-var-dropbox); } +.#{$fa-css-prefix}-drum:before { content: fa-content($fa-var-drum); } +.#{$fa-css-prefix}-drum-steelpan:before { content: fa-content($fa-var-drum-steelpan); } +.#{$fa-css-prefix}-drumstick-bite:before { content: fa-content($fa-var-drumstick-bite); } +.#{$fa-css-prefix}-drupal:before { content: fa-content($fa-var-drupal); } +.#{$fa-css-prefix}-dumbbell:before { content: fa-content($fa-var-dumbbell); } +.#{$fa-css-prefix}-dumpster:before { content: fa-content($fa-var-dumpster); } +.#{$fa-css-prefix}-dumpster-fire:before { content: fa-content($fa-var-dumpster-fire); } +.#{$fa-css-prefix}-dungeon:before { content: fa-content($fa-var-dungeon); } +.#{$fa-css-prefix}-dyalog:before { content: fa-content($fa-var-dyalog); } +.#{$fa-css-prefix}-earlybirds:before { content: fa-content($fa-var-earlybirds); } +.#{$fa-css-prefix}-ebay:before { content: fa-content($fa-var-ebay); } +.#{$fa-css-prefix}-edge:before { content: fa-content($fa-var-edge); } +.#{$fa-css-prefix}-edge-legacy:before { content: fa-content($fa-var-edge-legacy); } +.#{$fa-css-prefix}-edit:before { content: fa-content($fa-var-edit); } +.#{$fa-css-prefix}-egg:before { content: fa-content($fa-var-egg); } +.#{$fa-css-prefix}-eject:before { content: fa-content($fa-var-eject); } +.#{$fa-css-prefix}-elementor:before { content: fa-content($fa-var-elementor); } +.#{$fa-css-prefix}-ellipsis-h:before { content: fa-content($fa-var-ellipsis-h); } +.#{$fa-css-prefix}-ellipsis-v:before { content: fa-content($fa-var-ellipsis-v); } +.#{$fa-css-prefix}-ello:before { content: fa-content($fa-var-ello); } +.#{$fa-css-prefix}-ember:before { content: fa-content($fa-var-ember); } +.#{$fa-css-prefix}-empire:before { content: fa-content($fa-var-empire); } +.#{$fa-css-prefix}-envelope:before { content: fa-content($fa-var-envelope); } +.#{$fa-css-prefix}-envelope-open:before { content: fa-content($fa-var-envelope-open); } +.#{$fa-css-prefix}-envelope-open-text:before { content: fa-content($fa-var-envelope-open-text); } +.#{$fa-css-prefix}-envelope-square:before { content: fa-content($fa-var-envelope-square); } +.#{$fa-css-prefix}-envira:before { content: fa-content($fa-var-envira); } +.#{$fa-css-prefix}-equals:before { content: fa-content($fa-var-equals); } +.#{$fa-css-prefix}-eraser:before { content: fa-content($fa-var-eraser); } +.#{$fa-css-prefix}-erlang:before { content: fa-content($fa-var-erlang); } +.#{$fa-css-prefix}-ethereum:before { content: fa-content($fa-var-ethereum); } +.#{$fa-css-prefix}-ethernet:before { content: fa-content($fa-var-ethernet); } +.#{$fa-css-prefix}-etsy:before { content: fa-content($fa-var-etsy); } +.#{$fa-css-prefix}-euro-sign:before { content: fa-content($fa-var-euro-sign); } +.#{$fa-css-prefix}-evernote:before { content: fa-content($fa-var-evernote); } +.#{$fa-css-prefix}-exchange-alt:before { content: fa-content($fa-var-exchange-alt); } +.#{$fa-css-prefix}-exclamation:before { content: fa-content($fa-var-exclamation); } +.#{$fa-css-prefix}-exclamation-circle:before { content: fa-content($fa-var-exclamation-circle); } +.#{$fa-css-prefix}-exclamation-triangle:before { content: fa-content($fa-var-exclamation-triangle); } +.#{$fa-css-prefix}-expand:before { content: fa-content($fa-var-expand); } +.#{$fa-css-prefix}-expand-alt:before { content: fa-content($fa-var-expand-alt); } +.#{$fa-css-prefix}-expand-arrows-alt:before { content: fa-content($fa-var-expand-arrows-alt); } +.#{$fa-css-prefix}-expeditedssl:before { content: fa-content($fa-var-expeditedssl); } +.#{$fa-css-prefix}-external-link-alt:before { content: fa-content($fa-var-external-link-alt); } +.#{$fa-css-prefix}-external-link-square-alt:before { content: fa-content($fa-var-external-link-square-alt); } +.#{$fa-css-prefix}-eye:before { content: fa-content($fa-var-eye); } +.#{$fa-css-prefix}-eye-dropper:before { content: fa-content($fa-var-eye-dropper); } +.#{$fa-css-prefix}-eye-slash:before { content: fa-content($fa-var-eye-slash); } +.#{$fa-css-prefix}-facebook:before { content: fa-content($fa-var-facebook); } +.#{$fa-css-prefix}-facebook-f:before { content: fa-content($fa-var-facebook-f); } +.#{$fa-css-prefix}-facebook-messenger:before { content: fa-content($fa-var-facebook-messenger); } +.#{$fa-css-prefix}-facebook-square:before { content: fa-content($fa-var-facebook-square); } +.#{$fa-css-prefix}-fan:before { content: fa-content($fa-var-fan); } +.#{$fa-css-prefix}-fantasy-flight-games:before { content: fa-content($fa-var-fantasy-flight-games); } +.#{$fa-css-prefix}-fast-backward:before { content: fa-content($fa-var-fast-backward); } +.#{$fa-css-prefix}-fast-forward:before { content: fa-content($fa-var-fast-forward); } +.#{$fa-css-prefix}-faucet:before { content: fa-content($fa-var-faucet); } +.#{$fa-css-prefix}-fax:before { content: fa-content($fa-var-fax); } +.#{$fa-css-prefix}-feather:before { content: fa-content($fa-var-feather); } +.#{$fa-css-prefix}-feather-alt:before { content: fa-content($fa-var-feather-alt); } +.#{$fa-css-prefix}-fedex:before { content: fa-content($fa-var-fedex); } +.#{$fa-css-prefix}-fedora:before { content: fa-content($fa-var-fedora); } +.#{$fa-css-prefix}-female:before { content: fa-content($fa-var-female); } +.#{$fa-css-prefix}-fighter-jet:before { content: fa-content($fa-var-fighter-jet); } +.#{$fa-css-prefix}-figma:before { content: fa-content($fa-var-figma); } +.#{$fa-css-prefix}-file:before { content: fa-content($fa-var-file); } +.#{$fa-css-prefix}-file-alt:before { content: fa-content($fa-var-file-alt); } +.#{$fa-css-prefix}-file-archive:before { content: fa-content($fa-var-file-archive); } +.#{$fa-css-prefix}-file-audio:before { content: fa-content($fa-var-file-audio); } +.#{$fa-css-prefix}-file-code:before { content: fa-content($fa-var-file-code); } +.#{$fa-css-prefix}-file-contract:before { content: fa-content($fa-var-file-contract); } +.#{$fa-css-prefix}-file-csv:before { content: fa-content($fa-var-file-csv); } +.#{$fa-css-prefix}-file-download:before { content: fa-content($fa-var-file-download); } +.#{$fa-css-prefix}-file-excel:before { content: fa-content($fa-var-file-excel); } +.#{$fa-css-prefix}-file-export:before { content: fa-content($fa-var-file-export); } +.#{$fa-css-prefix}-file-image:before { content: fa-content($fa-var-file-image); } +.#{$fa-css-prefix}-file-import:before { content: fa-content($fa-var-file-import); } +.#{$fa-css-prefix}-file-invoice:before { content: fa-content($fa-var-file-invoice); } +.#{$fa-css-prefix}-file-invoice-dollar:before { content: fa-content($fa-var-file-invoice-dollar); } +.#{$fa-css-prefix}-file-medical:before { content: fa-content($fa-var-file-medical); } +.#{$fa-css-prefix}-file-medical-alt:before { content: fa-content($fa-var-file-medical-alt); } +.#{$fa-css-prefix}-file-pdf:before { content: fa-content($fa-var-file-pdf); } +.#{$fa-css-prefix}-file-powerpoint:before { content: fa-content($fa-var-file-powerpoint); } +.#{$fa-css-prefix}-file-prescription:before { content: fa-content($fa-var-file-prescription); } +.#{$fa-css-prefix}-file-signature:before { content: fa-content($fa-var-file-signature); } +.#{$fa-css-prefix}-file-upload:before { content: fa-content($fa-var-file-upload); } +.#{$fa-css-prefix}-file-video:before { content: fa-content($fa-var-file-video); } +.#{$fa-css-prefix}-file-word:before { content: fa-content($fa-var-file-word); } +.#{$fa-css-prefix}-fill:before { content: fa-content($fa-var-fill); } +.#{$fa-css-prefix}-fill-drip:before { content: fa-content($fa-var-fill-drip); } +.#{$fa-css-prefix}-film:before { content: fa-content($fa-var-film); } +.#{$fa-css-prefix}-filter:before { content: fa-content($fa-var-filter); } +.#{$fa-css-prefix}-fingerprint:before { content: fa-content($fa-var-fingerprint); } +.#{$fa-css-prefix}-fire:before { content: fa-content($fa-var-fire); } +.#{$fa-css-prefix}-fire-alt:before { content: fa-content($fa-var-fire-alt); } +.#{$fa-css-prefix}-fire-extinguisher:before { content: fa-content($fa-var-fire-extinguisher); } +.#{$fa-css-prefix}-firefox:before { content: fa-content($fa-var-firefox); } +.#{$fa-css-prefix}-firefox-browser:before { content: fa-content($fa-var-firefox-browser); } +.#{$fa-css-prefix}-first-aid:before { content: fa-content($fa-var-first-aid); } +.#{$fa-css-prefix}-first-order:before { content: fa-content($fa-var-first-order); } +.#{$fa-css-prefix}-first-order-alt:before { content: fa-content($fa-var-first-order-alt); } +.#{$fa-css-prefix}-firstdraft:before { content: fa-content($fa-var-firstdraft); } +.#{$fa-css-prefix}-fish:before { content: fa-content($fa-var-fish); } +.#{$fa-css-prefix}-fist-raised:before { content: fa-content($fa-var-fist-raised); } +.#{$fa-css-prefix}-flag:before { content: fa-content($fa-var-flag); } +.#{$fa-css-prefix}-flag-checkered:before { content: fa-content($fa-var-flag-checkered); } +.#{$fa-css-prefix}-flag-usa:before { content: fa-content($fa-var-flag-usa); } +.#{$fa-css-prefix}-flask:before { content: fa-content($fa-var-flask); } +.#{$fa-css-prefix}-flickr:before { content: fa-content($fa-var-flickr); } +.#{$fa-css-prefix}-flipboard:before { content: fa-content($fa-var-flipboard); } +.#{$fa-css-prefix}-flushed:before { content: fa-content($fa-var-flushed); } +.#{$fa-css-prefix}-fly:before { content: fa-content($fa-var-fly); } +.#{$fa-css-prefix}-folder:before { content: fa-content($fa-var-folder); } +.#{$fa-css-prefix}-folder-minus:before { content: fa-content($fa-var-folder-minus); } +.#{$fa-css-prefix}-folder-open:before { content: fa-content($fa-var-folder-open); } +.#{$fa-css-prefix}-folder-plus:before { content: fa-content($fa-var-folder-plus); } +.#{$fa-css-prefix}-font:before { content: fa-content($fa-var-font); } +.#{$fa-css-prefix}-font-awesome:before { content: fa-content($fa-var-font-awesome); } +.#{$fa-css-prefix}-font-awesome-alt:before { content: fa-content($fa-var-font-awesome-alt); } +.#{$fa-css-prefix}-font-awesome-flag:before { content: fa-content($fa-var-font-awesome-flag); } +.#{$fa-css-prefix}-font-awesome-logo-full:before { content: fa-content($fa-var-font-awesome-logo-full); } +.#{$fa-css-prefix}-fonticons:before { content: fa-content($fa-var-fonticons); } +.#{$fa-css-prefix}-fonticons-fi:before { content: fa-content($fa-var-fonticons-fi); } +.#{$fa-css-prefix}-football-ball:before { content: fa-content($fa-var-football-ball); } +.#{$fa-css-prefix}-fort-awesome:before { content: fa-content($fa-var-fort-awesome); } +.#{$fa-css-prefix}-fort-awesome-alt:before { content: fa-content($fa-var-fort-awesome-alt); } +.#{$fa-css-prefix}-forumbee:before { content: fa-content($fa-var-forumbee); } +.#{$fa-css-prefix}-forward:before { content: fa-content($fa-var-forward); } +.#{$fa-css-prefix}-foursquare:before { content: fa-content($fa-var-foursquare); } +.#{$fa-css-prefix}-free-code-camp:before { content: fa-content($fa-var-free-code-camp); } +.#{$fa-css-prefix}-freebsd:before { content: fa-content($fa-var-freebsd); } +.#{$fa-css-prefix}-frog:before { content: fa-content($fa-var-frog); } +.#{$fa-css-prefix}-frown:before { content: fa-content($fa-var-frown); } +.#{$fa-css-prefix}-frown-open:before { content: fa-content($fa-var-frown-open); } +.#{$fa-css-prefix}-fulcrum:before { content: fa-content($fa-var-fulcrum); } +.#{$fa-css-prefix}-funnel-dollar:before { content: fa-content($fa-var-funnel-dollar); } +.#{$fa-css-prefix}-futbol:before { content: fa-content($fa-var-futbol); } +.#{$fa-css-prefix}-galactic-republic:before { content: fa-content($fa-var-galactic-republic); } +.#{$fa-css-prefix}-galactic-senate:before { content: fa-content($fa-var-galactic-senate); } +.#{$fa-css-prefix}-gamepad:before { content: fa-content($fa-var-gamepad); } +.#{$fa-css-prefix}-gas-pump:before { content: fa-content($fa-var-gas-pump); } +.#{$fa-css-prefix}-gavel:before { content: fa-content($fa-var-gavel); } +.#{$fa-css-prefix}-gem:before { content: fa-content($fa-var-gem); } +.#{$fa-css-prefix}-genderless:before { content: fa-content($fa-var-genderless); } +.#{$fa-css-prefix}-get-pocket:before { content: fa-content($fa-var-get-pocket); } +.#{$fa-css-prefix}-gg:before { content: fa-content($fa-var-gg); } +.#{$fa-css-prefix}-gg-circle:before { content: fa-content($fa-var-gg-circle); } +.#{$fa-css-prefix}-ghost:before { content: fa-content($fa-var-ghost); } +.#{$fa-css-prefix}-gift:before { content: fa-content($fa-var-gift); } +.#{$fa-css-prefix}-gifts:before { content: fa-content($fa-var-gifts); } +.#{$fa-css-prefix}-git:before { content: fa-content($fa-var-git); } +.#{$fa-css-prefix}-git-alt:before { content: fa-content($fa-var-git-alt); } +.#{$fa-css-prefix}-git-square:before { content: fa-content($fa-var-git-square); } +.#{$fa-css-prefix}-github:before { content: fa-content($fa-var-github); } +.#{$fa-css-prefix}-github-alt:before { content: fa-content($fa-var-github-alt); } +.#{$fa-css-prefix}-github-square:before { content: fa-content($fa-var-github-square); } +.#{$fa-css-prefix}-gitkraken:before { content: fa-content($fa-var-gitkraken); } +.#{$fa-css-prefix}-gitlab:before { content: fa-content($fa-var-gitlab); } +.#{$fa-css-prefix}-gitter:before { content: fa-content($fa-var-gitter); } +.#{$fa-css-prefix}-glass-cheers:before { content: fa-content($fa-var-glass-cheers); } +.#{$fa-css-prefix}-glass-martini:before { content: fa-content($fa-var-glass-martini); } +.#{$fa-css-prefix}-glass-martini-alt:before { content: fa-content($fa-var-glass-martini-alt); } +.#{$fa-css-prefix}-glass-whiskey:before { content: fa-content($fa-var-glass-whiskey); } +.#{$fa-css-prefix}-glasses:before { content: fa-content($fa-var-glasses); } +.#{$fa-css-prefix}-glide:before { content: fa-content($fa-var-glide); } +.#{$fa-css-prefix}-glide-g:before { content: fa-content($fa-var-glide-g); } +.#{$fa-css-prefix}-globe:before { content: fa-content($fa-var-globe); } +.#{$fa-css-prefix}-globe-africa:before { content: fa-content($fa-var-globe-africa); } +.#{$fa-css-prefix}-globe-americas:before { content: fa-content($fa-var-globe-americas); } +.#{$fa-css-prefix}-globe-asia:before { content: fa-content($fa-var-globe-asia); } +.#{$fa-css-prefix}-globe-europe:before { content: fa-content($fa-var-globe-europe); } +.#{$fa-css-prefix}-gofore:before { content: fa-content($fa-var-gofore); } +.#{$fa-css-prefix}-golf-ball:before { content: fa-content($fa-var-golf-ball); } +.#{$fa-css-prefix}-goodreads:before { content: fa-content($fa-var-goodreads); } +.#{$fa-css-prefix}-goodreads-g:before { content: fa-content($fa-var-goodreads-g); } +.#{$fa-css-prefix}-google:before { content: fa-content($fa-var-google); } +.#{$fa-css-prefix}-google-drive:before { content: fa-content($fa-var-google-drive); } +.#{$fa-css-prefix}-google-pay:before { content: fa-content($fa-var-google-pay); } +.#{$fa-css-prefix}-google-play:before { content: fa-content($fa-var-google-play); } +.#{$fa-css-prefix}-google-plus:before { content: fa-content($fa-var-google-plus); } +.#{$fa-css-prefix}-google-plus-g:before { content: fa-content($fa-var-google-plus-g); } +.#{$fa-css-prefix}-google-plus-square:before { content: fa-content($fa-var-google-plus-square); } +.#{$fa-css-prefix}-google-wallet:before { content: fa-content($fa-var-google-wallet); } +.#{$fa-css-prefix}-gopuram:before { content: fa-content($fa-var-gopuram); } +.#{$fa-css-prefix}-graduation-cap:before { content: fa-content($fa-var-graduation-cap); } +.#{$fa-css-prefix}-gratipay:before { content: fa-content($fa-var-gratipay); } +.#{$fa-css-prefix}-grav:before { content: fa-content($fa-var-grav); } +.#{$fa-css-prefix}-greater-than:before { content: fa-content($fa-var-greater-than); } +.#{$fa-css-prefix}-greater-than-equal:before { content: fa-content($fa-var-greater-than-equal); } +.#{$fa-css-prefix}-grimace:before { content: fa-content($fa-var-grimace); } +.#{$fa-css-prefix}-grin:before { content: fa-content($fa-var-grin); } +.#{$fa-css-prefix}-grin-alt:before { content: fa-content($fa-var-grin-alt); } +.#{$fa-css-prefix}-grin-beam:before { content: fa-content($fa-var-grin-beam); } +.#{$fa-css-prefix}-grin-beam-sweat:before { content: fa-content($fa-var-grin-beam-sweat); } +.#{$fa-css-prefix}-grin-hearts:before { content: fa-content($fa-var-grin-hearts); } +.#{$fa-css-prefix}-grin-squint:before { content: fa-content($fa-var-grin-squint); } +.#{$fa-css-prefix}-grin-squint-tears:before { content: fa-content($fa-var-grin-squint-tears); } +.#{$fa-css-prefix}-grin-stars:before { content: fa-content($fa-var-grin-stars); } +.#{$fa-css-prefix}-grin-tears:before { content: fa-content($fa-var-grin-tears); } +.#{$fa-css-prefix}-grin-tongue:before { content: fa-content($fa-var-grin-tongue); } +.#{$fa-css-prefix}-grin-tongue-squint:before { content: fa-content($fa-var-grin-tongue-squint); } +.#{$fa-css-prefix}-grin-tongue-wink:before { content: fa-content($fa-var-grin-tongue-wink); } +.#{$fa-css-prefix}-grin-wink:before { content: fa-content($fa-var-grin-wink); } +.#{$fa-css-prefix}-grip-horizontal:before { content: fa-content($fa-var-grip-horizontal); } +.#{$fa-css-prefix}-grip-lines:before { content: fa-content($fa-var-grip-lines); } +.#{$fa-css-prefix}-grip-lines-vertical:before { content: fa-content($fa-var-grip-lines-vertical); } +.#{$fa-css-prefix}-grip-vertical:before { content: fa-content($fa-var-grip-vertical); } +.#{$fa-css-prefix}-gripfire:before { content: fa-content($fa-var-gripfire); } +.#{$fa-css-prefix}-grunt:before { content: fa-content($fa-var-grunt); } +.#{$fa-css-prefix}-guitar:before { content: fa-content($fa-var-guitar); } +.#{$fa-css-prefix}-gulp:before { content: fa-content($fa-var-gulp); } +.#{$fa-css-prefix}-h-square:before { content: fa-content($fa-var-h-square); } +.#{$fa-css-prefix}-hacker-news:before { content: fa-content($fa-var-hacker-news); } +.#{$fa-css-prefix}-hacker-news-square:before { content: fa-content($fa-var-hacker-news-square); } +.#{$fa-css-prefix}-hackerrank:before { content: fa-content($fa-var-hackerrank); } +.#{$fa-css-prefix}-hamburger:before { content: fa-content($fa-var-hamburger); } +.#{$fa-css-prefix}-hammer:before { content: fa-content($fa-var-hammer); } +.#{$fa-css-prefix}-hamsa:before { content: fa-content($fa-var-hamsa); } +.#{$fa-css-prefix}-hand-holding:before { content: fa-content($fa-var-hand-holding); } +.#{$fa-css-prefix}-hand-holding-heart:before { content: fa-content($fa-var-hand-holding-heart); } +.#{$fa-css-prefix}-hand-holding-medical:before { content: fa-content($fa-var-hand-holding-medical); } +.#{$fa-css-prefix}-hand-holding-usd:before { content: fa-content($fa-var-hand-holding-usd); } +.#{$fa-css-prefix}-hand-holding-water:before { content: fa-content($fa-var-hand-holding-water); } +.#{$fa-css-prefix}-hand-lizard:before { content: fa-content($fa-var-hand-lizard); } +.#{$fa-css-prefix}-hand-middle-finger:before { content: fa-content($fa-var-hand-middle-finger); } +.#{$fa-css-prefix}-hand-paper:before { content: fa-content($fa-var-hand-paper); } +.#{$fa-css-prefix}-hand-peace:before { content: fa-content($fa-var-hand-peace); } +.#{$fa-css-prefix}-hand-point-down:before { content: fa-content($fa-var-hand-point-down); } +.#{$fa-css-prefix}-hand-point-left:before { content: fa-content($fa-var-hand-point-left); } +.#{$fa-css-prefix}-hand-point-right:before { content: fa-content($fa-var-hand-point-right); } +.#{$fa-css-prefix}-hand-point-up:before { content: fa-content($fa-var-hand-point-up); } +.#{$fa-css-prefix}-hand-pointer:before { content: fa-content($fa-var-hand-pointer); } +.#{$fa-css-prefix}-hand-rock:before { content: fa-content($fa-var-hand-rock); } +.#{$fa-css-prefix}-hand-scissors:before { content: fa-content($fa-var-hand-scissors); } +.#{$fa-css-prefix}-hand-sparkles:before { content: fa-content($fa-var-hand-sparkles); } +.#{$fa-css-prefix}-hand-spock:before { content: fa-content($fa-var-hand-spock); } +.#{$fa-css-prefix}-hands:before { content: fa-content($fa-var-hands); } +.#{$fa-css-prefix}-hands-helping:before { content: fa-content($fa-var-hands-helping); } +.#{$fa-css-prefix}-hands-wash:before { content: fa-content($fa-var-hands-wash); } +.#{$fa-css-prefix}-handshake:before { content: fa-content($fa-var-handshake); } +.#{$fa-css-prefix}-handshake-alt-slash:before { content: fa-content($fa-var-handshake-alt-slash); } +.#{$fa-css-prefix}-handshake-slash:before { content: fa-content($fa-var-handshake-slash); } +.#{$fa-css-prefix}-hanukiah:before { content: fa-content($fa-var-hanukiah); } +.#{$fa-css-prefix}-hard-hat:before { content: fa-content($fa-var-hard-hat); } +.#{$fa-css-prefix}-hashtag:before { content: fa-content($fa-var-hashtag); } +.#{$fa-css-prefix}-hat-cowboy:before { content: fa-content($fa-var-hat-cowboy); } +.#{$fa-css-prefix}-hat-cowboy-side:before { content: fa-content($fa-var-hat-cowboy-side); } +.#{$fa-css-prefix}-hat-wizard:before { content: fa-content($fa-var-hat-wizard); } +.#{$fa-css-prefix}-hdd:before { content: fa-content($fa-var-hdd); } +.#{$fa-css-prefix}-head-side-cough:before { content: fa-content($fa-var-head-side-cough); } +.#{$fa-css-prefix}-head-side-cough-slash:before { content: fa-content($fa-var-head-side-cough-slash); } +.#{$fa-css-prefix}-head-side-mask:before { content: fa-content($fa-var-head-side-mask); } +.#{$fa-css-prefix}-head-side-virus:before { content: fa-content($fa-var-head-side-virus); } +.#{$fa-css-prefix}-heading:before { content: fa-content($fa-var-heading); } +.#{$fa-css-prefix}-headphones:before { content: fa-content($fa-var-headphones); } +.#{$fa-css-prefix}-headphones-alt:before { content: fa-content($fa-var-headphones-alt); } +.#{$fa-css-prefix}-headset:before { content: fa-content($fa-var-headset); } +.#{$fa-css-prefix}-heart:before { content: fa-content($fa-var-heart); } +.#{$fa-css-prefix}-heart-broken:before { content: fa-content($fa-var-heart-broken); } +.#{$fa-css-prefix}-heartbeat:before { content: fa-content($fa-var-heartbeat); } +.#{$fa-css-prefix}-helicopter:before { content: fa-content($fa-var-helicopter); } +.#{$fa-css-prefix}-highlighter:before { content: fa-content($fa-var-highlighter); } +.#{$fa-css-prefix}-hiking:before { content: fa-content($fa-var-hiking); } +.#{$fa-css-prefix}-hippo:before { content: fa-content($fa-var-hippo); } +.#{$fa-css-prefix}-hips:before { content: fa-content($fa-var-hips); } +.#{$fa-css-prefix}-hire-a-helper:before { content: fa-content($fa-var-hire-a-helper); } +.#{$fa-css-prefix}-history:before { content: fa-content($fa-var-history); } +.#{$fa-css-prefix}-hockey-puck:before { content: fa-content($fa-var-hockey-puck); } +.#{$fa-css-prefix}-holly-berry:before { content: fa-content($fa-var-holly-berry); } +.#{$fa-css-prefix}-home:before { content: fa-content($fa-var-home); } +.#{$fa-css-prefix}-hooli:before { content: fa-content($fa-var-hooli); } +.#{$fa-css-prefix}-hornbill:before { content: fa-content($fa-var-hornbill); } +.#{$fa-css-prefix}-horse:before { content: fa-content($fa-var-horse); } +.#{$fa-css-prefix}-horse-head:before { content: fa-content($fa-var-horse-head); } +.#{$fa-css-prefix}-hospital:before { content: fa-content($fa-var-hospital); } +.#{$fa-css-prefix}-hospital-alt:before { content: fa-content($fa-var-hospital-alt); } +.#{$fa-css-prefix}-hospital-symbol:before { content: fa-content($fa-var-hospital-symbol); } +.#{$fa-css-prefix}-hospital-user:before { content: fa-content($fa-var-hospital-user); } +.#{$fa-css-prefix}-hot-tub:before { content: fa-content($fa-var-hot-tub); } +.#{$fa-css-prefix}-hotdog:before { content: fa-content($fa-var-hotdog); } +.#{$fa-css-prefix}-hotel:before { content: fa-content($fa-var-hotel); } +.#{$fa-css-prefix}-hotjar:before { content: fa-content($fa-var-hotjar); } +.#{$fa-css-prefix}-hourglass:before { content: fa-content($fa-var-hourglass); } +.#{$fa-css-prefix}-hourglass-end:before { content: fa-content($fa-var-hourglass-end); } +.#{$fa-css-prefix}-hourglass-half:before { content: fa-content($fa-var-hourglass-half); } +.#{$fa-css-prefix}-hourglass-start:before { content: fa-content($fa-var-hourglass-start); } +.#{$fa-css-prefix}-house-damage:before { content: fa-content($fa-var-house-damage); } +.#{$fa-css-prefix}-house-user:before { content: fa-content($fa-var-house-user); } +.#{$fa-css-prefix}-houzz:before { content: fa-content($fa-var-houzz); } +.#{$fa-css-prefix}-hryvnia:before { content: fa-content($fa-var-hryvnia); } +.#{$fa-css-prefix}-html5:before { content: fa-content($fa-var-html5); } +.#{$fa-css-prefix}-hubspot:before { content: fa-content($fa-var-hubspot); } +.#{$fa-css-prefix}-i-cursor:before { content: fa-content($fa-var-i-cursor); } +.#{$fa-css-prefix}-ice-cream:before { content: fa-content($fa-var-ice-cream); } +.#{$fa-css-prefix}-icicles:before { content: fa-content($fa-var-icicles); } +.#{$fa-css-prefix}-icons:before { content: fa-content($fa-var-icons); } +.#{$fa-css-prefix}-id-badge:before { content: fa-content($fa-var-id-badge); } +.#{$fa-css-prefix}-id-card:before { content: fa-content($fa-var-id-card); } +.#{$fa-css-prefix}-id-card-alt:before { content: fa-content($fa-var-id-card-alt); } +.#{$fa-css-prefix}-ideal:before { content: fa-content($fa-var-ideal); } +.#{$fa-css-prefix}-igloo:before { content: fa-content($fa-var-igloo); } +.#{$fa-css-prefix}-image:before { content: fa-content($fa-var-image); } +.#{$fa-css-prefix}-images:before { content: fa-content($fa-var-images); } +.#{$fa-css-prefix}-imdb:before { content: fa-content($fa-var-imdb); } +.#{$fa-css-prefix}-inbox:before { content: fa-content($fa-var-inbox); } +.#{$fa-css-prefix}-indent:before { content: fa-content($fa-var-indent); } +.#{$fa-css-prefix}-industry:before { content: fa-content($fa-var-industry); } +.#{$fa-css-prefix}-infinity:before { content: fa-content($fa-var-infinity); } +.#{$fa-css-prefix}-info:before { content: fa-content($fa-var-info); } +.#{$fa-css-prefix}-info-circle:before { content: fa-content($fa-var-info-circle); } +.#{$fa-css-prefix}-instagram:before { content: fa-content($fa-var-instagram); } +.#{$fa-css-prefix}-instagram-square:before { content: fa-content($fa-var-instagram-square); } +.#{$fa-css-prefix}-intercom:before { content: fa-content($fa-var-intercom); } +.#{$fa-css-prefix}-internet-explorer:before { content: fa-content($fa-var-internet-explorer); } +.#{$fa-css-prefix}-invision:before { content: fa-content($fa-var-invision); } +.#{$fa-css-prefix}-ioxhost:before { content: fa-content($fa-var-ioxhost); } +.#{$fa-css-prefix}-italic:before { content: fa-content($fa-var-italic); } +.#{$fa-css-prefix}-itch-io:before { content: fa-content($fa-var-itch-io); } +.#{$fa-css-prefix}-itunes:before { content: fa-content($fa-var-itunes); } +.#{$fa-css-prefix}-itunes-note:before { content: fa-content($fa-var-itunes-note); } +.#{$fa-css-prefix}-java:before { content: fa-content($fa-var-java); } +.#{$fa-css-prefix}-jedi:before { content: fa-content($fa-var-jedi); } +.#{$fa-css-prefix}-jedi-order:before { content: fa-content($fa-var-jedi-order); } +.#{$fa-css-prefix}-jenkins:before { content: fa-content($fa-var-jenkins); } +.#{$fa-css-prefix}-jira:before { content: fa-content($fa-var-jira); } +.#{$fa-css-prefix}-joget:before { content: fa-content($fa-var-joget); } +.#{$fa-css-prefix}-joint:before { content: fa-content($fa-var-joint); } +.#{$fa-css-prefix}-joomla:before { content: fa-content($fa-var-joomla); } +.#{$fa-css-prefix}-journal-whills:before { content: fa-content($fa-var-journal-whills); } +.#{$fa-css-prefix}-js:before { content: fa-content($fa-var-js); } +.#{$fa-css-prefix}-js-square:before { content: fa-content($fa-var-js-square); } +.#{$fa-css-prefix}-jsfiddle:before { content: fa-content($fa-var-jsfiddle); } +.#{$fa-css-prefix}-kaaba:before { content: fa-content($fa-var-kaaba); } +.#{$fa-css-prefix}-kaggle:before { content: fa-content($fa-var-kaggle); } +.#{$fa-css-prefix}-key:before { content: fa-content($fa-var-key); } +.#{$fa-css-prefix}-keybase:before { content: fa-content($fa-var-keybase); } +.#{$fa-css-prefix}-keyboard:before { content: fa-content($fa-var-keyboard); } +.#{$fa-css-prefix}-keycdn:before { content: fa-content($fa-var-keycdn); } +.#{$fa-css-prefix}-khanda:before { content: fa-content($fa-var-khanda); } +.#{$fa-css-prefix}-kickstarter:before { content: fa-content($fa-var-kickstarter); } +.#{$fa-css-prefix}-kickstarter-k:before { content: fa-content($fa-var-kickstarter-k); } +.#{$fa-css-prefix}-kiss:before { content: fa-content($fa-var-kiss); } +.#{$fa-css-prefix}-kiss-beam:before { content: fa-content($fa-var-kiss-beam); } +.#{$fa-css-prefix}-kiss-wink-heart:before { content: fa-content($fa-var-kiss-wink-heart); } +.#{$fa-css-prefix}-kiwi-bird:before { content: fa-content($fa-var-kiwi-bird); } +.#{$fa-css-prefix}-korvue:before { content: fa-content($fa-var-korvue); } +.#{$fa-css-prefix}-landmark:before { content: fa-content($fa-var-landmark); } +.#{$fa-css-prefix}-language:before { content: fa-content($fa-var-language); } +.#{$fa-css-prefix}-laptop:before { content: fa-content($fa-var-laptop); } +.#{$fa-css-prefix}-laptop-code:before { content: fa-content($fa-var-laptop-code); } +.#{$fa-css-prefix}-laptop-house:before { content: fa-content($fa-var-laptop-house); } +.#{$fa-css-prefix}-laptop-medical:before { content: fa-content($fa-var-laptop-medical); } +.#{$fa-css-prefix}-laravel:before { content: fa-content($fa-var-laravel); } +.#{$fa-css-prefix}-lastfm:before { content: fa-content($fa-var-lastfm); } +.#{$fa-css-prefix}-lastfm-square:before { content: fa-content($fa-var-lastfm-square); } +.#{$fa-css-prefix}-laugh:before { content: fa-content($fa-var-laugh); } +.#{$fa-css-prefix}-laugh-beam:before { content: fa-content($fa-var-laugh-beam); } +.#{$fa-css-prefix}-laugh-squint:before { content: fa-content($fa-var-laugh-squint); } +.#{$fa-css-prefix}-laugh-wink:before { content: fa-content($fa-var-laugh-wink); } +.#{$fa-css-prefix}-layer-group:before { content: fa-content($fa-var-layer-group); } +.#{$fa-css-prefix}-leaf:before { content: fa-content($fa-var-leaf); } +.#{$fa-css-prefix}-leanpub:before { content: fa-content($fa-var-leanpub); } +.#{$fa-css-prefix}-lemon:before { content: fa-content($fa-var-lemon); } +.#{$fa-css-prefix}-less:before { content: fa-content($fa-var-less); } +.#{$fa-css-prefix}-less-than:before { content: fa-content($fa-var-less-than); } +.#{$fa-css-prefix}-less-than-equal:before { content: fa-content($fa-var-less-than-equal); } +.#{$fa-css-prefix}-level-down-alt:before { content: fa-content($fa-var-level-down-alt); } +.#{$fa-css-prefix}-level-up-alt:before { content: fa-content($fa-var-level-up-alt); } +.#{$fa-css-prefix}-life-ring:before { content: fa-content($fa-var-life-ring); } +.#{$fa-css-prefix}-lightbulb:before { content: fa-content($fa-var-lightbulb); } +.#{$fa-css-prefix}-line:before { content: fa-content($fa-var-line); } +.#{$fa-css-prefix}-link:before { content: fa-content($fa-var-link); } +.#{$fa-css-prefix}-linkedin:before { content: fa-content($fa-var-linkedin); } +.#{$fa-css-prefix}-linkedin-in:before { content: fa-content($fa-var-linkedin-in); } +.#{$fa-css-prefix}-linode:before { content: fa-content($fa-var-linode); } +.#{$fa-css-prefix}-linux:before { content: fa-content($fa-var-linux); } +.#{$fa-css-prefix}-lira-sign:before { content: fa-content($fa-var-lira-sign); } +.#{$fa-css-prefix}-list:before { content: fa-content($fa-var-list); } +.#{$fa-css-prefix}-list-alt:before { content: fa-content($fa-var-list-alt); } +.#{$fa-css-prefix}-list-ol:before { content: fa-content($fa-var-list-ol); } +.#{$fa-css-prefix}-list-ul:before { content: fa-content($fa-var-list-ul); } +.#{$fa-css-prefix}-location-arrow:before { content: fa-content($fa-var-location-arrow); } +.#{$fa-css-prefix}-lock:before { content: fa-content($fa-var-lock); } +.#{$fa-css-prefix}-lock-open:before { content: fa-content($fa-var-lock-open); } +.#{$fa-css-prefix}-long-arrow-alt-down:before { content: fa-content($fa-var-long-arrow-alt-down); } +.#{$fa-css-prefix}-long-arrow-alt-left:before { content: fa-content($fa-var-long-arrow-alt-left); } +.#{$fa-css-prefix}-long-arrow-alt-right:before { content: fa-content($fa-var-long-arrow-alt-right); } +.#{$fa-css-prefix}-long-arrow-alt-up:before { content: fa-content($fa-var-long-arrow-alt-up); } +.#{$fa-css-prefix}-low-vision:before { content: fa-content($fa-var-low-vision); } +.#{$fa-css-prefix}-luggage-cart:before { content: fa-content($fa-var-luggage-cart); } +.#{$fa-css-prefix}-lungs:before { content: fa-content($fa-var-lungs); } +.#{$fa-css-prefix}-lungs-virus:before { content: fa-content($fa-var-lungs-virus); } +.#{$fa-css-prefix}-lyft:before { content: fa-content($fa-var-lyft); } +.#{$fa-css-prefix}-magento:before { content: fa-content($fa-var-magento); } +.#{$fa-css-prefix}-magic:before { content: fa-content($fa-var-magic); } +.#{$fa-css-prefix}-magnet:before { content: fa-content($fa-var-magnet); } +.#{$fa-css-prefix}-mail-bulk:before { content: fa-content($fa-var-mail-bulk); } +.#{$fa-css-prefix}-mailchimp:before { content: fa-content($fa-var-mailchimp); } +.#{$fa-css-prefix}-male:before { content: fa-content($fa-var-male); } +.#{$fa-css-prefix}-mandalorian:before { content: fa-content($fa-var-mandalorian); } +.#{$fa-css-prefix}-map:before { content: fa-content($fa-var-map); } +.#{$fa-css-prefix}-map-marked:before { content: fa-content($fa-var-map-marked); } +.#{$fa-css-prefix}-map-marked-alt:before { content: fa-content($fa-var-map-marked-alt); } +.#{$fa-css-prefix}-map-marker:before { content: fa-content($fa-var-map-marker); } +.#{$fa-css-prefix}-map-marker-alt:before { content: fa-content($fa-var-map-marker-alt); } +.#{$fa-css-prefix}-map-pin:before { content: fa-content($fa-var-map-pin); } +.#{$fa-css-prefix}-map-signs:before { content: fa-content($fa-var-map-signs); } +.#{$fa-css-prefix}-markdown:before { content: fa-content($fa-var-markdown); } +.#{$fa-css-prefix}-marker:before { content: fa-content($fa-var-marker); } +.#{$fa-css-prefix}-mars:before { content: fa-content($fa-var-mars); } +.#{$fa-css-prefix}-mars-double:before { content: fa-content($fa-var-mars-double); } +.#{$fa-css-prefix}-mars-stroke:before { content: fa-content($fa-var-mars-stroke); } +.#{$fa-css-prefix}-mars-stroke-h:before { content: fa-content($fa-var-mars-stroke-h); } +.#{$fa-css-prefix}-mars-stroke-v:before { content: fa-content($fa-var-mars-stroke-v); } +.#{$fa-css-prefix}-mask:before { content: fa-content($fa-var-mask); } +.#{$fa-css-prefix}-mastodon:before { content: fa-content($fa-var-mastodon); } +.#{$fa-css-prefix}-maxcdn:before { content: fa-content($fa-var-maxcdn); } +.#{$fa-css-prefix}-mdb:before { content: fa-content($fa-var-mdb); } +.#{$fa-css-prefix}-medal:before { content: fa-content($fa-var-medal); } +.#{$fa-css-prefix}-medapps:before { content: fa-content($fa-var-medapps); } +.#{$fa-css-prefix}-medium:before { content: fa-content($fa-var-medium); } +.#{$fa-css-prefix}-medium-m:before { content: fa-content($fa-var-medium-m); } +.#{$fa-css-prefix}-medkit:before { content: fa-content($fa-var-medkit); } +.#{$fa-css-prefix}-medrt:before { content: fa-content($fa-var-medrt); } +.#{$fa-css-prefix}-meetup:before { content: fa-content($fa-var-meetup); } +.#{$fa-css-prefix}-megaport:before { content: fa-content($fa-var-megaport); } +.#{$fa-css-prefix}-meh:before { content: fa-content($fa-var-meh); } +.#{$fa-css-prefix}-meh-blank:before { content: fa-content($fa-var-meh-blank); } +.#{$fa-css-prefix}-meh-rolling-eyes:before { content: fa-content($fa-var-meh-rolling-eyes); } +.#{$fa-css-prefix}-memory:before { content: fa-content($fa-var-memory); } +.#{$fa-css-prefix}-mendeley:before { content: fa-content($fa-var-mendeley); } +.#{$fa-css-prefix}-menorah:before { content: fa-content($fa-var-menorah); } +.#{$fa-css-prefix}-mercury:before { content: fa-content($fa-var-mercury); } +.#{$fa-css-prefix}-meteor:before { content: fa-content($fa-var-meteor); } +.#{$fa-css-prefix}-microblog:before { content: fa-content($fa-var-microblog); } +.#{$fa-css-prefix}-microchip:before { content: fa-content($fa-var-microchip); } +.#{$fa-css-prefix}-microphone:before { content: fa-content($fa-var-microphone); } +.#{$fa-css-prefix}-microphone-alt:before { content: fa-content($fa-var-microphone-alt); } +.#{$fa-css-prefix}-microphone-alt-slash:before { content: fa-content($fa-var-microphone-alt-slash); } +.#{$fa-css-prefix}-microphone-slash:before { content: fa-content($fa-var-microphone-slash); } +.#{$fa-css-prefix}-microscope:before { content: fa-content($fa-var-microscope); } +.#{$fa-css-prefix}-microsoft:before { content: fa-content($fa-var-microsoft); } +.#{$fa-css-prefix}-minus:before { content: fa-content($fa-var-minus); } +.#{$fa-css-prefix}-minus-circle:before { content: fa-content($fa-var-minus-circle); } +.#{$fa-css-prefix}-minus-square:before { content: fa-content($fa-var-minus-square); } +.#{$fa-css-prefix}-mitten:before { content: fa-content($fa-var-mitten); } +.#{$fa-css-prefix}-mix:before { content: fa-content($fa-var-mix); } +.#{$fa-css-prefix}-mixcloud:before { content: fa-content($fa-var-mixcloud); } +.#{$fa-css-prefix}-mixer:before { content: fa-content($fa-var-mixer); } +.#{$fa-css-prefix}-mizuni:before { content: fa-content($fa-var-mizuni); } +.#{$fa-css-prefix}-mobile:before { content: fa-content($fa-var-mobile); } +.#{$fa-css-prefix}-mobile-alt:before { content: fa-content($fa-var-mobile-alt); } +.#{$fa-css-prefix}-modx:before { content: fa-content($fa-var-modx); } +.#{$fa-css-prefix}-monero:before { content: fa-content($fa-var-monero); } +.#{$fa-css-prefix}-money-bill:before { content: fa-content($fa-var-money-bill); } +.#{$fa-css-prefix}-money-bill-alt:before { content: fa-content($fa-var-money-bill-alt); } +.#{$fa-css-prefix}-money-bill-wave:before { content: fa-content($fa-var-money-bill-wave); } +.#{$fa-css-prefix}-money-bill-wave-alt:before { content: fa-content($fa-var-money-bill-wave-alt); } +.#{$fa-css-prefix}-money-check:before { content: fa-content($fa-var-money-check); } +.#{$fa-css-prefix}-money-check-alt:before { content: fa-content($fa-var-money-check-alt); } +.#{$fa-css-prefix}-monument:before { content: fa-content($fa-var-monument); } +.#{$fa-css-prefix}-moon:before { content: fa-content($fa-var-moon); } +.#{$fa-css-prefix}-mortar-pestle:before { content: fa-content($fa-var-mortar-pestle); } +.#{$fa-css-prefix}-mosque:before { content: fa-content($fa-var-mosque); } +.#{$fa-css-prefix}-motorcycle:before { content: fa-content($fa-var-motorcycle); } +.#{$fa-css-prefix}-mountain:before { content: fa-content($fa-var-mountain); } +.#{$fa-css-prefix}-mouse:before { content: fa-content($fa-var-mouse); } +.#{$fa-css-prefix}-mouse-pointer:before { content: fa-content($fa-var-mouse-pointer); } +.#{$fa-css-prefix}-mug-hot:before { content: fa-content($fa-var-mug-hot); } +.#{$fa-css-prefix}-music:before { content: fa-content($fa-var-music); } +.#{$fa-css-prefix}-napster:before { content: fa-content($fa-var-napster); } +.#{$fa-css-prefix}-neos:before { content: fa-content($fa-var-neos); } +.#{$fa-css-prefix}-network-wired:before { content: fa-content($fa-var-network-wired); } +.#{$fa-css-prefix}-neuter:before { content: fa-content($fa-var-neuter); } +.#{$fa-css-prefix}-newspaper:before { content: fa-content($fa-var-newspaper); } +.#{$fa-css-prefix}-nimblr:before { content: fa-content($fa-var-nimblr); } +.#{$fa-css-prefix}-node:before { content: fa-content($fa-var-node); } +.#{$fa-css-prefix}-node-js:before { content: fa-content($fa-var-node-js); } +.#{$fa-css-prefix}-not-equal:before { content: fa-content($fa-var-not-equal); } +.#{$fa-css-prefix}-notes-medical:before { content: fa-content($fa-var-notes-medical); } +.#{$fa-css-prefix}-npm:before { content: fa-content($fa-var-npm); } +.#{$fa-css-prefix}-ns8:before { content: fa-content($fa-var-ns8); } +.#{$fa-css-prefix}-nutritionix:before { content: fa-content($fa-var-nutritionix); } +.#{$fa-css-prefix}-object-group:before { content: fa-content($fa-var-object-group); } +.#{$fa-css-prefix}-object-ungroup:before { content: fa-content($fa-var-object-ungroup); } +.#{$fa-css-prefix}-odnoklassniki:before { content: fa-content($fa-var-odnoklassniki); } +.#{$fa-css-prefix}-odnoklassniki-square:before { content: fa-content($fa-var-odnoklassniki-square); } +.#{$fa-css-prefix}-oil-can:before { content: fa-content($fa-var-oil-can); } +.#{$fa-css-prefix}-old-republic:before { content: fa-content($fa-var-old-republic); } +.#{$fa-css-prefix}-om:before { content: fa-content($fa-var-om); } +.#{$fa-css-prefix}-opencart:before { content: fa-content($fa-var-opencart); } +.#{$fa-css-prefix}-openid:before { content: fa-content($fa-var-openid); } +.#{$fa-css-prefix}-opera:before { content: fa-content($fa-var-opera); } +.#{$fa-css-prefix}-optin-monster:before { content: fa-content($fa-var-optin-monster); } +.#{$fa-css-prefix}-orcid:before { content: fa-content($fa-var-orcid); } +.#{$fa-css-prefix}-osi:before { content: fa-content($fa-var-osi); } +.#{$fa-css-prefix}-otter:before { content: fa-content($fa-var-otter); } +.#{$fa-css-prefix}-outdent:before { content: fa-content($fa-var-outdent); } +.#{$fa-css-prefix}-page4:before { content: fa-content($fa-var-page4); } +.#{$fa-css-prefix}-pagelines:before { content: fa-content($fa-var-pagelines); } +.#{$fa-css-prefix}-pager:before { content: fa-content($fa-var-pager); } +.#{$fa-css-prefix}-paint-brush:before { content: fa-content($fa-var-paint-brush); } +.#{$fa-css-prefix}-paint-roller:before { content: fa-content($fa-var-paint-roller); } +.#{$fa-css-prefix}-palette:before { content: fa-content($fa-var-palette); } +.#{$fa-css-prefix}-palfed:before { content: fa-content($fa-var-palfed); } +.#{$fa-css-prefix}-pallet:before { content: fa-content($fa-var-pallet); } +.#{$fa-css-prefix}-paper-plane:before { content: fa-content($fa-var-paper-plane); } +.#{$fa-css-prefix}-paperclip:before { content: fa-content($fa-var-paperclip); } +.#{$fa-css-prefix}-parachute-box:before { content: fa-content($fa-var-parachute-box); } +.#{$fa-css-prefix}-paragraph:before { content: fa-content($fa-var-paragraph); } +.#{$fa-css-prefix}-parking:before { content: fa-content($fa-var-parking); } +.#{$fa-css-prefix}-passport:before { content: fa-content($fa-var-passport); } +.#{$fa-css-prefix}-pastafarianism:before { content: fa-content($fa-var-pastafarianism); } +.#{$fa-css-prefix}-paste:before { content: fa-content($fa-var-paste); } +.#{$fa-css-prefix}-patreon:before { content: fa-content($fa-var-patreon); } +.#{$fa-css-prefix}-pause:before { content: fa-content($fa-var-pause); } +.#{$fa-css-prefix}-pause-circle:before { content: fa-content($fa-var-pause-circle); } +.#{$fa-css-prefix}-paw:before { content: fa-content($fa-var-paw); } +.#{$fa-css-prefix}-paypal:before { content: fa-content($fa-var-paypal); } +.#{$fa-css-prefix}-peace:before { content: fa-content($fa-var-peace); } +.#{$fa-css-prefix}-pen:before { content: fa-content($fa-var-pen); } +.#{$fa-css-prefix}-pen-alt:before { content: fa-content($fa-var-pen-alt); } +.#{$fa-css-prefix}-pen-fancy:before { content: fa-content($fa-var-pen-fancy); } +.#{$fa-css-prefix}-pen-nib:before { content: fa-content($fa-var-pen-nib); } +.#{$fa-css-prefix}-pen-square:before { content: fa-content($fa-var-pen-square); } +.#{$fa-css-prefix}-pencil-alt:before { content: fa-content($fa-var-pencil-alt); } +.#{$fa-css-prefix}-pencil-ruler:before { content: fa-content($fa-var-pencil-ruler); } +.#{$fa-css-prefix}-penny-arcade:before { content: fa-content($fa-var-penny-arcade); } +.#{$fa-css-prefix}-people-arrows:before { content: fa-content($fa-var-people-arrows); } +.#{$fa-css-prefix}-people-carry:before { content: fa-content($fa-var-people-carry); } +.#{$fa-css-prefix}-pepper-hot:before { content: fa-content($fa-var-pepper-hot); } +.#{$fa-css-prefix}-percent:before { content: fa-content($fa-var-percent); } +.#{$fa-css-prefix}-percentage:before { content: fa-content($fa-var-percentage); } +.#{$fa-css-prefix}-periscope:before { content: fa-content($fa-var-periscope); } +.#{$fa-css-prefix}-person-booth:before { content: fa-content($fa-var-person-booth); } +.#{$fa-css-prefix}-phabricator:before { content: fa-content($fa-var-phabricator); } +.#{$fa-css-prefix}-phoenix-framework:before { content: fa-content($fa-var-phoenix-framework); } +.#{$fa-css-prefix}-phoenix-squadron:before { content: fa-content($fa-var-phoenix-squadron); } +.#{$fa-css-prefix}-phone:before { content: fa-content($fa-var-phone); } +.#{$fa-css-prefix}-phone-alt:before { content: fa-content($fa-var-phone-alt); } +.#{$fa-css-prefix}-phone-slash:before { content: fa-content($fa-var-phone-slash); } +.#{$fa-css-prefix}-phone-square:before { content: fa-content($fa-var-phone-square); } +.#{$fa-css-prefix}-phone-square-alt:before { content: fa-content($fa-var-phone-square-alt); } +.#{$fa-css-prefix}-phone-volume:before { content: fa-content($fa-var-phone-volume); } +.#{$fa-css-prefix}-photo-video:before { content: fa-content($fa-var-photo-video); } +.#{$fa-css-prefix}-php:before { content: fa-content($fa-var-php); } +.#{$fa-css-prefix}-pied-piper:before { content: fa-content($fa-var-pied-piper); } +.#{$fa-css-prefix}-pied-piper-alt:before { content: fa-content($fa-var-pied-piper-alt); } +.#{$fa-css-prefix}-pied-piper-hat:before { content: fa-content($fa-var-pied-piper-hat); } +.#{$fa-css-prefix}-pied-piper-pp:before { content: fa-content($fa-var-pied-piper-pp); } +.#{$fa-css-prefix}-pied-piper-square:before { content: fa-content($fa-var-pied-piper-square); } +.#{$fa-css-prefix}-piggy-bank:before { content: fa-content($fa-var-piggy-bank); } +.#{$fa-css-prefix}-pills:before { content: fa-content($fa-var-pills); } +.#{$fa-css-prefix}-pinterest:before { content: fa-content($fa-var-pinterest); } +.#{$fa-css-prefix}-pinterest-p:before { content: fa-content($fa-var-pinterest-p); } +.#{$fa-css-prefix}-pinterest-square:before { content: fa-content($fa-var-pinterest-square); } +.#{$fa-css-prefix}-pizza-slice:before { content: fa-content($fa-var-pizza-slice); } +.#{$fa-css-prefix}-place-of-worship:before { content: fa-content($fa-var-place-of-worship); } +.#{$fa-css-prefix}-plane:before { content: fa-content($fa-var-plane); } +.#{$fa-css-prefix}-plane-arrival:before { content: fa-content($fa-var-plane-arrival); } +.#{$fa-css-prefix}-plane-departure:before { content: fa-content($fa-var-plane-departure); } +.#{$fa-css-prefix}-plane-slash:before { content: fa-content($fa-var-plane-slash); } +.#{$fa-css-prefix}-play:before { content: fa-content($fa-var-play); } +.#{$fa-css-prefix}-play-circle:before { content: fa-content($fa-var-play-circle); } +.#{$fa-css-prefix}-playstation:before { content: fa-content($fa-var-playstation); } +.#{$fa-css-prefix}-plug:before { content: fa-content($fa-var-plug); } +.#{$fa-css-prefix}-plus:before { content: fa-content($fa-var-plus); } +.#{$fa-css-prefix}-plus-circle:before { content: fa-content($fa-var-plus-circle); } +.#{$fa-css-prefix}-plus-square:before { content: fa-content($fa-var-plus-square); } +.#{$fa-css-prefix}-podcast:before { content: fa-content($fa-var-podcast); } +.#{$fa-css-prefix}-poll:before { content: fa-content($fa-var-poll); } +.#{$fa-css-prefix}-poll-h:before { content: fa-content($fa-var-poll-h); } +.#{$fa-css-prefix}-poo:before { content: fa-content($fa-var-poo); } +.#{$fa-css-prefix}-poo-storm:before { content: fa-content($fa-var-poo-storm); } +.#{$fa-css-prefix}-poop:before { content: fa-content($fa-var-poop); } +.#{$fa-css-prefix}-portrait:before { content: fa-content($fa-var-portrait); } +.#{$fa-css-prefix}-pound-sign:before { content: fa-content($fa-var-pound-sign); } +.#{$fa-css-prefix}-power-off:before { content: fa-content($fa-var-power-off); } +.#{$fa-css-prefix}-pray:before { content: fa-content($fa-var-pray); } +.#{$fa-css-prefix}-praying-hands:before { content: fa-content($fa-var-praying-hands); } +.#{$fa-css-prefix}-prescription:before { content: fa-content($fa-var-prescription); } +.#{$fa-css-prefix}-prescription-bottle:before { content: fa-content($fa-var-prescription-bottle); } +.#{$fa-css-prefix}-prescription-bottle-alt:before { content: fa-content($fa-var-prescription-bottle-alt); } +.#{$fa-css-prefix}-print:before { content: fa-content($fa-var-print); } +.#{$fa-css-prefix}-procedures:before { content: fa-content($fa-var-procedures); } +.#{$fa-css-prefix}-product-hunt:before { content: fa-content($fa-var-product-hunt); } +.#{$fa-css-prefix}-project-diagram:before { content: fa-content($fa-var-project-diagram); } +.#{$fa-css-prefix}-pump-medical:before { content: fa-content($fa-var-pump-medical); } +.#{$fa-css-prefix}-pump-soap:before { content: fa-content($fa-var-pump-soap); } +.#{$fa-css-prefix}-pushed:before { content: fa-content($fa-var-pushed); } +.#{$fa-css-prefix}-puzzle-piece:before { content: fa-content($fa-var-puzzle-piece); } +.#{$fa-css-prefix}-python:before { content: fa-content($fa-var-python); } +.#{$fa-css-prefix}-qq:before { content: fa-content($fa-var-qq); } +.#{$fa-css-prefix}-qrcode:before { content: fa-content($fa-var-qrcode); } +.#{$fa-css-prefix}-question:before { content: fa-content($fa-var-question); } +.#{$fa-css-prefix}-question-circle:before { content: fa-content($fa-var-question-circle); } +.#{$fa-css-prefix}-quidditch:before { content: fa-content($fa-var-quidditch); } +.#{$fa-css-prefix}-quinscape:before { content: fa-content($fa-var-quinscape); } +.#{$fa-css-prefix}-quora:before { content: fa-content($fa-var-quora); } +.#{$fa-css-prefix}-quote-left:before { content: fa-content($fa-var-quote-left); } +.#{$fa-css-prefix}-quote-right:before { content: fa-content($fa-var-quote-right); } +.#{$fa-css-prefix}-quran:before { content: fa-content($fa-var-quran); } +.#{$fa-css-prefix}-r-project:before { content: fa-content($fa-var-r-project); } +.#{$fa-css-prefix}-radiation:before { content: fa-content($fa-var-radiation); } +.#{$fa-css-prefix}-radiation-alt:before { content: fa-content($fa-var-radiation-alt); } +.#{$fa-css-prefix}-rainbow:before { content: fa-content($fa-var-rainbow); } +.#{$fa-css-prefix}-random:before { content: fa-content($fa-var-random); } +.#{$fa-css-prefix}-raspberry-pi:before { content: fa-content($fa-var-raspberry-pi); } +.#{$fa-css-prefix}-ravelry:before { content: fa-content($fa-var-ravelry); } +.#{$fa-css-prefix}-react:before { content: fa-content($fa-var-react); } +.#{$fa-css-prefix}-reacteurope:before { content: fa-content($fa-var-reacteurope); } +.#{$fa-css-prefix}-readme:before { content: fa-content($fa-var-readme); } +.#{$fa-css-prefix}-rebel:before { content: fa-content($fa-var-rebel); } +.#{$fa-css-prefix}-receipt:before { content: fa-content($fa-var-receipt); } +.#{$fa-css-prefix}-record-vinyl:before { content: fa-content($fa-var-record-vinyl); } +.#{$fa-css-prefix}-recycle:before { content: fa-content($fa-var-recycle); } +.#{$fa-css-prefix}-red-river:before { content: fa-content($fa-var-red-river); } +.#{$fa-css-prefix}-reddit:before { content: fa-content($fa-var-reddit); } +.#{$fa-css-prefix}-reddit-alien:before { content: fa-content($fa-var-reddit-alien); } +.#{$fa-css-prefix}-reddit-square:before { content: fa-content($fa-var-reddit-square); } +.#{$fa-css-prefix}-redhat:before { content: fa-content($fa-var-redhat); } +.#{$fa-css-prefix}-redo:before { content: fa-content($fa-var-redo); } +.#{$fa-css-prefix}-redo-alt:before { content: fa-content($fa-var-redo-alt); } +.#{$fa-css-prefix}-registered:before { content: fa-content($fa-var-registered); } +.#{$fa-css-prefix}-remove-format:before { content: fa-content($fa-var-remove-format); } +.#{$fa-css-prefix}-renren:before { content: fa-content($fa-var-renren); } +.#{$fa-css-prefix}-reply:before { content: fa-content($fa-var-reply); } +.#{$fa-css-prefix}-reply-all:before { content: fa-content($fa-var-reply-all); } +.#{$fa-css-prefix}-replyd:before { content: fa-content($fa-var-replyd); } +.#{$fa-css-prefix}-republican:before { content: fa-content($fa-var-republican); } +.#{$fa-css-prefix}-researchgate:before { content: fa-content($fa-var-researchgate); } +.#{$fa-css-prefix}-resolving:before { content: fa-content($fa-var-resolving); } +.#{$fa-css-prefix}-restroom:before { content: fa-content($fa-var-restroom); } +.#{$fa-css-prefix}-retweet:before { content: fa-content($fa-var-retweet); } +.#{$fa-css-prefix}-rev:before { content: fa-content($fa-var-rev); } +.#{$fa-css-prefix}-ribbon:before { content: fa-content($fa-var-ribbon); } +.#{$fa-css-prefix}-ring:before { content: fa-content($fa-var-ring); } +.#{$fa-css-prefix}-road:before { content: fa-content($fa-var-road); } +.#{$fa-css-prefix}-robot:before { content: fa-content($fa-var-robot); } +.#{$fa-css-prefix}-rocket:before { content: fa-content($fa-var-rocket); } +.#{$fa-css-prefix}-rocketchat:before { content: fa-content($fa-var-rocketchat); } +.#{$fa-css-prefix}-rockrms:before { content: fa-content($fa-var-rockrms); } +.#{$fa-css-prefix}-route:before { content: fa-content($fa-var-route); } +.#{$fa-css-prefix}-rss:before { content: fa-content($fa-var-rss); } +.#{$fa-css-prefix}-rss-square:before { content: fa-content($fa-var-rss-square); } +.#{$fa-css-prefix}-ruble-sign:before { content: fa-content($fa-var-ruble-sign); } +.#{$fa-css-prefix}-ruler:before { content: fa-content($fa-var-ruler); } +.#{$fa-css-prefix}-ruler-combined:before { content: fa-content($fa-var-ruler-combined); } +.#{$fa-css-prefix}-ruler-horizontal:before { content: fa-content($fa-var-ruler-horizontal); } +.#{$fa-css-prefix}-ruler-vertical:before { content: fa-content($fa-var-ruler-vertical); } +.#{$fa-css-prefix}-running:before { content: fa-content($fa-var-running); } +.#{$fa-css-prefix}-rupee-sign:before { content: fa-content($fa-var-rupee-sign); } +.#{$fa-css-prefix}-rust:before { content: fa-content($fa-var-rust); } +.#{$fa-css-prefix}-sad-cry:before { content: fa-content($fa-var-sad-cry); } +.#{$fa-css-prefix}-sad-tear:before { content: fa-content($fa-var-sad-tear); } +.#{$fa-css-prefix}-safari:before { content: fa-content($fa-var-safari); } +.#{$fa-css-prefix}-salesforce:before { content: fa-content($fa-var-salesforce); } +.#{$fa-css-prefix}-sass:before { content: fa-content($fa-var-sass); } +.#{$fa-css-prefix}-satellite:before { content: fa-content($fa-var-satellite); } +.#{$fa-css-prefix}-satellite-dish:before { content: fa-content($fa-var-satellite-dish); } +.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-save); } +.#{$fa-css-prefix}-schlix:before { content: fa-content($fa-var-schlix); } +.#{$fa-css-prefix}-school:before { content: fa-content($fa-var-school); } +.#{$fa-css-prefix}-screwdriver:before { content: fa-content($fa-var-screwdriver); } +.#{$fa-css-prefix}-scribd:before { content: fa-content($fa-var-scribd); } +.#{$fa-css-prefix}-scroll:before { content: fa-content($fa-var-scroll); } +.#{$fa-css-prefix}-sd-card:before { content: fa-content($fa-var-sd-card); } +.#{$fa-css-prefix}-search:before { content: fa-content($fa-var-search); } +.#{$fa-css-prefix}-search-dollar:before { content: fa-content($fa-var-search-dollar); } +.#{$fa-css-prefix}-search-location:before { content: fa-content($fa-var-search-location); } +.#{$fa-css-prefix}-search-minus:before { content: fa-content($fa-var-search-minus); } +.#{$fa-css-prefix}-search-plus:before { content: fa-content($fa-var-search-plus); } +.#{$fa-css-prefix}-searchengin:before { content: fa-content($fa-var-searchengin); } +.#{$fa-css-prefix}-seedling:before { content: fa-content($fa-var-seedling); } +.#{$fa-css-prefix}-sellcast:before { content: fa-content($fa-var-sellcast); } +.#{$fa-css-prefix}-sellsy:before { content: fa-content($fa-var-sellsy); } +.#{$fa-css-prefix}-server:before { content: fa-content($fa-var-server); } +.#{$fa-css-prefix}-servicestack:before { content: fa-content($fa-var-servicestack); } +.#{$fa-css-prefix}-shapes:before { content: fa-content($fa-var-shapes); } +.#{$fa-css-prefix}-share:before { content: fa-content($fa-var-share); } +.#{$fa-css-prefix}-share-alt:before { content: fa-content($fa-var-share-alt); } +.#{$fa-css-prefix}-share-alt-square:before { content: fa-content($fa-var-share-alt-square); } +.#{$fa-css-prefix}-share-square:before { content: fa-content($fa-var-share-square); } +.#{$fa-css-prefix}-shekel-sign:before { content: fa-content($fa-var-shekel-sign); } +.#{$fa-css-prefix}-shield-alt:before { content: fa-content($fa-var-shield-alt); } +.#{$fa-css-prefix}-shield-virus:before { content: fa-content($fa-var-shield-virus); } +.#{$fa-css-prefix}-ship:before { content: fa-content($fa-var-ship); } +.#{$fa-css-prefix}-shipping-fast:before { content: fa-content($fa-var-shipping-fast); } +.#{$fa-css-prefix}-shirtsinbulk:before { content: fa-content($fa-var-shirtsinbulk); } +.#{$fa-css-prefix}-shoe-prints:before { content: fa-content($fa-var-shoe-prints); } +.#{$fa-css-prefix}-shopify:before { content: fa-content($fa-var-shopify); } +.#{$fa-css-prefix}-shopping-bag:before { content: fa-content($fa-var-shopping-bag); } +.#{$fa-css-prefix}-shopping-basket:before { content: fa-content($fa-var-shopping-basket); } +.#{$fa-css-prefix}-shopping-cart:before { content: fa-content($fa-var-shopping-cart); } +.#{$fa-css-prefix}-shopware:before { content: fa-content($fa-var-shopware); } +.#{$fa-css-prefix}-shower:before { content: fa-content($fa-var-shower); } +.#{$fa-css-prefix}-shuttle-van:before { content: fa-content($fa-var-shuttle-van); } +.#{$fa-css-prefix}-sign:before { content: fa-content($fa-var-sign); } +.#{$fa-css-prefix}-sign-in-alt:before { content: fa-content($fa-var-sign-in-alt); } +.#{$fa-css-prefix}-sign-language:before { content: fa-content($fa-var-sign-language); } +.#{$fa-css-prefix}-sign-out-alt:before { content: fa-content($fa-var-sign-out-alt); } +.#{$fa-css-prefix}-signal:before { content: fa-content($fa-var-signal); } +.#{$fa-css-prefix}-signature:before { content: fa-content($fa-var-signature); } +.#{$fa-css-prefix}-sim-card:before { content: fa-content($fa-var-sim-card); } +.#{$fa-css-prefix}-simplybuilt:before { content: fa-content($fa-var-simplybuilt); } +.#{$fa-css-prefix}-sink:before { content: fa-content($fa-var-sink); } +.#{$fa-css-prefix}-sistrix:before { content: fa-content($fa-var-sistrix); } +.#{$fa-css-prefix}-sitemap:before { content: fa-content($fa-var-sitemap); } +.#{$fa-css-prefix}-sith:before { content: fa-content($fa-var-sith); } +.#{$fa-css-prefix}-skating:before { content: fa-content($fa-var-skating); } +.#{$fa-css-prefix}-sketch:before { content: fa-content($fa-var-sketch); } +.#{$fa-css-prefix}-skiing:before { content: fa-content($fa-var-skiing); } +.#{$fa-css-prefix}-skiing-nordic:before { content: fa-content($fa-var-skiing-nordic); } +.#{$fa-css-prefix}-skull:before { content: fa-content($fa-var-skull); } +.#{$fa-css-prefix}-skull-crossbones:before { content: fa-content($fa-var-skull-crossbones); } +.#{$fa-css-prefix}-skyatlas:before { content: fa-content($fa-var-skyatlas); } +.#{$fa-css-prefix}-skype:before { content: fa-content($fa-var-skype); } +.#{$fa-css-prefix}-slack:before { content: fa-content($fa-var-slack); } +.#{$fa-css-prefix}-slack-hash:before { content: fa-content($fa-var-slack-hash); } +.#{$fa-css-prefix}-slash:before { content: fa-content($fa-var-slash); } +.#{$fa-css-prefix}-sleigh:before { content: fa-content($fa-var-sleigh); } +.#{$fa-css-prefix}-sliders-h:before { content: fa-content($fa-var-sliders-h); } +.#{$fa-css-prefix}-slideshare:before { content: fa-content($fa-var-slideshare); } +.#{$fa-css-prefix}-smile:before { content: fa-content($fa-var-smile); } +.#{$fa-css-prefix}-smile-beam:before { content: fa-content($fa-var-smile-beam); } +.#{$fa-css-prefix}-smile-wink:before { content: fa-content($fa-var-smile-wink); } +.#{$fa-css-prefix}-smog:before { content: fa-content($fa-var-smog); } +.#{$fa-css-prefix}-smoking:before { content: fa-content($fa-var-smoking); } +.#{$fa-css-prefix}-smoking-ban:before { content: fa-content($fa-var-smoking-ban); } +.#{$fa-css-prefix}-sms:before { content: fa-content($fa-var-sms); } +.#{$fa-css-prefix}-snapchat:before { content: fa-content($fa-var-snapchat); } +.#{$fa-css-prefix}-snapchat-ghost:before { content: fa-content($fa-var-snapchat-ghost); } +.#{$fa-css-prefix}-snapchat-square:before { content: fa-content($fa-var-snapchat-square); } +.#{$fa-css-prefix}-snowboarding:before { content: fa-content($fa-var-snowboarding); } +.#{$fa-css-prefix}-snowflake:before { content: fa-content($fa-var-snowflake); } +.#{$fa-css-prefix}-snowman:before { content: fa-content($fa-var-snowman); } +.#{$fa-css-prefix}-snowplow:before { content: fa-content($fa-var-snowplow); } +.#{$fa-css-prefix}-soap:before { content: fa-content($fa-var-soap); } +.#{$fa-css-prefix}-socks:before { content: fa-content($fa-var-socks); } +.#{$fa-css-prefix}-solar-panel:before { content: fa-content($fa-var-solar-panel); } +.#{$fa-css-prefix}-sort:before { content: fa-content($fa-var-sort); } +.#{$fa-css-prefix}-sort-alpha-down:before { content: fa-content($fa-var-sort-alpha-down); } +.#{$fa-css-prefix}-sort-alpha-down-alt:before { content: fa-content($fa-var-sort-alpha-down-alt); } +.#{$fa-css-prefix}-sort-alpha-up:before { content: fa-content($fa-var-sort-alpha-up); } +.#{$fa-css-prefix}-sort-alpha-up-alt:before { content: fa-content($fa-var-sort-alpha-up-alt); } +.#{$fa-css-prefix}-sort-amount-down:before { content: fa-content($fa-var-sort-amount-down); } +.#{$fa-css-prefix}-sort-amount-down-alt:before { content: fa-content($fa-var-sort-amount-down-alt); } +.#{$fa-css-prefix}-sort-amount-up:before { content: fa-content($fa-var-sort-amount-up); } +.#{$fa-css-prefix}-sort-amount-up-alt:before { content: fa-content($fa-var-sort-amount-up-alt); } +.#{$fa-css-prefix}-sort-down:before { content: fa-content($fa-var-sort-down); } +.#{$fa-css-prefix}-sort-numeric-down:before { content: fa-content($fa-var-sort-numeric-down); } +.#{$fa-css-prefix}-sort-numeric-down-alt:before { content: fa-content($fa-var-sort-numeric-down-alt); } +.#{$fa-css-prefix}-sort-numeric-up:before { content: fa-content($fa-var-sort-numeric-up); } +.#{$fa-css-prefix}-sort-numeric-up-alt:before { content: fa-content($fa-var-sort-numeric-up-alt); } +.#{$fa-css-prefix}-sort-up:before { content: fa-content($fa-var-sort-up); } +.#{$fa-css-prefix}-soundcloud:before { content: fa-content($fa-var-soundcloud); } +.#{$fa-css-prefix}-sourcetree:before { content: fa-content($fa-var-sourcetree); } +.#{$fa-css-prefix}-spa:before { content: fa-content($fa-var-spa); } +.#{$fa-css-prefix}-space-shuttle:before { content: fa-content($fa-var-space-shuttle); } +.#{$fa-css-prefix}-speakap:before { content: fa-content($fa-var-speakap); } +.#{$fa-css-prefix}-speaker-deck:before { content: fa-content($fa-var-speaker-deck); } +.#{$fa-css-prefix}-spell-check:before { content: fa-content($fa-var-spell-check); } +.#{$fa-css-prefix}-spider:before { content: fa-content($fa-var-spider); } +.#{$fa-css-prefix}-spinner:before { content: fa-content($fa-var-spinner); } +.#{$fa-css-prefix}-splotch:before { content: fa-content($fa-var-splotch); } +.#{$fa-css-prefix}-spotify:before { content: fa-content($fa-var-spotify); } +.#{$fa-css-prefix}-spray-can:before { content: fa-content($fa-var-spray-can); } +.#{$fa-css-prefix}-square:before { content: fa-content($fa-var-square); } +.#{$fa-css-prefix}-square-full:before { content: fa-content($fa-var-square-full); } +.#{$fa-css-prefix}-square-root-alt:before { content: fa-content($fa-var-square-root-alt); } +.#{$fa-css-prefix}-squarespace:before { content: fa-content($fa-var-squarespace); } +.#{$fa-css-prefix}-stack-exchange:before { content: fa-content($fa-var-stack-exchange); } +.#{$fa-css-prefix}-stack-overflow:before { content: fa-content($fa-var-stack-overflow); } +.#{$fa-css-prefix}-stackpath:before { content: fa-content($fa-var-stackpath); } +.#{$fa-css-prefix}-stamp:before { content: fa-content($fa-var-stamp); } +.#{$fa-css-prefix}-star:before { content: fa-content($fa-var-star); } +.#{$fa-css-prefix}-star-and-crescent:before { content: fa-content($fa-var-star-and-crescent); } +.#{$fa-css-prefix}-star-half:before { content: fa-content($fa-var-star-half); } +.#{$fa-css-prefix}-star-half-alt:before { content: fa-content($fa-var-star-half-alt); } +.#{$fa-css-prefix}-star-of-david:before { content: fa-content($fa-var-star-of-david); } +.#{$fa-css-prefix}-star-of-life:before { content: fa-content($fa-var-star-of-life); } +.#{$fa-css-prefix}-staylinked:before { content: fa-content($fa-var-staylinked); } +.#{$fa-css-prefix}-steam:before { content: fa-content($fa-var-steam); } +.#{$fa-css-prefix}-steam-square:before { content: fa-content($fa-var-steam-square); } +.#{$fa-css-prefix}-steam-symbol:before { content: fa-content($fa-var-steam-symbol); } +.#{$fa-css-prefix}-step-backward:before { content: fa-content($fa-var-step-backward); } +.#{$fa-css-prefix}-step-forward:before { content: fa-content($fa-var-step-forward); } +.#{$fa-css-prefix}-stethoscope:before { content: fa-content($fa-var-stethoscope); } +.#{$fa-css-prefix}-sticker-mule:before { content: fa-content($fa-var-sticker-mule); } +.#{$fa-css-prefix}-sticky-note:before { content: fa-content($fa-var-sticky-note); } +.#{$fa-css-prefix}-stop:before { content: fa-content($fa-var-stop); } +.#{$fa-css-prefix}-stop-circle:before { content: fa-content($fa-var-stop-circle); } +.#{$fa-css-prefix}-stopwatch:before { content: fa-content($fa-var-stopwatch); } +.#{$fa-css-prefix}-stopwatch-20:before { content: fa-content($fa-var-stopwatch-20); } +.#{$fa-css-prefix}-store:before { content: fa-content($fa-var-store); } +.#{$fa-css-prefix}-store-alt:before { content: fa-content($fa-var-store-alt); } +.#{$fa-css-prefix}-store-alt-slash:before { content: fa-content($fa-var-store-alt-slash); } +.#{$fa-css-prefix}-store-slash:before { content: fa-content($fa-var-store-slash); } +.#{$fa-css-prefix}-strava:before { content: fa-content($fa-var-strava); } +.#{$fa-css-prefix}-stream:before { content: fa-content($fa-var-stream); } +.#{$fa-css-prefix}-street-view:before { content: fa-content($fa-var-street-view); } +.#{$fa-css-prefix}-strikethrough:before { content: fa-content($fa-var-strikethrough); } +.#{$fa-css-prefix}-stripe:before { content: fa-content($fa-var-stripe); } +.#{$fa-css-prefix}-stripe-s:before { content: fa-content($fa-var-stripe-s); } +.#{$fa-css-prefix}-stroopwafel:before { content: fa-content($fa-var-stroopwafel); } +.#{$fa-css-prefix}-studiovinari:before { content: fa-content($fa-var-studiovinari); } +.#{$fa-css-prefix}-stumbleupon:before { content: fa-content($fa-var-stumbleupon); } +.#{$fa-css-prefix}-stumbleupon-circle:before { content: fa-content($fa-var-stumbleupon-circle); } +.#{$fa-css-prefix}-subscript:before { content: fa-content($fa-var-subscript); } +.#{$fa-css-prefix}-subway:before { content: fa-content($fa-var-subway); } +.#{$fa-css-prefix}-suitcase:before { content: fa-content($fa-var-suitcase); } +.#{$fa-css-prefix}-suitcase-rolling:before { content: fa-content($fa-var-suitcase-rolling); } +.#{$fa-css-prefix}-sun:before { content: fa-content($fa-var-sun); } +.#{$fa-css-prefix}-superpowers:before { content: fa-content($fa-var-superpowers); } +.#{$fa-css-prefix}-superscript:before { content: fa-content($fa-var-superscript); } +.#{$fa-css-prefix}-supple:before { content: fa-content($fa-var-supple); } +.#{$fa-css-prefix}-surprise:before { content: fa-content($fa-var-surprise); } +.#{$fa-css-prefix}-suse:before { content: fa-content($fa-var-suse); } +.#{$fa-css-prefix}-swatchbook:before { content: fa-content($fa-var-swatchbook); } +.#{$fa-css-prefix}-swift:before { content: fa-content($fa-var-swift); } +.#{$fa-css-prefix}-swimmer:before { content: fa-content($fa-var-swimmer); } +.#{$fa-css-prefix}-swimming-pool:before { content: fa-content($fa-var-swimming-pool); } +.#{$fa-css-prefix}-symfony:before { content: fa-content($fa-var-symfony); } +.#{$fa-css-prefix}-synagogue:before { content: fa-content($fa-var-synagogue); } +.#{$fa-css-prefix}-sync:before { content: fa-content($fa-var-sync); } +.#{$fa-css-prefix}-sync-alt:before { content: fa-content($fa-var-sync-alt); } +.#{$fa-css-prefix}-syringe:before { content: fa-content($fa-var-syringe); } +.#{$fa-css-prefix}-table:before { content: fa-content($fa-var-table); } +.#{$fa-css-prefix}-table-tennis:before { content: fa-content($fa-var-table-tennis); } +.#{$fa-css-prefix}-tablet:before { content: fa-content($fa-var-tablet); } +.#{$fa-css-prefix}-tablet-alt:before { content: fa-content($fa-var-tablet-alt); } +.#{$fa-css-prefix}-tablets:before { content: fa-content($fa-var-tablets); } +.#{$fa-css-prefix}-tachometer-alt:before { content: fa-content($fa-var-tachometer-alt); } +.#{$fa-css-prefix}-tag:before { content: fa-content($fa-var-tag); } +.#{$fa-css-prefix}-tags:before { content: fa-content($fa-var-tags); } +.#{$fa-css-prefix}-tape:before { content: fa-content($fa-var-tape); } +.#{$fa-css-prefix}-tasks:before { content: fa-content($fa-var-tasks); } +.#{$fa-css-prefix}-taxi:before { content: fa-content($fa-var-taxi); } +.#{$fa-css-prefix}-teamspeak:before { content: fa-content($fa-var-teamspeak); } +.#{$fa-css-prefix}-teeth:before { content: fa-content($fa-var-teeth); } +.#{$fa-css-prefix}-teeth-open:before { content: fa-content($fa-var-teeth-open); } +.#{$fa-css-prefix}-telegram:before { content: fa-content($fa-var-telegram); } +.#{$fa-css-prefix}-telegram-plane:before { content: fa-content($fa-var-telegram-plane); } +.#{$fa-css-prefix}-temperature-high:before { content: fa-content($fa-var-temperature-high); } +.#{$fa-css-prefix}-temperature-low:before { content: fa-content($fa-var-temperature-low); } +.#{$fa-css-prefix}-tencent-weibo:before { content: fa-content($fa-var-tencent-weibo); } +.#{$fa-css-prefix}-tenge:before { content: fa-content($fa-var-tenge); } +.#{$fa-css-prefix}-terminal:before { content: fa-content($fa-var-terminal); } +.#{$fa-css-prefix}-text-height:before { content: fa-content($fa-var-text-height); } +.#{$fa-css-prefix}-text-width:before { content: fa-content($fa-var-text-width); } +.#{$fa-css-prefix}-th:before { content: fa-content($fa-var-th); } +.#{$fa-css-prefix}-th-large:before { content: fa-content($fa-var-th-large); } +.#{$fa-css-prefix}-th-list:before { content: fa-content($fa-var-th-list); } +.#{$fa-css-prefix}-the-red-yeti:before { content: fa-content($fa-var-the-red-yeti); } +.#{$fa-css-prefix}-theater-masks:before { content: fa-content($fa-var-theater-masks); } +.#{$fa-css-prefix}-themeco:before { content: fa-content($fa-var-themeco); } +.#{$fa-css-prefix}-themeisle:before { content: fa-content($fa-var-themeisle); } +.#{$fa-css-prefix}-thermometer:before { content: fa-content($fa-var-thermometer); } +.#{$fa-css-prefix}-thermometer-empty:before { content: fa-content($fa-var-thermometer-empty); } +.#{$fa-css-prefix}-thermometer-full:before { content: fa-content($fa-var-thermometer-full); } +.#{$fa-css-prefix}-thermometer-half:before { content: fa-content($fa-var-thermometer-half); } +.#{$fa-css-prefix}-thermometer-quarter:before { content: fa-content($fa-var-thermometer-quarter); } +.#{$fa-css-prefix}-thermometer-three-quarters:before { content: fa-content($fa-var-thermometer-three-quarters); } +.#{$fa-css-prefix}-think-peaks:before { content: fa-content($fa-var-think-peaks); } +.#{$fa-css-prefix}-thumbs-down:before { content: fa-content($fa-var-thumbs-down); } +.#{$fa-css-prefix}-thumbs-up:before { content: fa-content($fa-var-thumbs-up); } +.#{$fa-css-prefix}-thumbtack:before { content: fa-content($fa-var-thumbtack); } +.#{$fa-css-prefix}-ticket-alt:before { content: fa-content($fa-var-ticket-alt); } +.#{$fa-css-prefix}-tiktok:before { content: fa-content($fa-var-tiktok); } +.#{$fa-css-prefix}-times:before { content: fa-content($fa-var-times); } +.#{$fa-css-prefix}-times-circle:before { content: fa-content($fa-var-times-circle); } +.#{$fa-css-prefix}-tint:before { content: fa-content($fa-var-tint); } +.#{$fa-css-prefix}-tint-slash:before { content: fa-content($fa-var-tint-slash); } +.#{$fa-css-prefix}-tired:before { content: fa-content($fa-var-tired); } +.#{$fa-css-prefix}-toggle-off:before { content: fa-content($fa-var-toggle-off); } +.#{$fa-css-prefix}-toggle-on:before { content: fa-content($fa-var-toggle-on); } +.#{$fa-css-prefix}-toilet:before { content: fa-content($fa-var-toilet); } +.#{$fa-css-prefix}-toilet-paper:before { content: fa-content($fa-var-toilet-paper); } +.#{$fa-css-prefix}-toilet-paper-slash:before { content: fa-content($fa-var-toilet-paper-slash); } +.#{$fa-css-prefix}-toolbox:before { content: fa-content($fa-var-toolbox); } +.#{$fa-css-prefix}-tools:before { content: fa-content($fa-var-tools); } +.#{$fa-css-prefix}-tooth:before { content: fa-content($fa-var-tooth); } +.#{$fa-css-prefix}-torah:before { content: fa-content($fa-var-torah); } +.#{$fa-css-prefix}-torii-gate:before { content: fa-content($fa-var-torii-gate); } +.#{$fa-css-prefix}-tractor:before { content: fa-content($fa-var-tractor); } +.#{$fa-css-prefix}-trade-federation:before { content: fa-content($fa-var-trade-federation); } +.#{$fa-css-prefix}-trademark:before { content: fa-content($fa-var-trademark); } +.#{$fa-css-prefix}-traffic-light:before { content: fa-content($fa-var-traffic-light); } +.#{$fa-css-prefix}-trailer:before { content: fa-content($fa-var-trailer); } +.#{$fa-css-prefix}-train:before { content: fa-content($fa-var-train); } +.#{$fa-css-prefix}-tram:before { content: fa-content($fa-var-tram); } +.#{$fa-css-prefix}-transgender:before { content: fa-content($fa-var-transgender); } +.#{$fa-css-prefix}-transgender-alt:before { content: fa-content($fa-var-transgender-alt); } +.#{$fa-css-prefix}-trash:before { content: fa-content($fa-var-trash); } +.#{$fa-css-prefix}-trash-alt:before { content: fa-content($fa-var-trash-alt); } +.#{$fa-css-prefix}-trash-restore:before { content: fa-content($fa-var-trash-restore); } +.#{$fa-css-prefix}-trash-restore-alt:before { content: fa-content($fa-var-trash-restore-alt); } +.#{$fa-css-prefix}-tree:before { content: fa-content($fa-var-tree); } +.#{$fa-css-prefix}-trello:before { content: fa-content($fa-var-trello); } +.#{$fa-css-prefix}-tripadvisor:before { content: fa-content($fa-var-tripadvisor); } +.#{$fa-css-prefix}-trophy:before { content: fa-content($fa-var-trophy); } +.#{$fa-css-prefix}-truck:before { content: fa-content($fa-var-truck); } +.#{$fa-css-prefix}-truck-loading:before { content: fa-content($fa-var-truck-loading); } +.#{$fa-css-prefix}-truck-monster:before { content: fa-content($fa-var-truck-monster); } +.#{$fa-css-prefix}-truck-moving:before { content: fa-content($fa-var-truck-moving); } +.#{$fa-css-prefix}-truck-pickup:before { content: fa-content($fa-var-truck-pickup); } +.#{$fa-css-prefix}-tshirt:before { content: fa-content($fa-var-tshirt); } +.#{$fa-css-prefix}-tty:before { content: fa-content($fa-var-tty); } +.#{$fa-css-prefix}-tumblr:before { content: fa-content($fa-var-tumblr); } +.#{$fa-css-prefix}-tumblr-square:before { content: fa-content($fa-var-tumblr-square); } +.#{$fa-css-prefix}-tv:before { content: fa-content($fa-var-tv); } +.#{$fa-css-prefix}-twitch:before { content: fa-content($fa-var-twitch); } +.#{$fa-css-prefix}-twitter:before { content: fa-content($fa-var-twitter); } +.#{$fa-css-prefix}-twitter-square:before { content: fa-content($fa-var-twitter-square); } +.#{$fa-css-prefix}-typo3:before { content: fa-content($fa-var-typo3); } +.#{$fa-css-prefix}-uber:before { content: fa-content($fa-var-uber); } +.#{$fa-css-prefix}-ubuntu:before { content: fa-content($fa-var-ubuntu); } +.#{$fa-css-prefix}-uikit:before { content: fa-content($fa-var-uikit); } +.#{$fa-css-prefix}-umbraco:before { content: fa-content($fa-var-umbraco); } +.#{$fa-css-prefix}-umbrella:before { content: fa-content($fa-var-umbrella); } +.#{$fa-css-prefix}-umbrella-beach:before { content: fa-content($fa-var-umbrella-beach); } +.#{$fa-css-prefix}-underline:before { content: fa-content($fa-var-underline); } +.#{$fa-css-prefix}-undo:before { content: fa-content($fa-var-undo); } +.#{$fa-css-prefix}-undo-alt:before { content: fa-content($fa-var-undo-alt); } +.#{$fa-css-prefix}-uniregistry:before { content: fa-content($fa-var-uniregistry); } +.#{$fa-css-prefix}-unity:before { content: fa-content($fa-var-unity); } +.#{$fa-css-prefix}-universal-access:before { content: fa-content($fa-var-universal-access); } +.#{$fa-css-prefix}-university:before { content: fa-content($fa-var-university); } +.#{$fa-css-prefix}-unlink:before { content: fa-content($fa-var-unlink); } +.#{$fa-css-prefix}-unlock:before { content: fa-content($fa-var-unlock); } +.#{$fa-css-prefix}-unlock-alt:before { content: fa-content($fa-var-unlock-alt); } +.#{$fa-css-prefix}-unsplash:before { content: fa-content($fa-var-unsplash); } +.#{$fa-css-prefix}-untappd:before { content: fa-content($fa-var-untappd); } +.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-upload); } +.#{$fa-css-prefix}-ups:before { content: fa-content($fa-var-ups); } +.#{$fa-css-prefix}-usb:before { content: fa-content($fa-var-usb); } +.#{$fa-css-prefix}-user:before { content: fa-content($fa-var-user); } +.#{$fa-css-prefix}-user-alt:before { content: fa-content($fa-var-user-alt); } +.#{$fa-css-prefix}-user-alt-slash:before { content: fa-content($fa-var-user-alt-slash); } +.#{$fa-css-prefix}-user-astronaut:before { content: fa-content($fa-var-user-astronaut); } +.#{$fa-css-prefix}-user-check:before { content: fa-content($fa-var-user-check); } +.#{$fa-css-prefix}-user-circle:before { content: fa-content($fa-var-user-circle); } +.#{$fa-css-prefix}-user-clock:before { content: fa-content($fa-var-user-clock); } +.#{$fa-css-prefix}-user-cog:before { content: fa-content($fa-var-user-cog); } +.#{$fa-css-prefix}-user-edit:before { content: fa-content($fa-var-user-edit); } +.#{$fa-css-prefix}-user-friends:before { content: fa-content($fa-var-user-friends); } +.#{$fa-css-prefix}-user-graduate:before { content: fa-content($fa-var-user-graduate); } +.#{$fa-css-prefix}-user-injured:before { content: fa-content($fa-var-user-injured); } +.#{$fa-css-prefix}-user-lock:before { content: fa-content($fa-var-user-lock); } +.#{$fa-css-prefix}-user-md:before { content: fa-content($fa-var-user-md); } +.#{$fa-css-prefix}-user-minus:before { content: fa-content($fa-var-user-minus); } +.#{$fa-css-prefix}-user-ninja:before { content: fa-content($fa-var-user-ninja); } +.#{$fa-css-prefix}-user-nurse:before { content: fa-content($fa-var-user-nurse); } +.#{$fa-css-prefix}-user-plus:before { content: fa-content($fa-var-user-plus); } +.#{$fa-css-prefix}-user-secret:before { content: fa-content($fa-var-user-secret); } +.#{$fa-css-prefix}-user-shield:before { content: fa-content($fa-var-user-shield); } +.#{$fa-css-prefix}-user-slash:before { content: fa-content($fa-var-user-slash); } +.#{$fa-css-prefix}-user-tag:before { content: fa-content($fa-var-user-tag); } +.#{$fa-css-prefix}-user-tie:before { content: fa-content($fa-var-user-tie); } +.#{$fa-css-prefix}-user-times:before { content: fa-content($fa-var-user-times); } +.#{$fa-css-prefix}-users:before { content: fa-content($fa-var-users); } +.#{$fa-css-prefix}-users-cog:before { content: fa-content($fa-var-users-cog); } +.#{$fa-css-prefix}-users-slash:before { content: fa-content($fa-var-users-slash); } +.#{$fa-css-prefix}-usps:before { content: fa-content($fa-var-usps); } +.#{$fa-css-prefix}-ussunnah:before { content: fa-content($fa-var-ussunnah); } +.#{$fa-css-prefix}-utensil-spoon:before { content: fa-content($fa-var-utensil-spoon); } +.#{$fa-css-prefix}-utensils:before { content: fa-content($fa-var-utensils); } +.#{$fa-css-prefix}-vaadin:before { content: fa-content($fa-var-vaadin); } +.#{$fa-css-prefix}-vector-square:before { content: fa-content($fa-var-vector-square); } +.#{$fa-css-prefix}-venus:before { content: fa-content($fa-var-venus); } +.#{$fa-css-prefix}-venus-double:before { content: fa-content($fa-var-venus-double); } +.#{$fa-css-prefix}-venus-mars:before { content: fa-content($fa-var-venus-mars); } +.#{$fa-css-prefix}-viacoin:before { content: fa-content($fa-var-viacoin); } +.#{$fa-css-prefix}-viadeo:before { content: fa-content($fa-var-viadeo); } +.#{$fa-css-prefix}-viadeo-square:before { content: fa-content($fa-var-viadeo-square); } +.#{$fa-css-prefix}-vial:before { content: fa-content($fa-var-vial); } +.#{$fa-css-prefix}-vials:before { content: fa-content($fa-var-vials); } +.#{$fa-css-prefix}-viber:before { content: fa-content($fa-var-viber); } +.#{$fa-css-prefix}-video:before { content: fa-content($fa-var-video); } +.#{$fa-css-prefix}-video-slash:before { content: fa-content($fa-var-video-slash); } +.#{$fa-css-prefix}-vihara:before { content: fa-content($fa-var-vihara); } +.#{$fa-css-prefix}-vimeo:before { content: fa-content($fa-var-vimeo); } +.#{$fa-css-prefix}-vimeo-square:before { content: fa-content($fa-var-vimeo-square); } +.#{$fa-css-prefix}-vimeo-v:before { content: fa-content($fa-var-vimeo-v); } +.#{$fa-css-prefix}-vine:before { content: fa-content($fa-var-vine); } +.#{$fa-css-prefix}-virus:before { content: fa-content($fa-var-virus); } +.#{$fa-css-prefix}-virus-slash:before { content: fa-content($fa-var-virus-slash); } +.#{$fa-css-prefix}-viruses:before { content: fa-content($fa-var-viruses); } +.#{$fa-css-prefix}-vk:before { content: fa-content($fa-var-vk); } +.#{$fa-css-prefix}-vnv:before { content: fa-content($fa-var-vnv); } +.#{$fa-css-prefix}-voicemail:before { content: fa-content($fa-var-voicemail); } +.#{$fa-css-prefix}-volleyball-ball:before { content: fa-content($fa-var-volleyball-ball); } +.#{$fa-css-prefix}-volume-down:before { content: fa-content($fa-var-volume-down); } +.#{$fa-css-prefix}-volume-mute:before { content: fa-content($fa-var-volume-mute); } +.#{$fa-css-prefix}-volume-off:before { content: fa-content($fa-var-volume-off); } +.#{$fa-css-prefix}-volume-up:before { content: fa-content($fa-var-volume-up); } +.#{$fa-css-prefix}-vote-yea:before { content: fa-content($fa-var-vote-yea); } +.#{$fa-css-prefix}-vr-cardboard:before { content: fa-content($fa-var-vr-cardboard); } +.#{$fa-css-prefix}-vuejs:before { content: fa-content($fa-var-vuejs); } +.#{$fa-css-prefix}-walking:before { content: fa-content($fa-var-walking); } +.#{$fa-css-prefix}-wallet:before { content: fa-content($fa-var-wallet); } +.#{$fa-css-prefix}-warehouse:before { content: fa-content($fa-var-warehouse); } +.#{$fa-css-prefix}-water:before { content: fa-content($fa-var-water); } +.#{$fa-css-prefix}-wave-square:before { content: fa-content($fa-var-wave-square); } +.#{$fa-css-prefix}-waze:before { content: fa-content($fa-var-waze); } +.#{$fa-css-prefix}-weebly:before { content: fa-content($fa-var-weebly); } +.#{$fa-css-prefix}-weibo:before { content: fa-content($fa-var-weibo); } +.#{$fa-css-prefix}-weight:before { content: fa-content($fa-var-weight); } +.#{$fa-css-prefix}-weight-hanging:before { content: fa-content($fa-var-weight-hanging); } +.#{$fa-css-prefix}-weixin:before { content: fa-content($fa-var-weixin); } +.#{$fa-css-prefix}-whatsapp:before { content: fa-content($fa-var-whatsapp); } +.#{$fa-css-prefix}-whatsapp-square:before { content: fa-content($fa-var-whatsapp-square); } +.#{$fa-css-prefix}-wheelchair:before { content: fa-content($fa-var-wheelchair); } +.#{$fa-css-prefix}-whmcs:before { content: fa-content($fa-var-whmcs); } +.#{$fa-css-prefix}-wifi:before { content: fa-content($fa-var-wifi); } +.#{$fa-css-prefix}-wikipedia-w:before { content: fa-content($fa-var-wikipedia-w); } +.#{$fa-css-prefix}-wind:before { content: fa-content($fa-var-wind); } +.#{$fa-css-prefix}-window-close:before { content: fa-content($fa-var-window-close); } +.#{$fa-css-prefix}-window-maximize:before { content: fa-content($fa-var-window-maximize); } +.#{$fa-css-prefix}-window-minimize:before { content: fa-content($fa-var-window-minimize); } +.#{$fa-css-prefix}-window-restore:before { content: fa-content($fa-var-window-restore); } +.#{$fa-css-prefix}-windows:before { content: fa-content($fa-var-windows); } +.#{$fa-css-prefix}-wine-bottle:before { content: fa-content($fa-var-wine-bottle); } +.#{$fa-css-prefix}-wine-glass:before { content: fa-content($fa-var-wine-glass); } +.#{$fa-css-prefix}-wine-glass-alt:before { content: fa-content($fa-var-wine-glass-alt); } +.#{$fa-css-prefix}-wix:before { content: fa-content($fa-var-wix); } +.#{$fa-css-prefix}-wizards-of-the-coast:before { content: fa-content($fa-var-wizards-of-the-coast); } +.#{$fa-css-prefix}-wolf-pack-battalion:before { content: fa-content($fa-var-wolf-pack-battalion); } +.#{$fa-css-prefix}-won-sign:before { content: fa-content($fa-var-won-sign); } +.#{$fa-css-prefix}-wordpress:before { content: fa-content($fa-var-wordpress); } +.#{$fa-css-prefix}-wordpress-simple:before { content: fa-content($fa-var-wordpress-simple); } +.#{$fa-css-prefix}-wpbeginner:before { content: fa-content($fa-var-wpbeginner); } +.#{$fa-css-prefix}-wpexplorer:before { content: fa-content($fa-var-wpexplorer); } +.#{$fa-css-prefix}-wpforms:before { content: fa-content($fa-var-wpforms); } +.#{$fa-css-prefix}-wpressr:before { content: fa-content($fa-var-wpressr); } +.#{$fa-css-prefix}-wrench:before { content: fa-content($fa-var-wrench); } +.#{$fa-css-prefix}-x-ray:before { content: fa-content($fa-var-x-ray); } +.#{$fa-css-prefix}-xbox:before { content: fa-content($fa-var-xbox); } +.#{$fa-css-prefix}-xing:before { content: fa-content($fa-var-xing); } +.#{$fa-css-prefix}-xing-square:before { content: fa-content($fa-var-xing-square); } +.#{$fa-css-prefix}-y-combinator:before { content: fa-content($fa-var-y-combinator); } +.#{$fa-css-prefix}-yahoo:before { content: fa-content($fa-var-yahoo); } +.#{$fa-css-prefix}-yammer:before { content: fa-content($fa-var-yammer); } +.#{$fa-css-prefix}-yandex:before { content: fa-content($fa-var-yandex); } +.#{$fa-css-prefix}-yandex-international:before { content: fa-content($fa-var-yandex-international); } +.#{$fa-css-prefix}-yarn:before { content: fa-content($fa-var-yarn); } +.#{$fa-css-prefix}-yelp:before { content: fa-content($fa-var-yelp); } +.#{$fa-css-prefix}-yen-sign:before { content: fa-content($fa-var-yen-sign); } +.#{$fa-css-prefix}-yin-yang:before { content: fa-content($fa-var-yin-yang); } +.#{$fa-css-prefix}-yoast:before { content: fa-content($fa-var-yoast); } +.#{$fa-css-prefix}-youtube:before { content: fa-content($fa-var-youtube); } +.#{$fa-css-prefix}-youtube-square:before { content: fa-content($fa-var-youtube-square); } +.#{$fa-css-prefix}-zhihu:before { content: fa-content($fa-var-zhihu); } diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_larger.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_larger.scss new file mode 100644 index 0000000..58a3f67 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_larger.scss @@ -0,0 +1,23 @@ +// Icon Sizes +// ------------------------- +@use 'sass:math'; +// makes the font 33% larger relative to the icon container +.#{$fa-css-prefix}-lg { + font-size: (math.div(4em, 3)); + line-height: (math.div(3em, 4)); + vertical-align: -0.0667em; +} + +.#{$fa-css-prefix}-xs { + font-size: 0.75em; +} + +.#{$fa-css-prefix}-sm { + font-size: 0.875em; +} + +@for $i from 1 through 10 { + .#{$fa-css-prefix}-#{$i}x { + font-size: $i * 1em; + } +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_list.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_list.scss new file mode 100644 index 0000000..a44483e --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_list.scss @@ -0,0 +1,21 @@ +// List Icons +// ------------------------- +@use 'sass:math'; + +.#{$fa-css-prefix}-ul { + list-style-type: none; + margin-left: math.div($fa-li-width * 5, 4); + padding-left: 0; + + > li { + position: relative; + } +} + +.#{$fa-css-prefix}-li { + left: -$fa-li-width; + position: absolute; + text-align: center; + width: $fa-li-width; + line-height: inherit; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_mixins.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_mixins.scss new file mode 100644 index 0000000..55baeeb --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_mixins.scss @@ -0,0 +1,56 @@ +// Mixins +// -------------------------- + +@mixin fa-icon { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: inline-block; + font-style: normal; + font-variant: normal; + font-weight: normal; + line-height: 1; +} + +@mixin fa-icon-rotate($degrees, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; + transform: rotate($degrees); +} + +@mixin fa-icon-flip($horiz, $vert, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; + transform: scale($horiz, $vert); +} + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +@mixin sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +@mixin sr-only-focusable { + &:active, + &:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; + } +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_rotated-flipped.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_rotated-flipped.scss new file mode 100644 index 0000000..164d972 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_rotated-flipped.scss @@ -0,0 +1,24 @@ +// Rotated & Flipped Icons +// ------------------------- + +.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } +.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } +.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } + +.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } +.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } +.#{$fa-css-prefix}-flip-both, .#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(-1, -1, 2); } + +// Hook for IE8-9 +// ------------------------- + +:root { + .#{$fa-css-prefix}-rotate-90, + .#{$fa-css-prefix}-rotate-180, + .#{$fa-css-prefix}-rotate-270, + .#{$fa-css-prefix}-flip-horizontal, + .#{$fa-css-prefix}-flip-vertical, + .#{$fa-css-prefix}-flip-both { + filter: none; + } +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_screen-reader.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_screen-reader.scss new file mode 100644 index 0000000..5d0ab26 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_screen-reader.scss @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { @include sr-only; } +.sr-only-focusable { @include sr-only-focusable; } diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_shims.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_shims.scss new file mode 100644 index 0000000..d175344 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_shims.scss @@ -0,0 +1,2066 @@ +.#{$fa-css-prefix}.#{$fa-css-prefix}-glass:before { content: fa-content($fa-var-glass-martini); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-meetup { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-o:before { content: fa-content($fa-var-star); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-remove:before { content: fa-content($fa-var-times); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-close:before { content: fa-content($fa-var-times); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gear:before { content: fa-content($fa-var-cog); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-trash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-trash-o:before { content: fa-content($fa-var-trash-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-o:before { content: fa-content($fa-var-file); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-clock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-clock-o:before { content: fa-content($fa-var-clock); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-down:before { content: fa-content($fa-var-arrow-alt-circle-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-up:before { content: fa-content($fa-var-arrow-alt-circle-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-play-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-play-circle-o:before { content: fa-content($fa-var-play-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-repeat:before { content: fa-content($fa-var-redo); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rotate-right:before { content: fa-content($fa-var-redo); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-refresh:before { content: fa-content($fa-var-sync); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-list-alt { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dedent:before { content: fa-content($fa-var-outdent); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-video-camera:before { content: fa-content($fa-var-video); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-picture-o:before { content: fa-content($fa-var-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-photo { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-photo:before { content: fa-content($fa-var-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-image { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-image:before { content: fa-content($fa-var-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pencil:before { content: fa-content($fa-var-pencil-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-map-marker:before { content: fa-content($fa-var-map-marker-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square-o:before { content: fa-content($fa-var-edit); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-share-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-share-square-o:before { content: fa-content($fa-var-share-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-check-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-check-square-o:before { content: fa-content($fa-var-check-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrows:before { content: fa-content($fa-var-arrows-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-times-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-times-circle-o:before { content: fa-content($fa-var-times-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-check-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-check-circle-o:before { content: fa-content($fa-var-check-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mail-forward:before { content: fa-content($fa-var-share); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-expand:before { content: fa-content($fa-var-expand-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-compress:before { content: fa-content($fa-var-compress-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-eye { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-eye-slash { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-warning:before { content: fa-content($fa-var-exclamation-triangle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar:before { content: fa-content($fa-var-calendar-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-v:before { content: fa-content($fa-var-arrows-alt-v); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-h:before { content: fa-content($fa-var-arrows-alt-h); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart:before { content: fa-content($fa-var-chart-bar); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bar-chart-o:before { content: fa-content($fa-var-chart-bar); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-twitter-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gears:before { content: fa-content($fa-var-cogs); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-up:before { content: fa-content($fa-var-thumbs-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-thumbs-o-down:before { content: fa-content($fa-var-thumbs-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-heart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-heart-o:before { content: fa-content($fa-var-heart); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sign-out:before { content: fa-content($fa-var-sign-out-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin-square:before { content: fa-content($fa-var-linkedin); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thumb-tack:before { content: fa-content($fa-var-thumbtack); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-external-link:before { content: fa-content($fa-var-external-link-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sign-in:before { content: fa-content($fa-var-sign-in-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-github-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-lemon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-lemon-o:before { content: fa-content($fa-var-lemon); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-square-o:before { content: fa-content($fa-var-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bookmark-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bookmark-o:before { content: fa-content($fa-var-bookmark); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-twitter { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook:before { content: fa-content($fa-var-facebook-f); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-f { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-f:before { content: fa-content($fa-var-facebook-f); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-github { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-credit-card { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-feed:before { content: fa-content($fa-var-rss); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hdd-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hdd-o:before { content: fa-content($fa-var-hdd); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-right:before { content: fa-content($fa-var-hand-point-right); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-left:before { content: fa-content($fa-var-hand-point-left); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-up:before { content: fa-content($fa-var-hand-point-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-o-down:before { content: fa-content($fa-var-hand-point-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrows-alt:before { content: fa-content($fa-var-expand-arrows-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-group:before { content: fa-content($fa-var-users); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-chain:before { content: fa-content($fa-var-link); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-scissors:before { content: fa-content($fa-var-cut); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-files-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-files-o:before { content: fa-content($fa-var-copy); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-floppy-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-floppy-o:before { content: fa-content($fa-var-save); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-navicon:before { content: fa-content($fa-var-bars); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-reorder:before { content: fa-content($fa-var-bars); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus:before { content: fa-content($fa-var-google-plus-g); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-money { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-money:before { content: fa-content($fa-var-money-bill-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-unsorted:before { content: fa-content($fa-var-sort); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-desc:before { content: fa-content($fa-var-sort-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-asc:before { content: fa-content($fa-var-sort-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-linkedin:before { content: fa-content($fa-var-linkedin-in); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rotate-left:before { content: fa-content($fa-var-undo); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-legal:before { content: fa-content($fa-var-gavel); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tachometer:before { content: fa-content($fa-var-tachometer-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dashboard:before { content: fa-content($fa-var-tachometer-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-comment-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-comment-o:before { content: fa-content($fa-var-comment); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-comments-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-comments-o:before { content: fa-content($fa-var-comments); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-flash:before { content: fa-content($fa-var-bolt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-clipboard { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-paste { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-paste:before { content: fa-content($fa-var-clipboard); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-lightbulb-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-lightbulb-o:before { content: fa-content($fa-var-lightbulb); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-exchange:before { content: fa-content($fa-var-exchange-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cloud-download:before { content: fa-content($fa-var-cloud-download-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cloud-upload:before { content: fa-content($fa-var-cloud-upload-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bell-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bell-o:before { content: fa-content($fa-var-bell); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cutlery:before { content: fa-content($fa-var-utensils); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-text-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-text-o:before { content: fa-content($fa-var-file-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-building-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-building-o:before { content: fa-content($fa-var-building); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hospital-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hospital-o:before { content: fa-content($fa-var-hospital); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tablet:before { content: fa-content($fa-var-tablet-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mobile:before { content: fa-content($fa-var-mobile-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mobile-phone:before { content: fa-content($fa-var-mobile-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o:before { content: fa-content($fa-var-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mail-reply:before { content: fa-content($fa-var-reply); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-github-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-folder-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-folder-o:before { content: fa-content($fa-var-folder); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-folder-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-folder-open-o:before { content: fa-content($fa-var-folder-open); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-smile-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-smile-o:before { content: fa-content($fa-var-smile); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-frown-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-frown-o:before { content: fa-content($fa-var-frown); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-meh-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-meh-o:before { content: fa-content($fa-var-meh); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-keyboard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-keyboard-o:before { content: fa-content($fa-var-keyboard); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-flag-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-flag-o:before { content: fa-content($fa-var-flag); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mail-reply-all:before { content: fa-content($fa-var-reply-all); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-o:before { content: fa-content($fa-var-star-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-empty { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-empty:before { content: fa-content($fa-var-star-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-full { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-star-half-full:before { content: fa-content($fa-var-star-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-code-fork:before { content: fa-content($fa-var-code-branch); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-chain-broken:before { content: fa-content($fa-var-unlink); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-shield:before { content: fa-content($fa-var-shield-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-o:before { content: fa-content($fa-var-calendar); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-maxcdn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-html5 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-css3 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ticket:before { content: fa-content($fa-var-ticket-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-minus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-minus-square-o:before { content: fa-content($fa-var-minus-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-level-up:before { content: fa-content($fa-var-level-up-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-level-down:before { content: fa-content($fa-var-level-down-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pencil-square:before { content: fa-content($fa-var-pen-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-external-link-square:before { content: fa-content($fa-var-external-link-square-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-compass { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-down:before { content: fa-content($fa-var-caret-square-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-down:before { content: fa-content($fa-var-caret-square-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-up:before { content: fa-content($fa-var-caret-square-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-up:before { content: fa-content($fa-var-caret-square-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-right:before { content: fa-content($fa-var-caret-square-right); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-right:before { content: fa-content($fa-var-caret-square-right); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-eur:before { content: fa-content($fa-var-euro-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-euro:before { content: fa-content($fa-var-euro-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gbp:before { content: fa-content($fa-var-pound-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-usd:before { content: fa-content($fa-var-dollar-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dollar:before { content: fa-content($fa-var-dollar-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-inr:before { content: fa-content($fa-var-rupee-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rupee:before { content: fa-content($fa-var-rupee-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-jpy:before { content: fa-content($fa-var-yen-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cny:before { content: fa-content($fa-var-yen-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rmb:before { content: fa-content($fa-var-yen-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yen:before { content: fa-content($fa-var-yen-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rub:before { content: fa-content($fa-var-ruble-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ruble:before { content: fa-content($fa-var-ruble-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rouble:before { content: fa-content($fa-var-ruble-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-krw:before { content: fa-content($fa-var-won-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-won:before { content: fa-content($fa-var-won-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-btc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bitcoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bitcoin:before { content: fa-content($fa-var-btc); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-text:before { content: fa-content($fa-var-file-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-alpha-asc:before { content: fa-content($fa-var-sort-alpha-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-alpha-desc:before { content: fa-content($fa-var-sort-alpha-down-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-amount-asc:before { content: fa-content($fa-var-sort-amount-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-amount-desc:before { content: fa-content($fa-var-sort-amount-down-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-numeric-asc:before { content: fa-content($fa-var-sort-numeric-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sort-numeric-desc:before { content: fa-content($fa-var-sort-numeric-down-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-youtube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-xing { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-xing-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-play { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-youtube-play:before { content: fa-content($fa-var-youtube); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dropbox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-stack-overflow { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-instagram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-flickr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-adn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bitbucket-square:before { content: fa-content($fa-var-bitbucket); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tumblr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tumblr-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-down:before { content: fa-content($fa-var-long-arrow-alt-down); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-up:before { content: fa-content($fa-var-long-arrow-alt-up); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-left:before { content: fa-content($fa-var-long-arrow-alt-left); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-long-arrow-right:before { content: fa-content($fa-var-long-arrow-alt-right); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-apple { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-windows { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-android { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-linux { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dribbble { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-skype { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-foursquare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-trello { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gratipay { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gittip { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-gittip:before { content: fa-content($fa-var-gratipay); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sun-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-sun-o:before { content: fa-content($fa-var-sun); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-moon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-moon-o:before { content: fa-content($fa-var-moon); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-renren { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pagelines { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-stack-exchange { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-right:before { content: fa-content($fa-var-arrow-alt-circle-right); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-arrow-circle-o-left:before { content: fa-content($fa-var-arrow-alt-circle-left); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-caret-square-o-left:before { content: fa-content($fa-var-caret-square-left); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-toggle-left:before { content: fa-content($fa-var-caret-square-left); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dot-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-dot-circle-o:before { content: fa-content($fa-var-dot-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-try:before { content: fa-content($fa-var-lira-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-turkish-lira:before { content: fa-content($fa-var-lira-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-plus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-plus-square-o:before { content: fa-content($fa-var-plus-square); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-slack { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wordpress { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-openid { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-institution:before { content: fa-content($fa-var-university); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bank:before { content: fa-content($fa-var-university); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mortar-board:before { content: fa-content($fa-var-graduation-cap); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yahoo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-reddit { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-reddit-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-stumbleupon-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-stumbleupon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-delicious { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-digg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper-pp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-drupal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-joomla { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-spoon:before { content: fa-content($fa-var-utensil-spoon); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-behance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-behance-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-steam { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-steam-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-automobile:before { content: fa-content($fa-var-car); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-o:before { content: fa-content($fa-var-envelope); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-spotify { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-deviantart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-soundcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-pdf-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-pdf-o:before { content: fa-content($fa-var-file-pdf); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-word-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-word-o:before { content: fa-content($fa-var-file-word); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-excel-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-excel-o:before { content: fa-content($fa-var-file-excel); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-powerpoint-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-powerpoint-o:before { content: fa-content($fa-var-file-powerpoint); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-image-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-image-o:before { content: fa-content($fa-var-file-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-photo-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-photo-o:before { content: fa-content($fa-var-file-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-picture-o:before { content: fa-content($fa-var-file-image); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-archive-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-archive-o:before { content: fa-content($fa-var-file-archive); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-zip-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-zip-o:before { content: fa-content($fa-var-file-archive); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-audio-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-audio-o:before { content: fa-content($fa-var-file-audio); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-sound-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-sound-o:before { content: fa-content($fa-var-file-audio); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-video-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-video-o:before { content: fa-content($fa-var-file-video); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-movie-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-movie-o:before { content: fa-content($fa-var-file-video); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-code-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-file-code-o:before { content: fa-content($fa-var-file-code); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vine { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-codepen { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-jsfiddle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-ring { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-bouy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-bouy:before { content: fa-content($fa-var-life-ring); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-buoy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-buoy:before { content: fa-content($fa-var-life-ring); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-saver { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-life-saver:before { content: fa-content($fa-var-life-ring); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-support { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-support:before { content: fa-content($fa-var-life-ring); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-circle-o-notch:before { content: fa-content($fa-var-circle-notch); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-rebel { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ra { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-ra:before { content: fa-content($fa-var-rebel); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-resistance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-resistance:before { content: fa-content($fa-var-rebel); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-empire { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-ge:before { content: fa-content($fa-var-empire); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-git-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-git { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hacker-news { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator-square:before { content: fa-content($fa-var-hacker-news); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yc-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-yc-square:before { content: fa-content($fa-var-hacker-news); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tencent-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-qq { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-weixin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wechat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-wechat:before { content: fa-content($fa-var-weixin); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-send:before { content: fa-content($fa-var-paper-plane); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-paper-plane-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-paper-plane-o:before { content: fa-content($fa-var-paper-plane); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-send-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-send-o:before { content: fa-content($fa-var-paper-plane); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-circle-thin { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-circle-thin:before { content: fa-content($fa-var-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-header:before { content: fa-content($fa-var-heading); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sliders:before { content: fa-content($fa-var-sliders-h); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-futbol-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-futbol-o:before { content: fa-content($fa-var-futbol); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-soccer-ball-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-soccer-ball-o:before { content: fa-content($fa-var-futbol); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-slideshare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-twitch { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yelp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-newspaper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-newspaper-o:before { content: fa-content($fa-var-newspaper); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-wallet { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-visa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-mastercard { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-discover { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-amex { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-stripe { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bell-slash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-bell-slash-o:before { content: fa-content($fa-var-bell-slash); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-trash:before { content: fa-content($fa-var-trash-alt); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-copyright { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-eyedropper:before { content: fa-content($fa-var-eye-dropper); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-area-chart:before { content: fa-content($fa-var-chart-area); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pie-chart:before { content: fa-content($fa-var-chart-pie); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-line-chart:before { content: fa-content($fa-var-chart-line); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-lastfm { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-lastfm-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ioxhost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-angellist { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc:before { content: fa-content($fa-var-closed-captioning); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ils:before { content: fa-content($fa-var-shekel-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-shekel:before { content: fa-content($fa-var-shekel-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sheqel:before { content: fa-content($fa-var-shekel-sign); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-meanpath { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-meanpath:before { content: fa-content($fa-var-font-awesome); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-buysellads { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-connectdevelop { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-dashcube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-forumbee { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-leanpub { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sellsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-shirtsinbulk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-simplybuilt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-skyatlas { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-diamond { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-diamond:before { content: fa-content($fa-var-gem); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-intersex:before { content: fa-content($fa-var-transgender); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-facebook-official:before { content: fa-content($fa-var-facebook); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pinterest-p { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-whatsapp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hotel:before { content: fa-content($fa-var-bed); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-viacoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-medium { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-y-combinator { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-yc:before { content: fa-content($fa-var-y-combinator); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-optin-monster { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-opencart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-expeditedssl { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery-4:before { content: fa-content($fa-var-battery-full); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery:before { content: fa-content($fa-var-battery-full); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery-3:before { content: fa-content($fa-var-battery-three-quarters); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery-2:before { content: fa-content($fa-var-battery-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery-1:before { content: fa-content($fa-var-battery-quarter); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-battery-0:before { content: fa-content($fa-var-battery-empty); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-object-group { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-object-ungroup { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-sticky-note-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-sticky-note-o:before { content: fa-content($fa-var-sticky-note); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-jcb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cc-diners-club { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-clone { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-o:before { content: fa-content($fa-var-hourglass); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-1:before { content: fa-content($fa-var-hourglass-start); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-2:before { content: fa-content($fa-var-hourglass-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hourglass-3:before { content: fa-content($fa-var-hourglass-end); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-rock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-rock-o:before { content: fa-content($fa-var-hand-rock); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-grab-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-grab-o:before { content: fa-content($fa-var-hand-rock); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-paper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-paper-o:before { content: fa-content($fa-var-hand-paper); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-stop-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-stop-o:before { content: fa-content($fa-var-hand-paper); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-scissors-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-scissors-o:before { content: fa-content($fa-var-hand-scissors); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-lizard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-lizard-o:before { content: fa-content($fa-var-hand-lizard); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-spock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-spock-o:before { content: fa-content($fa-var-hand-spock); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-pointer-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-pointer-o:before { content: fa-content($fa-var-hand-pointer); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-peace-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-hand-peace-o:before { content: fa-content($fa-var-hand-peace); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-registered { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-creative-commons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gg-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-tripadvisor { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-odnoklassniki { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-odnoklassniki-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-get-pocket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wikipedia-w { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-safari { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-chrome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-firefox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-opera { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-internet-explorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-television:before { content: fa-content($fa-var-tv); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-contao { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-500px { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-amazon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-plus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-plus-o:before { content: fa-content($fa-var-calendar-plus); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-minus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-minus-o:before { content: fa-content($fa-var-calendar-minus); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-times-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-times-o:before { content: fa-content($fa-var-calendar-times); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-check-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-calendar-check-o:before { content: fa-content($fa-var-calendar-check); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-map-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-map-o:before { content: fa-content($fa-var-map); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-commenting:before { content: fa-content($fa-var-comment-dots); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-commenting-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-commenting-o:before { content: fa-content($fa-var-comment-dots); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-houzz { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-vimeo:before { content: fa-content($fa-var-vimeo-v); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-black-tie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-fonticons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-reddit-alien { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-edge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-credit-card-alt:before { content: fa-content($fa-var-credit-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-codiepie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-modx { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-fort-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-usb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-product-hunt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-mixcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-scribd { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pause-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-pause-circle-o:before { content: fa-content($fa-var-pause-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-stop-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-stop-circle-o:before { content: fa-content($fa-var-stop-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bluetooth { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bluetooth-b { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-gitlab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wpbeginner { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wpforms { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-envira { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wheelchair-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-wheelchair-alt:before { content: fa-content($fa-var-accessible-icon); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-question-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-question-circle-o:before { content: fa-content($fa-var-question-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-volume-control-phone:before { content: fa-content($fa-var-phone-volume); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-asl-interpreting:before { content: fa-content($fa-var-american-sign-language-interpreting); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-deafness:before { content: fa-content($fa-var-deaf); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-hard-of-hearing:before { content: fa-content($fa-var-deaf); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-glide { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-glide-g { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-signing:before { content: fa-content($fa-var-sign-language); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-viadeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-viadeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-ghost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-snapchat-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-pied-piper { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-first-order { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-yoast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-themeisle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-official:before { content: fa-content($fa-var-google-plus); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-google-plus-circle:before { content: fa-content($fa-var-google-plus); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-font-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-fa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-fa:before { content: fa-content($fa-var-font-awesome); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-handshake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-handshake-o:before { content: fa-content($fa-var-handshake); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-envelope-open-o:before { content: fa-content($fa-var-envelope-open); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-linode { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-address-book-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-address-book-o:before { content: fa-content($fa-var-address-book); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vcard:before { content: fa-content($fa-var-address-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-address-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-address-card-o:before { content: fa-content($fa-var-address-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-vcard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-vcard-o:before { content: fa-content($fa-var-address-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-user-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-user-circle-o:before { content: fa-content($fa-var-user-circle); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-user-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-user-o:before { content: fa-content($fa-var-user); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-id-badge { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license:before { content: fa-content($fa-var-id-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-id-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-id-card-o:before { content: fa-content($fa-var-id-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-drivers-license-o:before { content: fa-content($fa-var-id-card); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-quora { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-free-code-camp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-telegram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-4:before { content: fa-content($fa-var-thermometer-full); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer:before { content: fa-content($fa-var-thermometer-full); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-3:before { content: fa-content($fa-var-thermometer-three-quarters); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-2:before { content: fa-content($fa-var-thermometer-half); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-1:before { content: fa-content($fa-var-thermometer-quarter); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-thermometer-0:before { content: fa-content($fa-var-thermometer-empty); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bathtub:before { content: fa-content($fa-var-bath); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-s15:before { content: fa-content($fa-var-bath); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-window-maximize { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-window-restore { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle:before { content: fa-content($fa-var-window-close); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-window-close-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-window-close-o:before { content: fa-content($fa-var-window-close); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-times-rectangle-o:before { content: fa-content($fa-var-window-close); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-bandcamp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-grav { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-etsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-imdb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-ravelry { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-eercast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-eercast:before { content: fa-content($fa-var-sellcast); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-snowflake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.#{$fa-css-prefix}.#{$fa-css-prefix}-snowflake-o:before { content: fa-content($fa-var-snowflake); } + +.#{$fa-css-prefix}.#{$fa-css-prefix}-superpowers { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-wpexplorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.#{$fa-css-prefix}.#{$fa-css-prefix}-cab:before { content: fa-content($fa-var-taxi); } + diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_stacked.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_stacked.scss new file mode 100644 index 0000000..ae7ef4e --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_stacked.scss @@ -0,0 +1,31 @@ +// Stacked Icons +// ------------------------- + +.#{$fa-css-prefix}-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: ($fa-fw-width*2); +} + +.#{$fa-css-prefix}-stack-1x, +.#{$fa-css-prefix}-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; +} + +.#{$fa-css-prefix}-stack-1x { + line-height: inherit; +} + +.#{$fa-css-prefix}-stack-2x { + font-size: 2em; +} + +.#{$fa-css-prefix}-inverse { + color: $fa-inverse; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_variables.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_variables.scss new file mode 100644 index 0000000..fb203b9 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/_variables.scss @@ -0,0 +1,1469 @@ +// Variables +// -------------------------- +@use 'sass:math'; + +$fa-font-path: '../../../fonts/fontawesome' !default; +$fa-font-size-base: 16px !default; +$fa-font-display: block !default; +$fa-css-prefix: fa !default; +$fa-version: '5.13.1' !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: 2em !default; +$fa-fw-width: (math.div(20em, 16)); +$fa-primary-opacity: 1 !default; +$fa-secondary-opacity: 0.4 !default; + +// Convenience function used to set content property +@function fa-content($fa-var) { + @return unquote('"#{ $fa-var }"'); +} + +$fa-var-500px: \f26e; +$fa-var-accessible-icon: \f368; +$fa-var-accusoft: \f369; +$fa-var-acquisitions-incorporated: \f6af; +$fa-var-ad: \f641; +$fa-var-address-book: \f2b9; +$fa-var-address-card: \f2bb; +$fa-var-adjust: \f042; +$fa-var-adn: \f170; +$fa-var-adobe: \f778; +$fa-var-adversal: \f36a; +$fa-var-affiliatetheme: \f36b; +$fa-var-air-freshener: \f5d0; +$fa-var-airbnb: \f834; +$fa-var-algolia: \f36c; +$fa-var-align-center: \f037; +$fa-var-align-justify: \f039; +$fa-var-align-left: \f036; +$fa-var-align-right: \f038; +$fa-var-alipay: \f642; +$fa-var-allergies: \f461; +$fa-var-amazon: \f270; +$fa-var-amazon-pay: \f42c; +$fa-var-ambulance: \f0f9; +$fa-var-american-sign-language-interpreting: \f2a3; +$fa-var-amilia: \f36d; +$fa-var-anchor: \f13d; +$fa-var-android: \f17b; +$fa-var-angellist: \f209; +$fa-var-angle-double-down: \f103; +$fa-var-angle-double-left: \f100; +$fa-var-angle-double-right: \f101; +$fa-var-angle-double-up: \f102; +$fa-var-angle-down: \f107; +$fa-var-angle-left: \f104; +$fa-var-angle-right: \f105; +$fa-var-angle-up: \f106; +$fa-var-angry: \f556; +$fa-var-angrycreative: \f36e; +$fa-var-angular: \f420; +$fa-var-ankh: \f644; +$fa-var-app-store: \f36f; +$fa-var-app-store-ios: \f370; +$fa-var-apper: \f371; +$fa-var-apple: \f179; +$fa-var-apple-alt: \f5d1; +$fa-var-apple-pay: \f415; +$fa-var-archive: \f187; +$fa-var-archway: \f557; +$fa-var-arrow-alt-circle-down: \f358; +$fa-var-arrow-alt-circle-left: \f359; +$fa-var-arrow-alt-circle-right: \f35a; +$fa-var-arrow-alt-circle-up: \f35b; +$fa-var-arrow-circle-down: \f0ab; +$fa-var-arrow-circle-left: \f0a8; +$fa-var-arrow-circle-right: \f0a9; +$fa-var-arrow-circle-up: \f0aa; +$fa-var-arrow-down: \f063; +$fa-var-arrow-left: \f060; +$fa-var-arrow-right: \f061; +$fa-var-arrow-up: \f062; +$fa-var-arrows-alt: \f0b2; +$fa-var-arrows-alt-h: \f337; +$fa-var-arrows-alt-v: \f338; +$fa-var-artstation: \f77a; +$fa-var-assistive-listening-systems: \f2a2; +$fa-var-asterisk: \f069; +$fa-var-asymmetrik: \f372; +$fa-var-at: \f1fa; +$fa-var-atlas: \f558; +$fa-var-atlassian: \f77b; +$fa-var-atom: \f5d2; +$fa-var-audible: \f373; +$fa-var-audio-description: \f29e; +$fa-var-autoprefixer: \f41c; +$fa-var-avianex: \f374; +$fa-var-aviato: \f421; +$fa-var-award: \f559; +$fa-var-aws: \f375; +$fa-var-baby: \f77c; +$fa-var-baby-carriage: \f77d; +$fa-var-backspace: \f55a; +$fa-var-backward: \f04a; +$fa-var-bacon: \f7e5; +$fa-var-bacteria: \f959; +$fa-var-bacterium: \f95a; +$fa-var-bahai: \f666; +$fa-var-balance-scale: \f24e; +$fa-var-balance-scale-left: \f515; +$fa-var-balance-scale-right: \f516; +$fa-var-ban: \f05e; +$fa-var-band-aid: \f462; +$fa-var-bandcamp: \f2d5; +$fa-var-barcode: \f02a; +$fa-var-bars: \f0c9; +$fa-var-baseball-ball: \f433; +$fa-var-basketball-ball: \f434; +$fa-var-bath: \f2cd; +$fa-var-battery-empty: \f244; +$fa-var-battery-full: \f240; +$fa-var-battery-half: \f242; +$fa-var-battery-quarter: \f243; +$fa-var-battery-three-quarters: \f241; +$fa-var-battle-net: \f835; +$fa-var-bed: \f236; +$fa-var-beer: \f0fc; +$fa-var-behance: \f1b4; +$fa-var-behance-square: \f1b5; +$fa-var-bell: \f0f3; +$fa-var-bell-slash: \f1f6; +$fa-var-bezier-curve: \f55b; +$fa-var-bible: \f647; +$fa-var-bicycle: \f206; +$fa-var-biking: \f84a; +$fa-var-bimobject: \f378; +$fa-var-binoculars: \f1e5; +$fa-var-biohazard: \f780; +$fa-var-birthday-cake: \f1fd; +$fa-var-bitbucket: \f171; +$fa-var-bitcoin: \f379; +$fa-var-bity: \f37a; +$fa-var-black-tie: \f27e; +$fa-var-blackberry: \f37b; +$fa-var-blender: \f517; +$fa-var-blender-phone: \f6b6; +$fa-var-blind: \f29d; +$fa-var-blog: \f781; +$fa-var-blogger: \f37c; +$fa-var-blogger-b: \f37d; +$fa-var-bluetooth: \f293; +$fa-var-bluetooth-b: \f294; +$fa-var-bold: \f032; +$fa-var-bolt: \f0e7; +$fa-var-bomb: \f1e2; +$fa-var-bone: \f5d7; +$fa-var-bong: \f55c; +$fa-var-book: \f02d; +$fa-var-book-dead: \f6b7; +$fa-var-book-medical: \f7e6; +$fa-var-book-open: \f518; +$fa-var-book-reader: \f5da; +$fa-var-bookmark: \f02e; +$fa-var-bootstrap: \f836; +$fa-var-border-all: \f84c; +$fa-var-border-none: \f850; +$fa-var-border-style: \f853; +$fa-var-bowling-ball: \f436; +$fa-var-box: \f466; +$fa-var-box-open: \f49e; +$fa-var-box-tissue: \f95b; +$fa-var-boxes: \f468; +$fa-var-braille: \f2a1; +$fa-var-brain: \f5dc; +$fa-var-bread-slice: \f7ec; +$fa-var-briefcase: \f0b1; +$fa-var-briefcase-medical: \f469; +$fa-var-broadcast-tower: \f519; +$fa-var-broom: \f51a; +$fa-var-brush: \f55d; +$fa-var-btc: \f15a; +$fa-var-buffer: \f837; +$fa-var-bug: \f188; +$fa-var-building: \f1ad; +$fa-var-bullhorn: \f0a1; +$fa-var-bullseye: \f140; +$fa-var-burn: \f46a; +$fa-var-buromobelexperte: \f37f; +$fa-var-bus: \f207; +$fa-var-bus-alt: \f55e; +$fa-var-business-time: \f64a; +$fa-var-buy-n-large: \f8a6; +$fa-var-buysellads: \f20d; +$fa-var-calculator: \f1ec; +$fa-var-calendar: \f133; +$fa-var-calendar-alt: \f073; +$fa-var-calendar-check: \f274; +$fa-var-calendar-day: \f783; +$fa-var-calendar-minus: \f272; +$fa-var-calendar-plus: \f271; +$fa-var-calendar-times: \f273; +$fa-var-calendar-week: \f784; +$fa-var-camera: \f030; +$fa-var-camera-retro: \f083; +$fa-var-campground: \f6bb; +$fa-var-canadian-maple-leaf: \f785; +$fa-var-candy-cane: \f786; +$fa-var-cannabis: \f55f; +$fa-var-capsules: \f46b; +$fa-var-car: \f1b9; +$fa-var-car-alt: \f5de; +$fa-var-car-battery: \f5df; +$fa-var-car-crash: \f5e1; +$fa-var-car-side: \f5e4; +$fa-var-caravan: \f8ff; +$fa-var-caret-down: \f0d7; +$fa-var-caret-left: \f0d9; +$fa-var-caret-right: \f0da; +$fa-var-caret-square-down: \f150; +$fa-var-caret-square-left: \f191; +$fa-var-caret-square-right: \f152; +$fa-var-caret-square-up: \f151; +$fa-var-caret-up: \f0d8; +$fa-var-carrot: \f787; +$fa-var-cart-arrow-down: \f218; +$fa-var-cart-plus: \f217; +$fa-var-cash-register: \f788; +$fa-var-cat: \f6be; +$fa-var-cc-amazon-pay: \f42d; +$fa-var-cc-amex: \f1f3; +$fa-var-cc-apple-pay: \f416; +$fa-var-cc-diners-club: \f24c; +$fa-var-cc-discover: \f1f2; +$fa-var-cc-jcb: \f24b; +$fa-var-cc-mastercard: \f1f1; +$fa-var-cc-paypal: \f1f4; +$fa-var-cc-stripe: \f1f5; +$fa-var-cc-visa: \f1f0; +$fa-var-centercode: \f380; +$fa-var-centos: \f789; +$fa-var-certificate: \f0a3; +$fa-var-chair: \f6c0; +$fa-var-chalkboard: \f51b; +$fa-var-chalkboard-teacher: \f51c; +$fa-var-charging-station: \f5e7; +$fa-var-chart-area: \f1fe; +$fa-var-chart-bar: \f080; +$fa-var-chart-line: \f201; +$fa-var-chart-pie: \f200; +$fa-var-check: \f00c; +$fa-var-check-circle: \f058; +$fa-var-check-double: \f560; +$fa-var-check-square: \f14a; +$fa-var-cheese: \f7ef; +$fa-var-chess: \f439; +$fa-var-chess-bishop: \f43a; +$fa-var-chess-board: \f43c; +$fa-var-chess-king: \f43f; +$fa-var-chess-knight: \f441; +$fa-var-chess-pawn: \f443; +$fa-var-chess-queen: \f445; +$fa-var-chess-rook: \f447; +$fa-var-chevron-circle-down: \f13a; +$fa-var-chevron-circle-left: \f137; +$fa-var-chevron-circle-right: \f138; +$fa-var-chevron-circle-up: \f139; +$fa-var-chevron-down: \f078; +$fa-var-chevron-left: \f053; +$fa-var-chevron-right: \f054; +$fa-var-chevron-up: \f077; +$fa-var-child: \f1ae; +$fa-var-chrome: \f268; +$fa-var-chromecast: \f838; +$fa-var-church: \f51d; +$fa-var-circle: \f111; +$fa-var-circle-notch: \f1ce; +$fa-var-city: \f64f; +$fa-var-clinic-medical: \f7f2; +$fa-var-clipboard: \f328; +$fa-var-clipboard-check: \f46c; +$fa-var-clipboard-list: \f46d; +$fa-var-clock: \f017; +$fa-var-clone: \f24d; +$fa-var-closed-captioning: \f20a; +$fa-var-cloud: \f0c2; +$fa-var-cloud-download-alt: \f381; +$fa-var-cloud-meatball: \f73b; +$fa-var-cloud-moon: \f6c3; +$fa-var-cloud-moon-rain: \f73c; +$fa-var-cloud-rain: \f73d; +$fa-var-cloud-showers-heavy: \f740; +$fa-var-cloud-sun: \f6c4; +$fa-var-cloud-sun-rain: \f743; +$fa-var-cloud-upload-alt: \f382; +$fa-var-cloudscale: \f383; +$fa-var-cloudsmith: \f384; +$fa-var-cloudversify: \f385; +$fa-var-cocktail: \f561; +$fa-var-code: \f121; +$fa-var-code-branch: \f126; +$fa-var-codepen: \f1cb; +$fa-var-codiepie: \f284; +$fa-var-coffee: \f0f4; +$fa-var-cog: \f013; +$fa-var-cogs: \f085; +$fa-var-coins: \f51e; +$fa-var-columns: \f0db; +$fa-var-comment: \f075; +$fa-var-comment-alt: \f27a; +$fa-var-comment-dollar: \f651; +$fa-var-comment-dots: \f4ad; +$fa-var-comment-medical: \f7f5; +$fa-var-comment-slash: \f4b3; +$fa-var-comments: \f086; +$fa-var-comments-dollar: \f653; +$fa-var-compact-disc: \f51f; +$fa-var-compass: \f14e; +$fa-var-compress: \f066; +$fa-var-compress-alt: \f422; +$fa-var-compress-arrows-alt: \f78c; +$fa-var-concierge-bell: \f562; +$fa-var-confluence: \f78d; +$fa-var-connectdevelop: \f20e; +$fa-var-contao: \f26d; +$fa-var-cookie: \f563; +$fa-var-cookie-bite: \f564; +$fa-var-copy: \f0c5; +$fa-var-copyright: \f1f9; +$fa-var-cotton-bureau: \f89e; +$fa-var-couch: \f4b8; +$fa-var-cpanel: \f388; +$fa-var-creative-commons: \f25e; +$fa-var-creative-commons-by: \f4e7; +$fa-var-creative-commons-nc: \f4e8; +$fa-var-creative-commons-nc-eu: \f4e9; +$fa-var-creative-commons-nc-jp: \f4ea; +$fa-var-creative-commons-nd: \f4eb; +$fa-var-creative-commons-pd: \f4ec; +$fa-var-creative-commons-pd-alt: \f4ed; +$fa-var-creative-commons-remix: \f4ee; +$fa-var-creative-commons-sa: \f4ef; +$fa-var-creative-commons-sampling: \f4f0; +$fa-var-creative-commons-sampling-plus: \f4f1; +$fa-var-creative-commons-share: \f4f2; +$fa-var-creative-commons-zero: \f4f3; +$fa-var-credit-card: \f09d; +$fa-var-critical-role: \f6c9; +$fa-var-crop: \f125; +$fa-var-crop-alt: \f565; +$fa-var-cross: \f654; +$fa-var-crosshairs: \f05b; +$fa-var-crow: \f520; +$fa-var-crown: \f521; +$fa-var-crutch: \f7f7; +$fa-var-css3: \f13c; +$fa-var-css3-alt: \f38b; +$fa-var-cube: \f1b2; +$fa-var-cubes: \f1b3; +$fa-var-cut: \f0c4; +$fa-var-cuttlefish: \f38c; +$fa-var-d-and-d: \f38d; +$fa-var-d-and-d-beyond: \f6ca; +$fa-var-dailymotion: \f952; +$fa-var-dashcube: \f210; +$fa-var-database: \f1c0; +$fa-var-deaf: \f2a4; +$fa-var-deezer: \f977; +$fa-var-delicious: \f1a5; +$fa-var-democrat: \f747; +$fa-var-deploydog: \f38e; +$fa-var-deskpro: \f38f; +$fa-var-desktop: \f108; +$fa-var-dev: \f6cc; +$fa-var-deviantart: \f1bd; +$fa-var-dharmachakra: \f655; +$fa-var-dhl: \f790; +$fa-var-diagnoses: \f470; +$fa-var-diaspora: \f791; +$fa-var-dice: \f522; +$fa-var-dice-d20: \f6cf; +$fa-var-dice-d6: \f6d1; +$fa-var-dice-five: \f523; +$fa-var-dice-four: \f524; +$fa-var-dice-one: \f525; +$fa-var-dice-six: \f526; +$fa-var-dice-three: \f527; +$fa-var-dice-two: \f528; +$fa-var-digg: \f1a6; +$fa-var-digital-ocean: \f391; +$fa-var-digital-tachograph: \f566; +$fa-var-directions: \f5eb; +$fa-var-discord: \f392; +$fa-var-discourse: \f393; +$fa-var-disease: \f7fa; +$fa-var-divide: \f529; +$fa-var-dizzy: \f567; +$fa-var-dna: \f471; +$fa-var-dochub: \f394; +$fa-var-docker: \f395; +$fa-var-dog: \f6d3; +$fa-var-dollar-sign: \f155; +$fa-var-dolly: \f472; +$fa-var-dolly-flatbed: \f474; +$fa-var-donate: \f4b9; +$fa-var-door-closed: \f52a; +$fa-var-door-open: \f52b; +$fa-var-dot-circle: \f192; +$fa-var-dove: \f4ba; +$fa-var-download: \f019; +$fa-var-draft2digital: \f396; +$fa-var-drafting-compass: \f568; +$fa-var-dragon: \f6d5; +$fa-var-draw-polygon: \f5ee; +$fa-var-dribbble: \f17d; +$fa-var-dribbble-square: \f397; +$fa-var-dropbox: \f16b; +$fa-var-drum: \f569; +$fa-var-drum-steelpan: \f56a; +$fa-var-drumstick-bite: \f6d7; +$fa-var-drupal: \f1a9; +$fa-var-dumbbell: \f44b; +$fa-var-dumpster: \f793; +$fa-var-dumpster-fire: \f794; +$fa-var-dungeon: \f6d9; +$fa-var-dyalog: \f399; +$fa-var-earlybirds: \f39a; +$fa-var-ebay: \f4f4; +$fa-var-edge: \f282; +$fa-var-edge-legacy: \f978; +$fa-var-edit: \f044; +$fa-var-egg: \f7fb; +$fa-var-eject: \f052; +$fa-var-elementor: \f430; +$fa-var-ellipsis-h: \f141; +$fa-var-ellipsis-v: \f142; +$fa-var-ello: \f5f1; +$fa-var-ember: \f423; +$fa-var-empire: \f1d1; +$fa-var-envelope: \f0e0; +$fa-var-envelope-open: \f2b6; +$fa-var-envelope-open-text: \f658; +$fa-var-envelope-square: \f199; +$fa-var-envira: \f299; +$fa-var-equals: \f52c; +$fa-var-eraser: \f12d; +$fa-var-erlang: \f39d; +$fa-var-ethereum: \f42e; +$fa-var-ethernet: \f796; +$fa-var-etsy: \f2d7; +$fa-var-euro-sign: \f153; +$fa-var-evernote: \f839; +$fa-var-exchange-alt: \f362; +$fa-var-exclamation: \f12a; +$fa-var-exclamation-circle: \f06a; +$fa-var-exclamation-triangle: \f071; +$fa-var-expand: \f065; +$fa-var-expand-alt: \f424; +$fa-var-expand-arrows-alt: \f31e; +$fa-var-expeditedssl: \f23e; +$fa-var-external-link-alt: \f35d; +$fa-var-external-link-square-alt: \f360; +$fa-var-eye: \f06e; +$fa-var-eye-dropper: \f1fb; +$fa-var-eye-slash: \f070; +$fa-var-facebook: \f09a; +$fa-var-facebook-f: \f39e; +$fa-var-facebook-messenger: \f39f; +$fa-var-facebook-square: \f082; +$fa-var-fan: \f863; +$fa-var-fantasy-flight-games: \f6dc; +$fa-var-fast-backward: \f049; +$fa-var-fast-forward: \f050; +$fa-var-faucet: \f905; +$fa-var-fax: \f1ac; +$fa-var-feather: \f52d; +$fa-var-feather-alt: \f56b; +$fa-var-fedex: \f797; +$fa-var-fedora: \f798; +$fa-var-female: \f182; +$fa-var-fighter-jet: \f0fb; +$fa-var-figma: \f799; +$fa-var-file: \f15b; +$fa-var-file-alt: \f15c; +$fa-var-file-archive: \f1c6; +$fa-var-file-audio: \f1c7; +$fa-var-file-code: \f1c9; +$fa-var-file-contract: \f56c; +$fa-var-file-csv: \f6dd; +$fa-var-file-download: \f56d; +$fa-var-file-excel: \f1c3; +$fa-var-file-export: \f56e; +$fa-var-file-image: \f1c5; +$fa-var-file-import: \f56f; +$fa-var-file-invoice: \f570; +$fa-var-file-invoice-dollar: \f571; +$fa-var-file-medical: \f477; +$fa-var-file-medical-alt: \f478; +$fa-var-file-pdf: \f1c1; +$fa-var-file-powerpoint: \f1c4; +$fa-var-file-prescription: \f572; +$fa-var-file-signature: \f573; +$fa-var-file-upload: \f574; +$fa-var-file-video: \f1c8; +$fa-var-file-word: \f1c2; +$fa-var-fill: \f575; +$fa-var-fill-drip: \f576; +$fa-var-film: \f008; +$fa-var-filter: \f0b0; +$fa-var-fingerprint: \f577; +$fa-var-fire: \f06d; +$fa-var-fire-alt: \f7e4; +$fa-var-fire-extinguisher: \f134; +$fa-var-firefox: \f269; +$fa-var-firefox-browser: \f907; +$fa-var-first-aid: \f479; +$fa-var-first-order: \f2b0; +$fa-var-first-order-alt: \f50a; +$fa-var-firstdraft: \f3a1; +$fa-var-fish: \f578; +$fa-var-fist-raised: \f6de; +$fa-var-flag: \f024; +$fa-var-flag-checkered: \f11e; +$fa-var-flag-usa: \f74d; +$fa-var-flask: \f0c3; +$fa-var-flickr: \f16e; +$fa-var-flipboard: \f44d; +$fa-var-flushed: \f579; +$fa-var-fly: \f417; +$fa-var-folder: \f07b; +$fa-var-folder-minus: \f65d; +$fa-var-folder-open: \f07c; +$fa-var-folder-plus: \f65e; +$fa-var-font: \f031; +$fa-var-font-awesome: \f2b4; +$fa-var-font-awesome-alt: \f35c; +$fa-var-font-awesome-flag: \f425; +$fa-var-font-awesome-logo-full: \f4e6; +$fa-var-fonticons: \f280; +$fa-var-fonticons-fi: \f3a2; +$fa-var-football-ball: \f44e; +$fa-var-fort-awesome: \f286; +$fa-var-fort-awesome-alt: \f3a3; +$fa-var-forumbee: \f211; +$fa-var-forward: \f04e; +$fa-var-foursquare: \f180; +$fa-var-free-code-camp: \f2c5; +$fa-var-freebsd: \f3a4; +$fa-var-frog: \f52e; +$fa-var-frown: \f119; +$fa-var-frown-open: \f57a; +$fa-var-fulcrum: \f50b; +$fa-var-funnel-dollar: \f662; +$fa-var-futbol: \f1e3; +$fa-var-galactic-republic: \f50c; +$fa-var-galactic-senate: \f50d; +$fa-var-gamepad: \f11b; +$fa-var-gas-pump: \f52f; +$fa-var-gavel: \f0e3; +$fa-var-gem: \f3a5; +$fa-var-genderless: \f22d; +$fa-var-get-pocket: \f265; +$fa-var-gg: \f260; +$fa-var-gg-circle: \f261; +$fa-var-ghost: \f6e2; +$fa-var-gift: \f06b; +$fa-var-gifts: \f79c; +$fa-var-git: \f1d3; +$fa-var-git-alt: \f841; +$fa-var-git-square: \f1d2; +$fa-var-github: \f09b; +$fa-var-github-alt: \f113; +$fa-var-github-square: \f092; +$fa-var-gitkraken: \f3a6; +$fa-var-gitlab: \f296; +$fa-var-gitter: \f426; +$fa-var-glass-cheers: \f79f; +$fa-var-glass-martini: \f000; +$fa-var-glass-martini-alt: \f57b; +$fa-var-glass-whiskey: \f7a0; +$fa-var-glasses: \f530; +$fa-var-glide: \f2a5; +$fa-var-glide-g: \f2a6; +$fa-var-globe: \f0ac; +$fa-var-globe-africa: \f57c; +$fa-var-globe-americas: \f57d; +$fa-var-globe-asia: \f57e; +$fa-var-globe-europe: \f7a2; +$fa-var-gofore: \f3a7; +$fa-var-golf-ball: \f450; +$fa-var-goodreads: \f3a8; +$fa-var-goodreads-g: \f3a9; +$fa-var-google: \f1a0; +$fa-var-google-drive: \f3aa; +$fa-var-google-pay: \f979; +$fa-var-google-play: \f3ab; +$fa-var-google-plus: \f2b3; +$fa-var-google-plus-g: \f0d5; +$fa-var-google-plus-square: \f0d4; +$fa-var-google-wallet: \f1ee; +$fa-var-gopuram: \f664; +$fa-var-graduation-cap: \f19d; +$fa-var-gratipay: \f184; +$fa-var-grav: \f2d6; +$fa-var-greater-than: \f531; +$fa-var-greater-than-equal: \f532; +$fa-var-grimace: \f57f; +$fa-var-grin: \f580; +$fa-var-grin-alt: \f581; +$fa-var-grin-beam: \f582; +$fa-var-grin-beam-sweat: \f583; +$fa-var-grin-hearts: \f584; +$fa-var-grin-squint: \f585; +$fa-var-grin-squint-tears: \f586; +$fa-var-grin-stars: \f587; +$fa-var-grin-tears: \f588; +$fa-var-grin-tongue: \f589; +$fa-var-grin-tongue-squint: \f58a; +$fa-var-grin-tongue-wink: \f58b; +$fa-var-grin-wink: \f58c; +$fa-var-grip-horizontal: \f58d; +$fa-var-grip-lines: \f7a4; +$fa-var-grip-lines-vertical: \f7a5; +$fa-var-grip-vertical: \f58e; +$fa-var-gripfire: \f3ac; +$fa-var-grunt: \f3ad; +$fa-var-guitar: \f7a6; +$fa-var-gulp: \f3ae; +$fa-var-h-square: \f0fd; +$fa-var-hacker-news: \f1d4; +$fa-var-hacker-news-square: \f3af; +$fa-var-hackerrank: \f5f7; +$fa-var-hamburger: \f805; +$fa-var-hammer: \f6e3; +$fa-var-hamsa: \f665; +$fa-var-hand-holding: \f4bd; +$fa-var-hand-holding-heart: \f4be; +$fa-var-hand-holding-medical: \f95c; +$fa-var-hand-holding-usd: \f4c0; +$fa-var-hand-holding-water: \f4c1; +$fa-var-hand-lizard: \f258; +$fa-var-hand-middle-finger: \f806; +$fa-var-hand-paper: \f256; +$fa-var-hand-peace: \f25b; +$fa-var-hand-point-down: \f0a7; +$fa-var-hand-point-left: \f0a5; +$fa-var-hand-point-right: \f0a4; +$fa-var-hand-point-up: \f0a6; +$fa-var-hand-pointer: \f25a; +$fa-var-hand-rock: \f255; +$fa-var-hand-scissors: \f257; +$fa-var-hand-sparkles: \f95d; +$fa-var-hand-spock: \f259; +$fa-var-hands: \f4c2; +$fa-var-hands-helping: \f4c4; +$fa-var-hands-wash: \f95e; +$fa-var-handshake: \f2b5; +$fa-var-handshake-alt-slash: \f95f; +$fa-var-handshake-slash: \f960; +$fa-var-hanukiah: \f6e6; +$fa-var-hard-hat: \f807; +$fa-var-hashtag: \f292; +$fa-var-hat-cowboy: \f8c0; +$fa-var-hat-cowboy-side: \f8c1; +$fa-var-hat-wizard: \f6e8; +$fa-var-hdd: \f0a0; +$fa-var-head-side-cough: \f961; +$fa-var-head-side-cough-slash: \f962; +$fa-var-head-side-mask: \f963; +$fa-var-head-side-virus: \f964; +$fa-var-heading: \f1dc; +$fa-var-headphones: \f025; +$fa-var-headphones-alt: \f58f; +$fa-var-headset: \f590; +$fa-var-heart: \f004; +$fa-var-heart-broken: \f7a9; +$fa-var-heartbeat: \f21e; +$fa-var-helicopter: \f533; +$fa-var-highlighter: \f591; +$fa-var-hiking: \f6ec; +$fa-var-hippo: \f6ed; +$fa-var-hips: \f452; +$fa-var-hire-a-helper: \f3b0; +$fa-var-history: \f1da; +$fa-var-hockey-puck: \f453; +$fa-var-holly-berry: \f7aa; +$fa-var-home: \f015; +$fa-var-hooli: \f427; +$fa-var-hornbill: \f592; +$fa-var-horse: \f6f0; +$fa-var-horse-head: \f7ab; +$fa-var-hospital: \f0f8; +$fa-var-hospital-alt: \f47d; +$fa-var-hospital-symbol: \f47e; +$fa-var-hospital-user: \f80d; +$fa-var-hot-tub: \f593; +$fa-var-hotdog: \f80f; +$fa-var-hotel: \f594; +$fa-var-hotjar: \f3b1; +$fa-var-hourglass: \f254; +$fa-var-hourglass-end: \f253; +$fa-var-hourglass-half: \f252; +$fa-var-hourglass-start: \f251; +$fa-var-house-damage: \f6f1; +$fa-var-house-user: \f965; +$fa-var-houzz: \f27c; +$fa-var-hryvnia: \f6f2; +$fa-var-html5: \f13b; +$fa-var-hubspot: \f3b2; +$fa-var-i-cursor: \f246; +$fa-var-ice-cream: \f810; +$fa-var-icicles: \f7ad; +$fa-var-icons: \f86d; +$fa-var-id-badge: \f2c1; +$fa-var-id-card: \f2c2; +$fa-var-id-card-alt: \f47f; +$fa-var-ideal: \f913; +$fa-var-igloo: \f7ae; +$fa-var-image: \f03e; +$fa-var-images: \f302; +$fa-var-imdb: \f2d8; +$fa-var-inbox: \f01c; +$fa-var-indent: \f03c; +$fa-var-industry: \f275; +$fa-var-infinity: \f534; +$fa-var-info: \f129; +$fa-var-info-circle: \f05a; +$fa-var-instagram: \f16d; +$fa-var-instagram-square: \f955; +$fa-var-intercom: \f7af; +$fa-var-internet-explorer: \f26b; +$fa-var-invision: \f7b0; +$fa-var-ioxhost: \f208; +$fa-var-italic: \f033; +$fa-var-itch-io: \f83a; +$fa-var-itunes: \f3b4; +$fa-var-itunes-note: \f3b5; +$fa-var-java: \f4e4; +$fa-var-jedi: \f669; +$fa-var-jedi-order: \f50e; +$fa-var-jenkins: \f3b6; +$fa-var-jira: \f7b1; +$fa-var-joget: \f3b7; +$fa-var-joint: \f595; +$fa-var-joomla: \f1aa; +$fa-var-journal-whills: \f66a; +$fa-var-js: \f3b8; +$fa-var-js-square: \f3b9; +$fa-var-jsfiddle: \f1cc; +$fa-var-kaaba: \f66b; +$fa-var-kaggle: \f5fa; +$fa-var-key: \f084; +$fa-var-keybase: \f4f5; +$fa-var-keyboard: \f11c; +$fa-var-keycdn: \f3ba; +$fa-var-khanda: \f66d; +$fa-var-kickstarter: \f3bb; +$fa-var-kickstarter-k: \f3bc; +$fa-var-kiss: \f596; +$fa-var-kiss-beam: \f597; +$fa-var-kiss-wink-heart: \f598; +$fa-var-kiwi-bird: \f535; +$fa-var-korvue: \f42f; +$fa-var-landmark: \f66f; +$fa-var-language: \f1ab; +$fa-var-laptop: \f109; +$fa-var-laptop-code: \f5fc; +$fa-var-laptop-house: \f966; +$fa-var-laptop-medical: \f812; +$fa-var-laravel: \f3bd; +$fa-var-lastfm: \f202; +$fa-var-lastfm-square: \f203; +$fa-var-laugh: \f599; +$fa-var-laugh-beam: \f59a; +$fa-var-laugh-squint: \f59b; +$fa-var-laugh-wink: \f59c; +$fa-var-layer-group: \f5fd; +$fa-var-leaf: \f06c; +$fa-var-leanpub: \f212; +$fa-var-lemon: \f094; +$fa-var-less: \f41d; +$fa-var-less-than: \f536; +$fa-var-less-than-equal: \f537; +$fa-var-level-down-alt: \f3be; +$fa-var-level-up-alt: \f3bf; +$fa-var-life-ring: \f1cd; +$fa-var-lightbulb: \f0eb; +$fa-var-line: \f3c0; +$fa-var-link: \f0c1; +$fa-var-linkedin: \f08c; +$fa-var-linkedin-in: \f0e1; +$fa-var-linode: \f2b8; +$fa-var-linux: \f17c; +$fa-var-lira-sign: \f195; +$fa-var-list: \f03a; +$fa-var-list-alt: \f022; +$fa-var-list-ol: \f0cb; +$fa-var-list-ul: \f0ca; +$fa-var-location-arrow: \f124; +$fa-var-lock: \f023; +$fa-var-lock-open: \f3c1; +$fa-var-long-arrow-alt-down: \f309; +$fa-var-long-arrow-alt-left: \f30a; +$fa-var-long-arrow-alt-right: \f30b; +$fa-var-long-arrow-alt-up: \f30c; +$fa-var-low-vision: \f2a8; +$fa-var-luggage-cart: \f59d; +$fa-var-lungs: \f604; +$fa-var-lungs-virus: \f967; +$fa-var-lyft: \f3c3; +$fa-var-magento: \f3c4; +$fa-var-magic: \f0d0; +$fa-var-magnet: \f076; +$fa-var-mail-bulk: \f674; +$fa-var-mailchimp: \f59e; +$fa-var-male: \f183; +$fa-var-mandalorian: \f50f; +$fa-var-map: \f279; +$fa-var-map-marked: \f59f; +$fa-var-map-marked-alt: \f5a0; +$fa-var-map-marker: \f041; +$fa-var-map-marker-alt: \f3c5; +$fa-var-map-pin: \f276; +$fa-var-map-signs: \f277; +$fa-var-markdown: \f60f; +$fa-var-marker: \f5a1; +$fa-var-mars: \f222; +$fa-var-mars-double: \f227; +$fa-var-mars-stroke: \f229; +$fa-var-mars-stroke-h: \f22b; +$fa-var-mars-stroke-v: \f22a; +$fa-var-mask: \f6fa; +$fa-var-mastodon: \f4f6; +$fa-var-maxcdn: \f136; +$fa-var-mdb: \f8ca; +$fa-var-medal: \f5a2; +$fa-var-medapps: \f3c6; +$fa-var-medium: \f23a; +$fa-var-medium-m: \f3c7; +$fa-var-medkit: \f0fa; +$fa-var-medrt: \f3c8; +$fa-var-meetup: \f2e0; +$fa-var-megaport: \f5a3; +$fa-var-meh: \f11a; +$fa-var-meh-blank: \f5a4; +$fa-var-meh-rolling-eyes: \f5a5; +$fa-var-memory: \f538; +$fa-var-mendeley: \f7b3; +$fa-var-menorah: \f676; +$fa-var-mercury: \f223; +$fa-var-meteor: \f753; +$fa-var-microblog: \f91a; +$fa-var-microchip: \f2db; +$fa-var-microphone: \f130; +$fa-var-microphone-alt: \f3c9; +$fa-var-microphone-alt-slash: \f539; +$fa-var-microphone-slash: \f131; +$fa-var-microscope: \f610; +$fa-var-microsoft: \f3ca; +$fa-var-minus: \f068; +$fa-var-minus-circle: \f056; +$fa-var-minus-square: \f146; +$fa-var-mitten: \f7b5; +$fa-var-mix: \f3cb; +$fa-var-mixcloud: \f289; +$fa-var-mixer: \f956; +$fa-var-mizuni: \f3cc; +$fa-var-mobile: \f10b; +$fa-var-mobile-alt: \f3cd; +$fa-var-modx: \f285; +$fa-var-monero: \f3d0; +$fa-var-money-bill: \f0d6; +$fa-var-money-bill-alt: \f3d1; +$fa-var-money-bill-wave: \f53a; +$fa-var-money-bill-wave-alt: \f53b; +$fa-var-money-check: \f53c; +$fa-var-money-check-alt: \f53d; +$fa-var-monument: \f5a6; +$fa-var-moon: \f186; +$fa-var-mortar-pestle: \f5a7; +$fa-var-mosque: \f678; +$fa-var-motorcycle: \f21c; +$fa-var-mountain: \f6fc; +$fa-var-mouse: \f8cc; +$fa-var-mouse-pointer: \f245; +$fa-var-mug-hot: \f7b6; +$fa-var-music: \f001; +$fa-var-napster: \f3d2; +$fa-var-neos: \f612; +$fa-var-network-wired: \f6ff; +$fa-var-neuter: \f22c; +$fa-var-newspaper: \f1ea; +$fa-var-nimblr: \f5a8; +$fa-var-node: \f419; +$fa-var-node-js: \f3d3; +$fa-var-not-equal: \f53e; +$fa-var-notes-medical: \f481; +$fa-var-npm: \f3d4; +$fa-var-ns8: \f3d5; +$fa-var-nutritionix: \f3d6; +$fa-var-object-group: \f247; +$fa-var-object-ungroup: \f248; +$fa-var-odnoklassniki: \f263; +$fa-var-odnoklassniki-square: \f264; +$fa-var-oil-can: \f613; +$fa-var-old-republic: \f510; +$fa-var-om: \f679; +$fa-var-opencart: \f23d; +$fa-var-openid: \f19b; +$fa-var-opera: \f26a; +$fa-var-optin-monster: \f23c; +$fa-var-orcid: \f8d2; +$fa-var-osi: \f41a; +$fa-var-otter: \f700; +$fa-var-outdent: \f03b; +$fa-var-page4: \f3d7; +$fa-var-pagelines: \f18c; +$fa-var-pager: \f815; +$fa-var-paint-brush: \f1fc; +$fa-var-paint-roller: \f5aa; +$fa-var-palette: \f53f; +$fa-var-palfed: \f3d8; +$fa-var-pallet: \f482; +$fa-var-paper-plane: \f1d8; +$fa-var-paperclip: \f0c6; +$fa-var-parachute-box: \f4cd; +$fa-var-paragraph: \f1dd; +$fa-var-parking: \f540; +$fa-var-passport: \f5ab; +$fa-var-pastafarianism: \f67b; +$fa-var-paste: \f0ea; +$fa-var-patreon: \f3d9; +$fa-var-pause: \f04c; +$fa-var-pause-circle: \f28b; +$fa-var-paw: \f1b0; +$fa-var-paypal: \f1ed; +$fa-var-peace: \f67c; +$fa-var-pen: \f304; +$fa-var-pen-alt: \f305; +$fa-var-pen-fancy: \f5ac; +$fa-var-pen-nib: \f5ad; +$fa-var-pen-square: \f14b; +$fa-var-pencil-alt: \f303; +$fa-var-pencil-ruler: \f5ae; +$fa-var-penny-arcade: \f704; +$fa-var-people-arrows: \f968; +$fa-var-people-carry: \f4ce; +$fa-var-pepper-hot: \f816; +$fa-var-percent: \f295; +$fa-var-percentage: \f541; +$fa-var-periscope: \f3da; +$fa-var-person-booth: \f756; +$fa-var-phabricator: \f3db; +$fa-var-phoenix-framework: \f3dc; +$fa-var-phoenix-squadron: \f511; +$fa-var-phone: \f095; +$fa-var-phone-alt: \f879; +$fa-var-phone-slash: \f3dd; +$fa-var-phone-square: \f098; +$fa-var-phone-square-alt: \f87b; +$fa-var-phone-volume: \f2a0; +$fa-var-photo-video: \f87c; +$fa-var-php: \f457; +$fa-var-pied-piper: \f2ae; +$fa-var-pied-piper-alt: \f1a8; +$fa-var-pied-piper-hat: \f4e5; +$fa-var-pied-piper-pp: \f1a7; +$fa-var-pied-piper-square: \f91e; +$fa-var-piggy-bank: \f4d3; +$fa-var-pills: \f484; +$fa-var-pinterest: \f0d2; +$fa-var-pinterest-p: \f231; +$fa-var-pinterest-square: \f0d3; +$fa-var-pizza-slice: \f818; +$fa-var-place-of-worship: \f67f; +$fa-var-plane: \f072; +$fa-var-plane-arrival: \f5af; +$fa-var-plane-departure: \f5b0; +$fa-var-plane-slash: \f969; +$fa-var-play: \f04b; +$fa-var-play-circle: \f144; +$fa-var-playstation: \f3df; +$fa-var-plug: \f1e6; +$fa-var-plus: \f067; +$fa-var-plus-circle: \f055; +$fa-var-plus-square: \f0fe; +$fa-var-podcast: \f2ce; +$fa-var-poll: \f681; +$fa-var-poll-h: \f682; +$fa-var-poo: \f2fe; +$fa-var-poo-storm: \f75a; +$fa-var-poop: \f619; +$fa-var-portrait: \f3e0; +$fa-var-pound-sign: \f154; +$fa-var-power-off: \f011; +$fa-var-pray: \f683; +$fa-var-praying-hands: \f684; +$fa-var-prescription: \f5b1; +$fa-var-prescription-bottle: \f485; +$fa-var-prescription-bottle-alt: \f486; +$fa-var-print: \f02f; +$fa-var-procedures: \f487; +$fa-var-product-hunt: \f288; +$fa-var-project-diagram: \f542; +$fa-var-pump-medical: \f96a; +$fa-var-pump-soap: \f96b; +$fa-var-pushed: \f3e1; +$fa-var-puzzle-piece: \f12e; +$fa-var-python: \f3e2; +$fa-var-qq: \f1d6; +$fa-var-qrcode: \f029; +$fa-var-question: \f128; +$fa-var-question-circle: \f059; +$fa-var-quidditch: \f458; +$fa-var-quinscape: \f459; +$fa-var-quora: \f2c4; +$fa-var-quote-left: \f10d; +$fa-var-quote-right: \f10e; +$fa-var-quran: \f687; +$fa-var-r-project: \f4f7; +$fa-var-radiation: \f7b9; +$fa-var-radiation-alt: \f7ba; +$fa-var-rainbow: \f75b; +$fa-var-random: \f074; +$fa-var-raspberry-pi: \f7bb; +$fa-var-ravelry: \f2d9; +$fa-var-react: \f41b; +$fa-var-reacteurope: \f75d; +$fa-var-readme: \f4d5; +$fa-var-rebel: \f1d0; +$fa-var-receipt: \f543; +$fa-var-record-vinyl: \f8d9; +$fa-var-recycle: \f1b8; +$fa-var-red-river: \f3e3; +$fa-var-reddit: \f1a1; +$fa-var-reddit-alien: \f281; +$fa-var-reddit-square: \f1a2; +$fa-var-redhat: \f7bc; +$fa-var-redo: \f01e; +$fa-var-redo-alt: \f2f9; +$fa-var-registered: \f25d; +$fa-var-remove-format: \f87d; +$fa-var-renren: \f18b; +$fa-var-reply: \f3e5; +$fa-var-reply-all: \f122; +$fa-var-replyd: \f3e6; +$fa-var-republican: \f75e; +$fa-var-researchgate: \f4f8; +$fa-var-resolving: \f3e7; +$fa-var-restroom: \f7bd; +$fa-var-retweet: \f079; +$fa-var-rev: \f5b2; +$fa-var-ribbon: \f4d6; +$fa-var-ring: \f70b; +$fa-var-road: \f018; +$fa-var-robot: \f544; +$fa-var-rocket: \f135; +$fa-var-rocketchat: \f3e8; +$fa-var-rockrms: \f3e9; +$fa-var-route: \f4d7; +$fa-var-rss: \f09e; +$fa-var-rss-square: \f143; +$fa-var-ruble-sign: \f158; +$fa-var-ruler: \f545; +$fa-var-ruler-combined: \f546; +$fa-var-ruler-horizontal: \f547; +$fa-var-ruler-vertical: \f548; +$fa-var-running: \f70c; +$fa-var-rupee-sign: \f156; +$fa-var-rust: \f97a; +$fa-var-sad-cry: \f5b3; +$fa-var-sad-tear: \f5b4; +$fa-var-safari: \f267; +$fa-var-salesforce: \f83b; +$fa-var-sass: \f41e; +$fa-var-satellite: \f7bf; +$fa-var-satellite-dish: \f7c0; +$fa-var-save: \f0c7; +$fa-var-schlix: \f3ea; +$fa-var-school: \f549; +$fa-var-screwdriver: \f54a; +$fa-var-scribd: \f28a; +$fa-var-scroll: \f70e; +$fa-var-sd-card: \f7c2; +$fa-var-search: \f002; +$fa-var-search-dollar: \f688; +$fa-var-search-location: \f689; +$fa-var-search-minus: \f010; +$fa-var-search-plus: \f00e; +$fa-var-searchengin: \f3eb; +$fa-var-seedling: \f4d8; +$fa-var-sellcast: \f2da; +$fa-var-sellsy: \f213; +$fa-var-server: \f233; +$fa-var-servicestack: \f3ec; +$fa-var-shapes: \f61f; +$fa-var-share: \f064; +$fa-var-share-alt: \f1e0; +$fa-var-share-alt-square: \f1e1; +$fa-var-share-square: \f14d; +$fa-var-shekel-sign: \f20b; +$fa-var-shield-alt: \f3ed; +$fa-var-shield-virus: \f96c; +$fa-var-ship: \f21a; +$fa-var-shipping-fast: \f48b; +$fa-var-shirtsinbulk: \f214; +$fa-var-shoe-prints: \f54b; +$fa-var-shopify: \f957; +$fa-var-shopping-bag: \f290; +$fa-var-shopping-basket: \f291; +$fa-var-shopping-cart: \f07a; +$fa-var-shopware: \f5b5; +$fa-var-shower: \f2cc; +$fa-var-shuttle-van: \f5b6; +$fa-var-sign: \f4d9; +$fa-var-sign-in-alt: \f2f6; +$fa-var-sign-language: \f2a7; +$fa-var-sign-out-alt: \f2f5; +$fa-var-signal: \f012; +$fa-var-signature: \f5b7; +$fa-var-sim-card: \f7c4; +$fa-var-simplybuilt: \f215; +$fa-var-sink: \f96d; +$fa-var-sistrix: \f3ee; +$fa-var-sitemap: \f0e8; +$fa-var-sith: \f512; +$fa-var-skating: \f7c5; +$fa-var-sketch: \f7c6; +$fa-var-skiing: \f7c9; +$fa-var-skiing-nordic: \f7ca; +$fa-var-skull: \f54c; +$fa-var-skull-crossbones: \f714; +$fa-var-skyatlas: \f216; +$fa-var-skype: \f17e; +$fa-var-slack: \f198; +$fa-var-slack-hash: \f3ef; +$fa-var-slash: \f715; +$fa-var-sleigh: \f7cc; +$fa-var-sliders-h: \f1de; +$fa-var-slideshare: \f1e7; +$fa-var-smile: \f118; +$fa-var-smile-beam: \f5b8; +$fa-var-smile-wink: \f4da; +$fa-var-smog: \f75f; +$fa-var-smoking: \f48d; +$fa-var-smoking-ban: \f54d; +$fa-var-sms: \f7cd; +$fa-var-snapchat: \f2ab; +$fa-var-snapchat-ghost: \f2ac; +$fa-var-snapchat-square: \f2ad; +$fa-var-snowboarding: \f7ce; +$fa-var-snowflake: \f2dc; +$fa-var-snowman: \f7d0; +$fa-var-snowplow: \f7d2; +$fa-var-soap: \f96e; +$fa-var-socks: \f696; +$fa-var-solar-panel: \f5ba; +$fa-var-sort: \f0dc; +$fa-var-sort-alpha-down: \f15d; +$fa-var-sort-alpha-down-alt: \f881; +$fa-var-sort-alpha-up: \f15e; +$fa-var-sort-alpha-up-alt: \f882; +$fa-var-sort-amount-down: \f160; +$fa-var-sort-amount-down-alt: \f884; +$fa-var-sort-amount-up: \f161; +$fa-var-sort-amount-up-alt: \f885; +$fa-var-sort-down: \f0dd; +$fa-var-sort-numeric-down: \f162; +$fa-var-sort-numeric-down-alt: \f886; +$fa-var-sort-numeric-up: \f163; +$fa-var-sort-numeric-up-alt: \f887; +$fa-var-sort-up: \f0de; +$fa-var-soundcloud: \f1be; +$fa-var-sourcetree: \f7d3; +$fa-var-spa: \f5bb; +$fa-var-space-shuttle: \f197; +$fa-var-speakap: \f3f3; +$fa-var-speaker-deck: \f83c; +$fa-var-spell-check: \f891; +$fa-var-spider: \f717; +$fa-var-spinner: \f110; +$fa-var-splotch: \f5bc; +$fa-var-spotify: \f1bc; +$fa-var-spray-can: \f5bd; +$fa-var-square: \f0c8; +$fa-var-square-full: \f45c; +$fa-var-square-root-alt: \f698; +$fa-var-squarespace: \f5be; +$fa-var-stack-exchange: \f18d; +$fa-var-stack-overflow: \f16c; +$fa-var-stackpath: \f842; +$fa-var-stamp: \f5bf; +$fa-var-star: \f005; +$fa-var-star-and-crescent: \f699; +$fa-var-star-half: \f089; +$fa-var-star-half-alt: \f5c0; +$fa-var-star-of-david: \f69a; +$fa-var-star-of-life: \f621; +$fa-var-staylinked: \f3f5; +$fa-var-steam: \f1b6; +$fa-var-steam-square: \f1b7; +$fa-var-steam-symbol: \f3f6; +$fa-var-step-backward: \f048; +$fa-var-step-forward: \f051; +$fa-var-stethoscope: \f0f1; +$fa-var-sticker-mule: \f3f7; +$fa-var-sticky-note: \f249; +$fa-var-stop: \f04d; +$fa-var-stop-circle: \f28d; +$fa-var-stopwatch: \f2f2; +$fa-var-stopwatch-20: \f96f; +$fa-var-store: \f54e; +$fa-var-store-alt: \f54f; +$fa-var-store-alt-slash: \f970; +$fa-var-store-slash: \f971; +$fa-var-strava: \f428; +$fa-var-stream: \f550; +$fa-var-street-view: \f21d; +$fa-var-strikethrough: \f0cc; +$fa-var-stripe: \f429; +$fa-var-stripe-s: \f42a; +$fa-var-stroopwafel: \f551; +$fa-var-studiovinari: \f3f8; +$fa-var-stumbleupon: \f1a4; +$fa-var-stumbleupon-circle: \f1a3; +$fa-var-subscript: \f12c; +$fa-var-subway: \f239; +$fa-var-suitcase: \f0f2; +$fa-var-suitcase-rolling: \f5c1; +$fa-var-sun: \f185; +$fa-var-superpowers: \f2dd; +$fa-var-superscript: \f12b; +$fa-var-supple: \f3f9; +$fa-var-surprise: \f5c2; +$fa-var-suse: \f7d6; +$fa-var-swatchbook: \f5c3; +$fa-var-swift: \f8e1; +$fa-var-swimmer: \f5c4; +$fa-var-swimming-pool: \f5c5; +$fa-var-symfony: \f83d; +$fa-var-synagogue: \f69b; +$fa-var-sync: \f021; +$fa-var-sync-alt: \f2f1; +$fa-var-syringe: \f48e; +$fa-var-table: \f0ce; +$fa-var-table-tennis: \f45d; +$fa-var-tablet: \f10a; +$fa-var-tablet-alt: \f3fa; +$fa-var-tablets: \f490; +$fa-var-tachometer-alt: \f3fd; +$fa-var-tag: \f02b; +$fa-var-tags: \f02c; +$fa-var-tape: \f4db; +$fa-var-tasks: \f0ae; +$fa-var-taxi: \f1ba; +$fa-var-teamspeak: \f4f9; +$fa-var-teeth: \f62e; +$fa-var-teeth-open: \f62f; +$fa-var-telegram: \f2c6; +$fa-var-telegram-plane: \f3fe; +$fa-var-temperature-high: \f769; +$fa-var-temperature-low: \f76b; +$fa-var-tencent-weibo: \f1d5; +$fa-var-tenge: \f7d7; +$fa-var-terminal: \f120; +$fa-var-text-height: \f034; +$fa-var-text-width: \f035; +$fa-var-th: \f00a; +$fa-var-th-large: \f009; +$fa-var-th-list: \f00b; +$fa-var-the-red-yeti: \f69d; +$fa-var-theater-masks: \f630; +$fa-var-themeco: \f5c6; +$fa-var-themeisle: \f2b2; +$fa-var-thermometer: \f491; +$fa-var-thermometer-empty: \f2cb; +$fa-var-thermometer-full: \f2c7; +$fa-var-thermometer-half: \f2c9; +$fa-var-thermometer-quarter: \f2ca; +$fa-var-thermometer-three-quarters: \f2c8; +$fa-var-think-peaks: \f731; +$fa-var-thumbs-down: \f165; +$fa-var-thumbs-up: \f164; +$fa-var-thumbtack: \f08d; +$fa-var-ticket-alt: \f3ff; +$fa-var-tiktok: \f97b; +$fa-var-times: \f00d; +$fa-var-times-circle: \f057; +$fa-var-tint: \f043; +$fa-var-tint-slash: \f5c7; +$fa-var-tired: \f5c8; +$fa-var-toggle-off: \f204; +$fa-var-toggle-on: \f205; +$fa-var-toilet: \f7d8; +$fa-var-toilet-paper: \f71e; +$fa-var-toilet-paper-slash: \f972; +$fa-var-toolbox: \f552; +$fa-var-tools: \f7d9; +$fa-var-tooth: \f5c9; +$fa-var-torah: \f6a0; +$fa-var-torii-gate: \f6a1; +$fa-var-tractor: \f722; +$fa-var-trade-federation: \f513; +$fa-var-trademark: \f25c; +$fa-var-traffic-light: \f637; +$fa-var-trailer: \f941; +$fa-var-train: \f238; +$fa-var-tram: \f7da; +$fa-var-transgender: \f224; +$fa-var-transgender-alt: \f225; +$fa-var-trash: \f1f8; +$fa-var-trash-alt: \f2ed; +$fa-var-trash-restore: \f829; +$fa-var-trash-restore-alt: \f82a; +$fa-var-tree: \f1bb; +$fa-var-trello: \f181; +$fa-var-tripadvisor: \f262; +$fa-var-trophy: \f091; +$fa-var-truck: \f0d1; +$fa-var-truck-loading: \f4de; +$fa-var-truck-monster: \f63b; +$fa-var-truck-moving: \f4df; +$fa-var-truck-pickup: \f63c; +$fa-var-tshirt: \f553; +$fa-var-tty: \f1e4; +$fa-var-tumblr: \f173; +$fa-var-tumblr-square: \f174; +$fa-var-tv: \f26c; +$fa-var-twitch: \f1e8; +$fa-var-twitter: \f099; +$fa-var-twitter-square: \f081; +$fa-var-typo3: \f42b; +$fa-var-uber: \f402; +$fa-var-ubuntu: \f7df; +$fa-var-uikit: \f403; +$fa-var-umbraco: \f8e8; +$fa-var-umbrella: \f0e9; +$fa-var-umbrella-beach: \f5ca; +$fa-var-underline: \f0cd; +$fa-var-undo: \f0e2; +$fa-var-undo-alt: \f2ea; +$fa-var-uniregistry: \f404; +$fa-var-unity: \f949; +$fa-var-universal-access: \f29a; +$fa-var-university: \f19c; +$fa-var-unlink: \f127; +$fa-var-unlock: \f09c; +$fa-var-unlock-alt: \f13e; +$fa-var-unsplash: \f97c; +$fa-var-untappd: \f405; +$fa-var-upload: \f093; +$fa-var-ups: \f7e0; +$fa-var-usb: \f287; +$fa-var-user: \f007; +$fa-var-user-alt: \f406; +$fa-var-user-alt-slash: \f4fa; +$fa-var-user-astronaut: \f4fb; +$fa-var-user-check: \f4fc; +$fa-var-user-circle: \f2bd; +$fa-var-user-clock: \f4fd; +$fa-var-user-cog: \f4fe; +$fa-var-user-edit: \f4ff; +$fa-var-user-friends: \f500; +$fa-var-user-graduate: \f501; +$fa-var-user-injured: \f728; +$fa-var-user-lock: \f502; +$fa-var-user-md: \f0f0; +$fa-var-user-minus: \f503; +$fa-var-user-ninja: \f504; +$fa-var-user-nurse: \f82f; +$fa-var-user-plus: \f234; +$fa-var-user-secret: \f21b; +$fa-var-user-shield: \f505; +$fa-var-user-slash: \f506; +$fa-var-user-tag: \f507; +$fa-var-user-tie: \f508; +$fa-var-user-times: \f235; +$fa-var-users: \f0c0; +$fa-var-users-cog: \f509; +$fa-var-users-slash: \f973; +$fa-var-usps: \f7e1; +$fa-var-ussunnah: \f407; +$fa-var-utensil-spoon: \f2e5; +$fa-var-utensils: \f2e7; +$fa-var-vaadin: \f408; +$fa-var-vector-square: \f5cb; +$fa-var-venus: \f221; +$fa-var-venus-double: \f226; +$fa-var-venus-mars: \f228; +$fa-var-viacoin: \f237; +$fa-var-viadeo: \f2a9; +$fa-var-viadeo-square: \f2aa; +$fa-var-vial: \f492; +$fa-var-vials: \f493; +$fa-var-viber: \f409; +$fa-var-video: \f03d; +$fa-var-video-slash: \f4e2; +$fa-var-vihara: \f6a7; +$fa-var-vimeo: \f40a; +$fa-var-vimeo-square: \f194; +$fa-var-vimeo-v: \f27d; +$fa-var-vine: \f1ca; +$fa-var-virus: \f974; +$fa-var-virus-slash: \f975; +$fa-var-viruses: \f976; +$fa-var-vk: \f189; +$fa-var-vnv: \f40b; +$fa-var-voicemail: \f897; +$fa-var-volleyball-ball: \f45f; +$fa-var-volume-down: \f027; +$fa-var-volume-mute: \f6a9; +$fa-var-volume-off: \f026; +$fa-var-volume-up: \f028; +$fa-var-vote-yea: \f772; +$fa-var-vr-cardboard: \f729; +$fa-var-vuejs: \f41f; +$fa-var-walking: \f554; +$fa-var-wallet: \f555; +$fa-var-warehouse: \f494; +$fa-var-water: \f773; +$fa-var-wave-square: \f83e; +$fa-var-waze: \f83f; +$fa-var-weebly: \f5cc; +$fa-var-weibo: \f18a; +$fa-var-weight: \f496; +$fa-var-weight-hanging: \f5cd; +$fa-var-weixin: \f1d7; +$fa-var-whatsapp: \f232; +$fa-var-whatsapp-square: \f40c; +$fa-var-wheelchair: \f193; +$fa-var-whmcs: \f40d; +$fa-var-wifi: \f1eb; +$fa-var-wikipedia-w: \f266; +$fa-var-wind: \f72e; +$fa-var-window-close: \f410; +$fa-var-window-maximize: \f2d0; +$fa-var-window-minimize: \f2d1; +$fa-var-window-restore: \f2d2; +$fa-var-windows: \f17a; +$fa-var-wine-bottle: \f72f; +$fa-var-wine-glass: \f4e3; +$fa-var-wine-glass-alt: \f5ce; +$fa-var-wix: \f5cf; +$fa-var-wizards-of-the-coast: \f730; +$fa-var-wolf-pack-battalion: \f514; +$fa-var-won-sign: \f159; +$fa-var-wordpress: \f19a; +$fa-var-wordpress-simple: \f411; +$fa-var-wpbeginner: \f297; +$fa-var-wpexplorer: \f2de; +$fa-var-wpforms: \f298; +$fa-var-wpressr: \f3e4; +$fa-var-wrench: \f0ad; +$fa-var-x-ray: \f497; +$fa-var-xbox: \f412; +$fa-var-xing: \f168; +$fa-var-xing-square: \f169; +$fa-var-y-combinator: \f23b; +$fa-var-yahoo: \f19e; +$fa-var-yammer: \f840; +$fa-var-yandex: \f413; +$fa-var-yandex-international: \f414; +$fa-var-yarn: \f7e3; +$fa-var-yelp: \f1e9; +$fa-var-yen-sign: \f157; +$fa-var-yin-yang: \f6ad; +$fa-var-yoast: \f2b1; +$fa-var-youtube: \f167; +$fa-var-youtube-square: \f431; +$fa-var-zhihu: \f63f; diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/brands.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/brands.scss new file mode 100644 index 0000000..eb153b2 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/brands.scss @@ -0,0 +1,23 @@ +/*! + * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: 400; + font-display: $fa-font-display; + src: url('#{$fa-font-path}/fa-brands-400.eot'); + src: url('#{$fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-brands-400.woff') format('woff'), + url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'), + url('#{$fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); +} + +.fab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/fontawesome.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/fontawesome.scss new file mode 100644 index 0000000..ac6d7e9 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/fontawesome.scss @@ -0,0 +1,16 @@ +/*! + * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; +@import 'mixins'; +@import 'core'; +@import 'larger'; +@import 'fixed-width'; +@import 'list'; +@import 'bordered-pulled'; +@import 'animated'; +@import 'rotated-flipped'; +@import 'stacked'; +@import 'icons'; +@import 'screen-reader'; diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/regular.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/regular.scss new file mode 100644 index 0000000..953e3a7 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/regular.scss @@ -0,0 +1,23 @@ +/*! + * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + font-display: $fa-font-display; + src: url('#{$fa-font-path}/fa-regular-400.eot'); + src: url('#{$fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-regular-400.woff') format('woff'), + url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'), + url('#{$fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); +} + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/solid.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/solid.scss new file mode 100644 index 0000000..6ea911c --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/solid.scss @@ -0,0 +1,24 @@ +/*! + * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + font-display: $fa-font-display; + src: url('#{$fa-font-path}/fa-solid-900.eot'); + src: url('#{$fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'), + url('#{$fa-font-path}/fa-solid-900.woff') format('woff'), + url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'), + url('#{$fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); +} + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} diff --git a/MyOffice.SPA/src/assets/scss/fonts/fontawesome/v4-shims.scss b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/v4-shims.scss new file mode 100644 index 0000000..90f1392 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/fonts/fontawesome/v4-shims.scss @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; +@import 'shims'; diff --git a/MyOffice.SPA/src/assets/scss/pages/_auth.scss b/MyOffice.SPA/src/assets/scss/pages/_auth.scss new file mode 100644 index 0000000..113d220 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_auth.scss @@ -0,0 +1,211 @@ +.auth-container { + height: 100%; + width: 100%; + .auth-main { + height: 100%; + width: 100%; + margin: 0px !important; + min-height: 100vh; + } +} + +.left-img { + height: 100%; + width: 100%; + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + display: flex; + align-items: center; + justify-content: center; + padding: 30px 15px; + position: relative; + z-index: 1; +} + +.left-content { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + max-width: 480px; + width: 100%; + text-align: center; + + h1 { + color: #fff; + } + p { + color: #fff; + } +} +.auth-form-section { + background-color: #fff; +} +.auth-form-btn { + display: flex; + justify-content: center; + width: 100%; + height: 50px !important; + border-radius: 10px; + cursor: pointer; + button { + width: 100%; + } +} +.form-section { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} +.auth-signup-text { + font-size: 18px; + color: #000; + padding-bottom: 30px; + width: 410px; + max-width: 100%; +} +.sign-up-link { + color: #3699ff; + font-size: 20px !important; + margin: 0px 10px; +} +.auth-wrapper { + width: 410px; + max-width: 100%; + margin-top: auto; + margin-bottom: auto; +} +.login-title { + margin-bottom: 20px; +} +.social-login-title { + font-size: 15px; + color: #919aa3; + display: flex; + margin: 20px 0px; + + &::before, + &::after { + content: ""; + background-image: linear-gradient(#bbb8b8, #f3f3f3); + flex-grow: 1; + background-size: calc(100% - 20px) 1px; + background-repeat: no-repeat; + } + + &::before { + background-position: center left; + } + + &::after { + background-position: center right; + } +} +.welcome-msg { + font-weight: 500; +} +.social-icon { + text-align: center; + li a { + color: #3c4858; + border: 1px solid #3c4858; + display: inline-block; + height: 32px; + width: 32px; + line-height: 32px; + text-align: center; + transition: all 0.4s ease; + overflow: hidden; + position: relative; + &:hover { + background-color: #2f55d4; + border-color: #2f55d4 !important; + color: #ffffff !important; + } + } +} +.sm-icon { + height: 16px !important; + width: 16px !important; +} +.show-pwd-icon { + color: rgba(0, 0, 0, 0.55); + padding: 12px; +} +.face-icon { + color: rgba(0, 0, 0, 0.55); +} +.auth-locked { + font-size: 60px; + color: #333; + width: 120px; + height: 120px; + background-color: transparent; + margin: 0 auto; + img { + width: 100px; + border-radius: 50%; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2); + } +} +.auth-locked-title { + font-size: 30px; + font-family: Poppins, sans-serif; + color: #403866; + line-height: 1.2; + text-align: center; + width: 100%; + display: block; +} +.error-header { + font-size: 80px; + line-height: 1.2; + color: #403866; + text-transform: uppercase; + text-align: center; + width: 100%; + display: block; + font-weight: 700; +} +.error-subheader { + font-size: 17px; + color: #403866; + text-transform: uppercase; + text-align: center; + width: 100%; + display: block; + font-weight: 700; +} +.error-subheader2 { + font-size: 12px; + color: #919192; + text-align: center; + width: 100%; + display: block; + font-weight: 500; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +.auth-spinner:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + border-radius: 50%; + border: 2px solid #ffffff; + border-top-color: #000000; + animation: spinner 0.8s linear infinite; +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_dashboard.scss b/MyOffice.SPA/src/assets/scss/pages/_dashboard.scss new file mode 100644 index 0000000..539e19f --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_dashboard.scss @@ -0,0 +1,47 @@ +/* + * Document : _dashboard.scss + * Author : RedStar Template + * Description: This scss file for dashboard page style classes + */ +.dashboard-flot-chart { + height: 275px; +} + +.dashboard-donut-chart { + height: 265px; + text-align: center; +} + +.dashboard-line-chart { + height: 250px; +} + +.dashboard-stat-list { + list-style: none; + padding-left: 0; + margin-top: 40px; + + li { + padding: 16px 0 0 0; + + small { + font-size: 8px; + } + } +} + +.dashboard-task-infos { + .progress { + height: 10px; + position: relative; + top: 6px; + } +} +.totalEarning { + text-align: center; + padding: 6px; + color: #ff9800; +} +.earningProgress .progress { + height: 9px; +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_inbox.scss b/MyOffice.SPA/src/assets/scss/pages/_inbox.scss new file mode 100644 index 0000000..ba18d1a --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_inbox.scss @@ -0,0 +1,121 @@ +/* + * Document : _inbox.scss + * Author : RedStar Template + * Description: This scss file for email page style classes + */ +.p-15 { + padding: 15px; +} +.p-10 { + padding: 10px; +} +.b-b { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); +} +.mail_listing { + .mail-option { + .btn-group { + margin-bottom: 5px; + } + } +} +#mail-nav { + .btn { + min-width: 110px; + } + #mail-folders { + list-style-type: none; + padding-left: 0px; + .badge { + float: right; + } + } + #mail-folders > li { + margin: 2px 0; + a { + &:hover { + color: #fff; + background-color: #337ab7; + } + } + &.active > a { + color: #fff; + background-color: #337ab7; + &:hover { + background-color: #32c0c3; + } + } + } + #mail-labels { + float: left; + width: 100%; + list-style-type: none; + padding-left: 0px; + li { + float: left; + } + .material-icons { + font-size: 16px; + height: 16px; + padding: 2px; + float: left; + } + } + #online-offline { + list-style-type: none; + padding-left: 0px; + .material-icons { + font-size: 8px; + height: 8px; + padding: 0px 5px 2px 0; + } + } + #mail-labels, + #online-offline { + li { + a:hover { + background-color: #e6e6e6; + } + } + } + li { + a { + color: #212529; + padding: 7px 10px; + display: block; + border-radius: 4px; + position: relative; + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; + } + } +} +.composeForm { + padding: 25px; +} +.inbox-body { + padding: 20px; +} +.replyBox { + border: 1px solid rgba(120, 130, 140, 0.13); + padding: 20px; +} +.inbox-center .table thead th { + vertical-align: middle; + padding: 20px; +} +.email-btn-group { + position: relative; + display: -ms-inline-flexbox; + display: -webkit-inline-box; + display: inline-flex; + vertical-align: middle; +} +.max-texts { + padding: 15px !important; + a { + color: #212529; + padding: 10px 0px 10px 0px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_pricing.scss b/MyOffice.SPA/src/assets/scss/pages/_pricing.scss new file mode 100644 index 0000000..74de917 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_pricing.scss @@ -0,0 +1,195 @@ +/* + * Document : _pricing.scss + * Author : RedStar Template + * Description: This scss file for pricing page style classes + */ +//colors +$white: #fff; +$color_orange: #ffa442; +$color_blue: #4b64ff; +$color_red: #ff4b4b; +$color_green: #40c952; + +.pricingTable { + text-align: center; + background: $white; + margin: 0 -10px; + box-shadow: 0 0 10px #ababab; + padding-bottom: 40px; + border-radius: 10px; + color: #cad0de; + transform: scale(1); + transition: all 0.5s ease 0s; + &:hover { + transform: scale(1.05); + z-index: 1; + .pricingTable-header { + background: #ff9624; + i { + color: $white; + } + } + .price-value { + color: $white; + } + .month { + color: $white; + } + } + .pricingTable-header { + padding: 40px 0; + background: #f5f6f9; + border-radius: 10px 10px 50% 50%; + transition: all 0.5s ease 0s; + i { + font-size: 50px; + color: #858c9a; + margin-bottom: 10px; + transition: all 0.5s ease 0s; + } + } + .price-value { + font-size: 35px; + color: #ff9624; + transition: all 0.5s ease 0s; + } + .month { + display: block; + font-size: 14px; + color: #cad0de; + } + .heading { + font-size: 24px; + color: #ff9624; + margin-bottom: 20px; + text-transform: uppercase; + padding-top: 15px; + } + &.blue { + .price-value { + color: $color_blue; + } + .heading { + color: $color_blue; + } + &:hover .pricingTable-header { + background: $color_blue; + } + .pricingTable-signup a { + background: $color_blue; + &:hover { + box-shadow: 0 0 10px $color_blue; + } + } + } + &.red { + .price-value { + color: $color_red; + } + .heading { + color: $color_red; + } + &:hover .pricingTable-header { + background: $color_red; + } + .pricingTable-signup a { + background: $color_red; + &:hover { + box-shadow: 0 0 10px $color_red; + } + } + } + &.greenColor { + .price-value { + color: $color_green; + } + &:hover { + .pricingTable-header { + background: $color_green; + } + .price-value { + color: $white; + } + } + .pricingTable-signup a { + background: $color_green; + &:hover { + box-shadow: 0 0 10px $color_green; + } + } + } + &.blueColor { + &:hover { + .pricingTable-header { + background: $color_blue; + } + .price-value { + color: $white; + } + } + .price-value { + color: $color_blue; + } + .heading { + color: $color_blue; + } + .pricingTable-signup a { + background: $color_blue; + } + } + &.redColor { + &:hover { + .pricingTable-header { + background: $color_red; + } + .price-value { + color: $white; + } + } + .price-value { + color: $color_red; + } + .heading { + color: $color_red; + } + .pricingTable-signup a { + background: $color_red; + } + } + .pricing-content ul { + list-style: none; + padding: 0; + margin-bottom: 30px; + li { + line-height: 30px; + color: #a7a8aa; + } + } + .pricingTable-signup a { + display: inline-block; + font-size: 15px; + color: $white; + padding: 10px 35px; + border-radius: 20px; + background: $color_orange; + text-transform: uppercase; + transition: all 0.3s ease 0s; + &:hover { + box-shadow: 0 0 10px $color_orange; + } + } + &.green .heading { + color: $color_green; + } +} +@media screen and (max-width: 990px) { + .pricingTable { + margin: 0 0 20px 0; + } +} +.greenColor .heading { + font-size: 24px; + color: $color_green; + margin-bottom: 20px; + text-transform: uppercase; +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_profile.scss b/MyOffice.SPA/src/assets/scss/pages/_profile.scss new file mode 100644 index 0000000..a7c61ea --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_profile.scss @@ -0,0 +1,26 @@ +/* + * Document : _profile.scss + * Author : RedStar Template + * Description: This scss file for profile page style classes + */ +.profile-tab-box { + background: $white; + padding: 10px; + margin-top: 10px; + margin-bottom: 15px; + width: 100%; +} + +.skill-progress { + height: 10px !important; +} +.tab-all a { + color: #948f8f !important; + &.active { + background-color: #e91e63 !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), + 0 13px 24px -11px rgba(233, 30, 99, 0.6); + color: #ffffff !important; + border-radius: 30px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_projects.scss b/MyOffice.SPA/src/assets/scss/pages/_projects.scss new file mode 100644 index 0000000..7d910ba --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_projects.scss @@ -0,0 +1,539 @@ +@use "sass:math"; + +@function rem($size) { + $remSize: math.div($size, 16px); + @return #{$remSize}rem; +} + +.board { + height: 100%; + max-width: 100%; + background-color: #ffffff; + float: none; + box-shadow: none; + .list { + flex: 1 0 0; + margin: rem(16px) 0; + border-radius: rem(5px); + box-shadow: 0 rem(2px) rem(2px) rgba(#000, 0.03), + 0 rem(1px) rem(5px) rgba(#000, 0.02); + overflow: hidden; + .header { + display: flex; + padding: rem(18px) rem(14px); + background: #ffffff; + align-items: center; + .title { + flex: 1; + text-align: center; + h2 { + margin: 0 0 rem(6px); + font-weight: 500; + font-size: 18px; + line-height: 1; + } + .count { + font-size: rem(14px); + line-height: 1; + opacity: 0.65; + } + } + } + .projects { + margin: 0; + padding: rem(14px); + list-style: none; + &.cdk-drop-list-dragging { + opacity: 0.65; + } + .cdk-drag-placeholder { + display: none; + } + } + } +} + +.project { + cursor: pointer; + list-style: none; + padding: rem(16px); + background: #ffffff; + transition: box-shadow 0.2s; + min-height: 50px; + position: relative; + margin-bottom: 24px; + border: 1px solid #d1d1d1; + border-radius: 10px; + box-shadow: 0 0 10px 0 rgba(183, 192, 206, 0.2); + -webkit-box-shadow: 0 0 10px 0 rgba(183, 192, 206, 0.2); + + &:hover { + box-shadow: 0 rem(2px) rem(2px) rgba(#000, 0.1), + 0 rem(1px) rem(5px) rgba(#000, 0.09); + } + &:last-child { + margin-bottom: 0; + } + &:not(.project-list-add) { + padding-right: rem(32px); + } + + h3 { + margin: 0 0 rem(4px); + font-size: 1em; + font-weight: 500; + } + .description { + margin: 0 0 rem(8px); + } + .gravity { + display: flex; + font-size: rem(11px); + letter-spacing: 1px; + text-transform: uppercase; + .priority { + flex: 1; + } + .deadline { + margin-right: rem(-16px); + text-align: right; + .icon { + margin-right: rem(4px); + display: inline-block; + height: 1em; + overflow: visible; + font-size: inherit; + vertical-align: -0.125em; + } + .deadline-label { + display: none; + } + } + } + .project-actions { + opacity: 0; + position: absolute; + top: rem(6px); + right: rem(-3px); + transition: opacity 0.2s; + } + .project-actions[aria-expanded="true"], + &:hover .project-actions { + opacity: 1; + } +} + +@media (min-width: 768px) { + .board { + display: flex; + .list { + margin: 0; + box-shadow: none; + border-radius: 0; + border-width: 0 0 0 1px; + // &:last-child { + // border-right: 1px solid #bbb; + // } + } + } +} + +@media (min-width: 992px) { + .project .gravity .deadline .deadline-label { + display: inline; + } +} + +.wrapper { + display: flex; + flex-flow: column; + height: 100%; +} + +.container { + margin: 0 auto; + padding: 0 rem(8px); + max-width: 1320px; +} + +.header { + flex: 0 1 auto; + padding: rem(18px) 0; + background: linear-gradient( + to top, + var(--color-purple-heart), + var(--color-purple-heart-light) + ); + color: var(--color-white); + .container { + display: flex; + justify-content: space-between; + align-items: center; + } + h1 { + margin: 0; + font-size: rem(16px); + line-height: 1; + font-weight: 500; + text-transform: uppercase; + letter-spacing: rem(1.5px); + a { + color: var(--color-white); + } + svg { + position: relative; + top: -1px; + margin-right: rem(12px); + display: inline-block; + vertical-align: middle; + height: rem(25px); + } + span { + display: inline-block; + vertical-align: middle; + } + } +} + +nav { + flex: 0 1 auto; + padding: rem(3px) 0; + background: var(--color-purple-heart-dark); + .container { + display: flex; + justify-content: space-between; + align-items: center; + } + .menu { + a { + display: inline-block; + padding: rem(11px) rem(11px) rem(9px); + color: var(--color-white); + font-weight: 500; + text-decoration: none; + border-bottom: rem(3px) solid rgba(#fff, 0.5); + } + } + .actions { + position: fixed; + z-index: 10; + right: rem(40px); + bottom: rem(40px); + button { + width: rem(60px); + height: rem(60px); + padding: 0; + background: #1075f2; + color: white; + border: none; + border-radius: 50%; + box-shadow: 0 0 rem(20px) rgba(#000, 0.3); + cursor: pointer; + svg { + height: rem(30px); + } + } + } +} + +main { + position: relative; + flex: 1 1 auto; + .container { + height: 100%; + } +} +.project-bedge { + font-size: 12px; + font-weight: 500; + background-color: #d1d3d4; + display: inline-flex; + cursor: default; + user-select: none; + border-radius: 3px; + padding: 2px 5px; + float: right; +} +.project-bedge2 { + font-size: 12px; + font-weight: 500; + background-color: #d1d3d4; + display: inline-flex; + cursor: default; + user-select: none; + border-radius: 3px; + padding: 2px 5px; + float: left; +} +.project-priority--1 { + color: rgb(0, 218, 0); + font-weight: 500; +} +.project-priority-0 { + color: rgb(233, 0, 233); + font-weight: 500; +} +.project-priority-1 { + color: hsl(0, 84%, 31%); + font-weight: 500; +} +.project-type-Website { + background-color: #1075f2; + color: #fff; +} +.project-type-Android { + background-color: #f21023; + color: #fff; +} +.project-type-IPhone { + background-color: #110103; + color: #fff; +} +.project-type-Testing { + background-color: #06a800; + color: #fff; +} +.pro-left { + font-weight: 500; + padding: 0px 10px; + border-radius: 5px; +} +.project-title { + font-size: 17px; + color: #1f2225; +} +.project-icon { + font-size: 19px; + margin-right: 5px; + vertical-align: text-top; +} +.project-icon2 { + font-size: 16px; + margin-right: 5px; + vertical-align: text-top; +} +.add-icon { + font-size: 19px; + margin-right: 5px; + vertical-align: text-top; +} + +@media (min-width: 576px) { + .container { + padding: 0 rem(16px); + } +} + +.project-people { + text-align: right; + vertical-align: middle; + img { + width: 32px; + height: 32px; + } +} +.project-actions { + text-align: right; + vertical-align: middle; +} +.profile-content { + border-top: none !important; +} +.profile-stats { + margin-right: 10px; +} +.profile-image { + width: 120px; + float: left; + img { + width: 96px; + height: 96px; + } +} +.profile-info { + margin-left: 120px; +} +.feed-element { + padding-bottom: 15px; + margin-top: 15px; + overflow: hidden; + &:first-child { + margin-top: 0; + } + .media { + margin-top: 15px; + } + .well { + border: 1px solid #e7eaec; + box-shadow: none; + background: #f2f3ff; + margin-top: 10px; + margin-bottom: 5px; + padding: 10px 20px; + font-size: 11px; + line-height: 16px; + } + .actions { + margin-top: 10px; + } + .photos { + margin: 10px 0; + } + > .pull-left { + margin-right: 10px; + } + img.img-circle { + width: 38px; + height: 38px; + } +} +.media-body { + overflow: hidden; +} +.feed-photo { + max-height: 180px; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 4px; + overflow: hidden; + margin-right: 10px; + margin-bottom: 10px; +} +.ibox { + clear: both; + margin-bottom: 25px; + margin-top: 0; + padding: 0; + &.collapsed { + .ibox-content { + display: none; + } + .fa { + &.fa-chevron-up:before { + content: "\f078"; + } + &.fa-chevron-down:before { + content: "\f077"; + } + } + } + &:after { + display: table; + } + &:before { + display: table; + } +} +.ibox-title { + background-color: #ffffff; + border-color: #e7eaec; + //Instead of the line below you could use @include border-image($value) + border-image: none; + border-style: solid solid none; + border-width: 3px 0 0; + color: inherit; + margin-bottom: 0; + padding: 14px 15px 7px; + min-height: 48px; +} +.ibox-content { + background-color: #ffffff; + color: inherit; + padding: 0px 20px 20px; + border-image: none; + border-width: 1px 0; +} +.ibox-footer { + color: inherit; + border-top: 1px solid #e7eaec; + font-size: 90%; + background: #ffffff; + padding: 10px 15px; +} +dd.project-people { + text-align: left; + margin-top: 5px; +} +.project-title a { + font-size: 14px; + color: #676a6c; + font-weight: 600; +} +.project-list table tr td { + border-top: none; + border-bottom: 1px solid #e7eaec; + padding: 15px 10px; + vertical-align: middle; +} +.project-manager .tag-list li a { + font-size: 10px; + background-color: white; + padding: 5px 12px; + color: inherit; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 2px; + border: 1px solid #e7eaec; + margin-right: 5px; + margin-top: 5px; + display: block; +} +.project-files li a { + font-size: 11px; + color: #676a6c; + margin-left: 10px; + line-height: 22px; +} +.feed-activity-list { + margin: 10px; + .feed-element { + border-bottom: 1px solid #e7eaec; + } +} +.dropdown-messages-box img.img-circle { + width: 38px; + height: 38px; +} +.file-list li { + padding: 5px 10px; + font-size: 11px; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 2px; + border: 1px solid #e7eaec; + margin-bottom: 5px; + a { + color: inherit; + &:hover { + color: #1ab394; + } + } +} +.user-friends img { + width: 42px; + height: 42px; + margin-bottom: 5px; + margin-right: 5px; +} +.project-activity { + border: 1px solid #e5e5e5; + margin-top: 20px; +} +.project-doc-icon { + width: 40px; + height: 40px; + border: 1px solid #d9d9d9; + border-radius: 0.55rem; + i { + font-size: 20px; + line-height: 40px; + } +} +.project-name { + color: #f68c1f; +} +.project-card-header { + color: #5b626b; + font-size: 17px; + line-height: 28px; + padding-right: 10px; + font-weight: 500; + margin-bottom: 10px; +} diff --git a/MyOffice.SPA/src/assets/scss/pages/_timeline.scss b/MyOffice.SPA/src/assets/scss/pages/_timeline.scss new file mode 100644 index 0000000..a5445a7 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/pages/_timeline.scss @@ -0,0 +1,744 @@ +/* + * Document : _timeline.scss + * Author : RedStar Template + * Description: This scss file for timeline page style classes + */ + +.cd-container { + width: 90%; + max-width: 1170px; + margin: 0 auto; +} + +.cd-container::after { + content: ""; + display: table; + clear: both; +} + +/* -------------------------------- + + Main components + + -------------------------------- */ +header { + height: 200px; + line-height: 200px; + text-align: center; + background: #303e49; +} + +header h1 { + color: #ffffff; + font-size: 18px; + font-size: 1.125rem; +} + +.timelineImgHight { + width: 150px; +} + +@media only screen and (min-width: 1170px) { + header { + height: 300px; + line-height: 300px; + } + header h1 { + font-size: 24px; + font-size: 1.5rem; + } +} + +#cd-timeline { + position: relative; + padding: 2em 0; + margin-top: 2em; + margin-bottom: 2em; +} + +#cd-timeline::before { + /* this is the vertical line */ + content: ""; + position: absolute; + top: 0; + left: 18px; + height: 100%; + width: 4px; + background: #d7e4ed; +} + +@media only screen and (min-width: 1170px) { + #cd-timeline { + margin-top: 3em; + margin-bottom: 3em; + } + #cd-timeline::before { + left: 50%; + margin-left: -2px; + } +} + +.cd-timeline-block { + position: relative; + margin: 2em 0; +} + +.cd-timeline-block::after { + clear: both; + content: ""; + display: table; +} + +.cd-timeline-block:first-child { + margin-top: 0; +} + +.cd-timeline-block:last-child { + margin-bottom: 0; +} + +@media only screen and (min-width: 1170px) { + .cd-timeline-block { + margin: 4em 0; + } + .cd-timeline-block:first-child { + margin-top: 0; + } + .cd-timeline-block:last-child { + margin-bottom: 0; + } +} + +.cd-timeline-img { + position: absolute; + top: 0; + left: 0; + width: 40px; + height: 40px; + border-radius: 50%; + box-shadow: 0 0 0 4px #e6dfdf, inset 0 2px 0 rgba(0, 0, 0, 0.08), + 0 3px 0 4px rgba(0, 0, 0, 0.05); +} + +.cd-timeline-img img { + display: block; + width: 48px; + position: relative; + left: 30%; + top: 30%; + margin-left: -12px; + margin-top: -12px; + border-radius: 50%; +} + +.cd-timeline-img.cd-picture { + background: #75ce66; +} + +.cd-timeline-img.cd-movie { + background: #c03b44; +} + +.cd-timeline-img.cd-location { + background: #f0ca45; +} + +@media only screen and (min-width: 1170px) { + .cd-timeline-img { + width: 60px; + height: 60px; + left: 50%; + margin-left: -30px; + /* Force Hardware Acceleration in WebKit */ + -webkit-transform: translateZ(0); + -webkit-backface-visibility: hidden; + } + .cssanimations .cd-timeline-img.is-hidden { + visibility: hidden; + } + .cssanimations .cd-timeline-img.bounce-in { + visibility: visible; + -webkit-animation: cd-bounce-1 0.6s; + -moz-animation: cd-bounce-1 0.6s; + animation: cd-bounce-1 0.6s; + } +} + +@-webkit-keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + } + 100% { + -webkit-transform: scale(1); + } +} + +@-moz-keyframes cd-bounce-1 { + 0% { + opacity: 0; + -moz-transform: scale(0.5); + } + 60% { + opacity: 1; + -moz-transform: scale(1.2); + } + 100% { + -moz-transform: scale(1); + } +} + +@keyframes cd-bounce-1 { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + -moz-transform: scale(0.5); + -ms-transform: scale(0.5); + -o-transform: scale(0.5); + transform: scale(0.5); + } + 60% { + opacity: 1; + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); + -ms-transform: scale(1.2); + -o-transform: scale(1.2); + transform: scale(1.2); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + -o-transform: scale(1); + transform: scale(1); + } +} + +.cd-timeline-content { + position: relative; + margin-left: 60px; + background: #f0f1f3; + border-radius: 0.25em; + padding: 1em; + box-shadow: 0 3px 0 #d7e4ed; +} + +.cd-timeline-content::after { + clear: both; + content: ""; + display: table; +} + +.cd-timeline-content h2 { + color: #303e49; +} + +.cd-timeline-content p, +.cd-timeline-content .cd-read-more, +.cd-timeline-content .cd-date { + font-size: 13px; + font-size: 0.8125rem; +} + +.cd-timeline-content .cd-read-more, +.cd-timeline-content .cd-date { + display: inline-block; +} + +.cd-timeline-content p { + margin: 1em 0; + line-height: 1.6; +} + +.cd-timeline-content .cd-read-more { + float: right; + padding: 0.8em 1em; + background: #acb7c0; + color: #ffffff; + border-radius: 0.25em; +} + +.no-touch .cd-timeline-content .cd-read-more:hover { + background-color: #bac4cb; +} + +.cd-timeline-content .cd-date { + float: left; + padding: 0.8em 0; + opacity: 0.7; +} + +.cd-timeline-content::before { + content: ""; + position: absolute; + top: 16px; + right: 100%; + height: 0; + width: 0; + border: 7px solid transparent; + border-right: 7px solid #ffffff; +} + +@media only screen and (min-width: 768px) { + .cd-timeline-content h2 { + font-size: 20px; + font-size: 1.25rem; + } + .cd-timeline-content p { + font-size: 16px; + font-size: 1rem; + } + .cd-timeline-content .cd-read-more, + .cd-timeline-content .cd-date { + font-size: 14px; + font-size: 0.875rem; + } +} + +@media only screen and (min-width: 1170px) { + .cd-timeline-content { + margin-left: 0; + padding: 1.6em; + width: 45%; + } + .cd-timeline-content::before { + top: 24px; + left: 100%; + border-color: transparent; + border-left-color: #f0f1f3; + } + .cd-timeline-content .cd-read-more { + float: left; + } + .cd-timeline-content .cd-date { + position: absolute; + width: 100%; + left: 122%; + top: 6px; + font-size: 16px; + font-size: 1rem; + } + .cd-timeline-block:nth-child(even) .cd-timeline-content { + float: right; + } + .cd-timeline-block:nth-child(even) .cd-timeline-content::before { + top: 24px; + left: auto; + right: 100%; + border-color: transparent; + border-right-color: #f0f1f3; + } + .cd-timeline-block:nth-child(even) .cd-timeline-content .cd-read-more { + float: right; + } + .cd-timeline-block:nth-child(even) .cd-timeline-content .cd-date { + left: auto; + right: 122%; + text-align: right; + } + .cssanimations .cd-timeline-content.is-hidden { + visibility: hidden; + } + .cssanimations .cd-timeline-content.bounce-in { + visibility: visible; + -webkit-animation: cd-bounce-2 0.6s; + -moz-animation: cd-bounce-2 0.6s; + animation: cd-bounce-2 0.6s; + } +} + +@media only screen and (min-width: 1170px) { + /* inverse bounce effect on even content blocks */ + .cssanimations + .cd-timeline-block:nth-child(even) + .cd-timeline-content.bounce-in { + -webkit-animation: cd-bounce-2-inverse 0.6s; + -moz-animation: cd-bounce-2-inverse 0.6s; + animation: cd-bounce-2-inverse 0.6s; + } +} + +@-webkit-keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + } + 100% { + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes cd-bounce-2 { + 0% { + opacity: 0; + -moz-transform: translateX(-100px); + } + 60% { + opacity: 1; + -moz-transform: translateX(20px); + } + 100% { + -moz-transform: translateX(0); + } +} + +@keyframes cd-bounce-2 { + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + -moz-transform: translateX(-100px); + -ms-transform: translateX(-100px); + -o-transform: translateX(-100px); + transform: translateX(-100px); + } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + -moz-transform: translateX(20px); + -ms-transform: translateX(20px); + -o-transform: translateX(20px); + transform: translateX(20px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -ms-transform: translateX(0); + -o-transform: translateX(0); + transform: translateX(0); + } +} + +@-webkit-keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + } + 100% { + -webkit-transform: translateX(0); + } +} + +@-moz-keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -moz-transform: translateX(100px); + } + 60% { + opacity: 1; + -moz-transform: translateX(-20px); + } + 100% { + -moz-transform: translateX(0); + } +} + +@keyframes cd-bounce-2-inverse { + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + -moz-transform: translateX(100px); + -ms-transform: translateX(100px); + -o-transform: translateX(100px); + transform: translateX(100px); + } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + -moz-transform: translateX(-20px); + -ms-transform: translateX(-20px); + -o-transform: translateX(-20px); + transform: translateX(-20px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -ms-transform: translateX(0); + -o-transform: translateX(0); + transform: translateX(0); + } +} + +.timeline { + list-style: none; + padding: 0 0 8px; + position: relative; + &:before { + top: 00px; + bottom: 0; + position: absolute; + content: " "; + width: 3px; + background-color: #e7e7e7; + left: 25px; + margin-right: -1.5px; + } + > li { + margin-bottom: 5px; + position: relative; + &:before { + content: " "; + display: table; + } + &:after { + content: " "; + display: table; + clear: both; + } + > { + .timeline-panel { + width: calc(100% - 70px); + float: right; + border: 1px solid #e7e7e7; + border-radius: 2px; + padding: 5px 20px; + position: relative; + border-radius: 10px; + margin-bottom: 5px; + &:before { + position: absolute; + top: 26px; + left: -15px; + display: inline-block; + border-top: 15px solid transparent; + border-right: 15px solid #e7e7e7; + border-left: 0 solid #e7e7e7; + border-bottom: 15px solid transparent; + content: " "; + } + &:after { + position: absolute; + top: 27px; + left: -14px; + display: inline-block; + border-top: 14px solid transparent; + border-right: 14px solid #ffffff; + border-left: 0 solid #ffffff; + border-bottom: 14px solid transparent; + content: " "; + } + } + .timeline-badge { + width: 35px; + height: 35px; + position: absolute; + top: 25px; + left: 8px; + img { + border-radius: 50%; + border: 2px solid #ffffff; + -webkit-box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); + -ms-box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); + } + } + } + } +} +.timeline-title { + margin: 4px 0 !important; + font-size: 13px; +} +.timeline-body > p { + font-size: 12px; + margin-bottom: 2px; +} + +// Timeline 2 + +.left-timeline { + margin: 0; + padding: 0; + list-style: none; + position: relative; + &:before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 3px; + background: #eee; + left: 20%; + margin-left: -6px; + } + > li { + position: relative; + &:first-child { + .left-icon { + background: #fff; + color: #666; + } + .left-time span.large { + color: #444; + font-size: 17px !important; + font-weight: 700; + } + } + &:nth-child(odd) { + .left-label { + background: #f0f1f3; + &:after { + border-right-color: #f0f1f3; + } + } + .left-time span:last-child { + color: #444; + font-size: 13px; + } + } + .left-time { + display: block; + width: 23%; + padding-right: 70px; + position: absolute; + span { + display: block; + text-align: right; + &:first-child { + font-size: 15px; + color: #3d4c5a; + font-weight: 700; + } + &:last-child { + font-size: 14px; + color: #444; + } + } + } + .left-label { + margin: 0 0 15px 25%; + background: #f0f1f3; + padding: 1.2em; + position: relative; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 5px; + &:after { + right: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-right-color: #f0f1f3; + border-width: 10px; + top: 10px; + } + blockquote { + font-size: 16px; + } + .map-checkin { + border: 5px solid rgba(235, 235, 235, 0.2); + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 0 0 1px #ebebeb; + background: #fff !important; + } + h2 { + margin: 0; + padding: 0 0 10px 0; + line-height: 26px; + font-size: 16px; + font-weight: normal; + a { + font-size: 15px; + &:hover { + text-decoration: none; + } + } + span { + font-size: 15px; + } + } + p { + color: #444; + } + } + .left-icon { + width: 40px; + height: 40px; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + font-size: 1.4em; + line-height: 40px; + position: absolute; + color: #fff; + border-radius: 50%; + box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.2); + text-align: center; + left: 20%; + top: 0; + margin: 0 0 0 -25px; + img { + border-radius: 50%; + } + } + .empty span { + color: #777; + } + } +} +@media screen and (max-width: 992px) and (min-width: 768px) { + .left-timeline > li .left-time { + padding-right: 60px; + } +} +@media screen and (max-width: 65.375em) { + .left-timeline > li .left-time span:last-child { + font-size: 12px; + } +} +@media screen and (max-width: 47.2em) { + .left-timeline { + &:before { + display: none; + } + > li { + .left-time { + width: 100%; + position: relative; + padding: 0 0 20px 0; + span { + text-align: left; + } + } + .left-label { + margin: 0 0 30px 0; + padding: 1em; + font-weight: 400; + font-size: 95%; + &:after { + right: auto; + left: 20px; + border-right-color: transparent; + border-bottom-color: #f5f5f6; + top: -20px; + } + } + .left-icon { + position: relative; + float: right; + left: auto; + margin: -64px 5px 0 0; + } + &:nth-child(odd) .left-label:after { + border-right-color: transparent; + border-bottom-color: #f5f5f6; + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_carousel.scss b/MyOffice.SPA/src/assets/scss/plugins/_carousel.scss new file mode 100644 index 0000000..435404d --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_carousel.scss @@ -0,0 +1,37 @@ +/* + * Document : _carousel.scss + * Author : RedStar Template + * Description: This scss file for owl carousel style classes + */ + +.owl-btns { + text-align: center; + [class*="owl-"] { + color: #fff; + font-size: 14px; + margin: 5px; + padding: 4px 7px; + background: #d6d6d6; + display: inline-block; + cursor: pointer; + border-radius: 3px; + &:hover { + background: #869791; + color: #fff; + text-decoration: none; + } + } +} +#dashboard_slide { + padding: 6px 10px 0px 0px; +} +#dashboard_slide2 { + padding: 0px 10px 0px 0px; +} +.carousel-content { + height: 232px; + border-radius: 5px; + .slide-heading { + font-size: 20px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_charts.scss b/MyOffice.SPA/src/assets/scss/plugins/_charts.scss new file mode 100644 index 0000000..fa7850f --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_charts.scss @@ -0,0 +1,305 @@ +/* + * Document : _charts.scss + * Author : RedStar Template + * Description: This scss file for all charts style classes + */ +/* Morris */ +.morris-hover { + &.morris-default-style { + @include border-radius(0); + } +} + +/* Flot */ +.flot-chart { + width: 100%; + height: 320px; +} + +.panel-switch-btn { + position: relative; + right: 20px; + z-index: 9; + + label { + font-weight: bold !important; + } +} + +.legendLabel { + width: 85px !important; + position: relative; + left: 3px; +} + +#multiple_axis_chart { + .legendLabel { + width: 160px !important; + } +} + +/* Sparkline */ +.sparkline { + text-align: center; +} + +.chart-box { + display: flex; + justify-content: space-between; + font-size: 14px; + margin-bottom: 30px; +} +.chart-box2 { + font-size: 14px; + margin-bottom: 30px; + text-align: center; +} +.chart-note { + text-transform: capitalize; + display: inline-block; + margin-right: 12px; + font-size: 14px; + .dot { + margin: 0px 7px; + } +} +.chart-statis { + display: inline-block; + margin-right: 35px; + .label { + display: block; + text-transform: capitalize; + line-height: 1.2; + } + .index { + font-size: 18px; + color: #333; + padding-left: 15px; + } +} + +.dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 100%; +} +.dot-black { + background: #000000; +} +.dot-orange { + background: #f96332; +} +.chart-box-left { + padding-left: 10px; +} +.area_chart-style { + height: 170px; + margin: 30px; +} +.chart-shadow2 { + -webkit-filter: drop-shadow(0 -6px 4px rgba(106, 141, 247, 0.5)); + filter: drop-shadow(0 -6px 4px rgba(106, 141, 247, 0.5)); +} +.chart-shadow { + -webkit-filter: drop-shadow(0px 9px 2px rgba(0, 0, 0, 0.3)); + filter: drop-shadow(0px 9px 2px rgba(0, 0, 0, 0.3)); +} +.chartsh { + height: 16rem; +} + +// chartjs + +.axisData { + position: absolute; + color: #fff; + z-index: 1; + text-transform: uppercase; + display: flex; + width: 100%; + bottom: 0; + .tick { + flex: 1; + position: relative; + overflow: hidden; + opacity: 0.2; + font-size: 11px; + text-align: center; + line-height: 40px; + padding-top: 150px; + &:hover { + opacity: 1; + background-color: rgba(255, 255, 255, 0.2); + .value { + &.productValue { + transform: translateY(0); + display: block; + } + &.serviceValue { + transform: translateY(0); + display: block; + left: 0; + top: 80px; + color: #000; + transition: 0.3s transform; + } + } + } + .value { + transform: translateY(20px); + transition: 0.3s transform; + position: absolute; + top: 20px; + color: #000; + border-radius: 2px; + width: 100%; + line-height: 20px; + } + } +} +.dot-product { + background: #5bcfe4; +} +.dot-service { + background: #64e88b; +} +.dot-product1 { + background: #d3d3d3; +} +.dot-service1 { + background: #6e68c1; +} + +// ngx-chart +.chart-legend { + header { + background: transparent; + height: 0px; + } + .legend-labels { + background: transparent !important; + } +} +.ngx-charts { + text { + fill: #9aa0ac; + } +} + +// Gauge chart +mwl-gauge { + width: 150px; + height: 150px; + display: block; + padding: 10px; +} +mwl-gauge > .gauge > .dial { + stroke: #d7d7d7; + stroke-width: 5; + fill: rgba(0, 0, 0, 0); +} +mwl-gauge > .gauge > .value { + stroke: #4fa1f1; + stroke-width: 5; + fill: rgba(0, 0, 0, 0); +} +mwl-gauge > .gauge > .value-text { + fill: #4fa1f1; + font-family: sans-serif; + font-weight: bold; + font-size: 0.8em; +} +/* ------- Alternate Style ------- */ +mwl-gauge.two { +} +mwl-gauge.two > .gauge > .dial { + stroke: #334455; + stroke-width: 10; +} +mwl-gauge.two > .gauge > .value { + stroke: orange; + stroke-dasharray: none; + stroke-width: 13; +} +mwl-gauge.two > .gauge > .value-text { + fill: orange; +} +/* ------- Alternate Style ------- */ +mwl-gauge.three { +} +mwl-gauge.three > .gauge > .dial { + stroke: #334455; + stroke-width: 2; +} +mwl-gauge.three > .gauge > .value { + stroke: #c9de3c; + stroke-width: 5; +} +mwl-gauge.three > .gauge > .value-text { + fill: #c9de3c; +} +/* ----- Alternate Style ----- */ +mwl-gauge.four > .gauge > .dial { + stroke: #334455; + stroke-width: 5; +} +mwl-gauge.four > .gauge > .value { + stroke: #be80ff; + stroke-dasharray: none; + stroke-width: 5; +} +mwl-gauge.four > .gauge > .value-text { + fill: #be80ff; +} +/* ----- Alternate Style ----- */ +mwl-gauge.five > .gauge > .dial { + stroke: #334455; + stroke-width: 5; +} +mwl-gauge.five > .gauge > .value { + stroke: #f8774b; + stroke-dasharray: 25 1; + stroke-width: 5; +} +mwl-gauge.five > .gauge > .value-text { + fill: #f8774b; + font-size: 0.7em; +} +/* ----- Alternate Style ----- */ +mwl-gauge.six > .gauge > .dial { + stroke: #334455; + fill: #334455; + stroke-width: 20; +} +mwl-gauge.six > .gauge > .value { + stroke: #ff6daf; + stroke-width: 20; +} +mwl-gauge.six > .gauge > .value-text { + fill: #ff6daf; + font-size: 0.7em; +} +mwl-gauge.seven > .gauge > .dial { + stroke: transparent; + stroke-width: 5; + transform: scale(0.9, 0.9) translate3d(5.5px, 5.5px, 0); + fill: rgba(191, 202, 214, 0.42); +} +mwl-gauge.seven > .gauge > .value { + stroke: #f8774b; + stroke-dasharray: none; + stroke-width: 5; +} +ngx-gauge.guage-chart-center { + display: flex; + justify-content: center; + width: 100% !important; +} +.apex-pie-center { + display: flex; + justify-content: center; +} +.apexcharts-legend-marker { + margin: 0px 5px !important; +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_formwizard.scss b/MyOffice.SPA/src/assets/scss/plugins/_formwizard.scss new file mode 100644 index 0000000..a63cbc2 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_formwizard.scss @@ -0,0 +1,368 @@ +/* + * Document : _formwizard.scss + * Author : RedStar Template + * Description: This scss file for wizard style classes + */ + +.wizard, +.tabcontrol { + display: block; + width: 100%; + overflow: hidden; +} + +.wizard a, +.tabcontrol a { + outline: 0; +} + +.wizard ul, +.tabcontrol ul { + list-style: none !important; + padding: 0; + margin: 0; +} + +.wizard ul > li, +.tabcontrol ul > li { + display: block; + padding: 0; +} +/* Accessibility */ +.wizard > .steps .current-info, +.tabcontrol > .steps .current-info, +.wizard > .content > .title, +.tabcontrol > .content > .title { + position: absolute; + left: -999em; +} + +.wizard { + > .steps { + position: relative; + display: block; + width: 100%; + } + + &.vertical { + > .steps { + float: left; + width: 30%; + clear: none; + } + + > .steps > ul > li { + float: none; + width: 100%; + } + + > .content { + float: left; + margin: 0 0 0.5em 0; + width: 70%; + clear: none; + } + + > .actions { + float: right; + width: 100%; + } + + > .actions > ul > li { + margin: 0 0 0 1em; + } + } + + > { + .steps { + .number { + font-size: 1.429em; + } + + > ul > li { + width: 25%; + float: left; + } + } + + .actions > ul > li { + float: left; + } + } + + > { + .steps { + a { + display: block; + width: auto; + margin: 0 0.5em 0.5em; + padding: 1em 1em; + text-decoration: none; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + + &:hover, + &:active { + display: block; + width: auto; + margin: 0 0.5em 0.5em; + padding: 1em 1em; + text-decoration: none; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + } + } + + .disabled a { + background: #eee; + color: #aaa; + cursor: default; + + &:hover, + &:active { + background: #eee; + color: #aaa; + cursor: default; + } + } + + .current a { + background: #2184be; + color: #fff; + cursor: default; + + &:hover, + &:active { + background: #2184be; + color: #fff; + cursor: default; + } + } + + .done a { + background: #9dc8e2; + color: #fff; + + &:hover, + &:active { + background: #9dc8e2; + color: #fff; + } + } + + .error a { + background: #ff3111; + color: #fff; + + &:hover, + &:active { + background: #ff3111; + color: #fff; + } + } + } + + .content { + border: 1px solid #ddd; + display: block; + margin: 0.5em; + min-height: 35em; + overflow: hidden; + position: relative; + width: auto; + } + } + + > { + .actions { + position: relative; + display: block; + text-align: right; + width: 100%; + } + } + + > .actions > ul { + display: inline-block; + text-align: right; + + > li { + margin: 0 0.5em; + } + } + + > { + .actions { + a { + background: #009688; + color: #fff; + display: block; + padding: 0.5em 1em; + text-decoration: none; + @include border-radius(0); + + &:hover, + &:active { + background: #009688; + color: #fff; + display: block; + padding: 0.5em 1em; + text-decoration: none; + @include border-radius(0); + } + } + + .disabled a { + background: #eee; + color: #aaa; + + &:hover, + &:active { + background: #eee; + color: #aaa; + } + } + } + } +} + +.tabcontrol > { + .steps { + position: relative; + display: block; + width: 100%; + + > ul { + position: relative; + margin: 6px 0 0 0; + top: 1px; + z-index: 1; + + > li { + float: left; + margin: 5px 2px 0 0; + padding: 1px; + -webkit-border-top-left-radius: 5px; + -webkit-border-top-right-radius: 5px; + -moz-border-radius-topleft: 5px; + -moz-border-radius-topright: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + + &:hover { + background: #edecec; + border: 1px solid #bbb; + padding: 0; + } + + &.current { + background: #fff; + border: 1px solid #bbb; + border-bottom: 0 none; + padding: 0 0 1px 0; + margin-top: 0; + + > a { + padding: 15px 30px 10px 30px; + } + } + + > a { + color: #5f5f5f; + display: inline-block; + border: 0 none; + margin: 0; + padding: 10px 30px; + text-decoration: none; + + &:hover { + text-decoration: none; + } + } + } + } + } + + .content { + position: relative; + display: inline-block; + width: 100%; + height: 35em; + overflow: hidden; + border-top: 1px solid #bbb; + padding-top: 20px; + + > .body { + float: left; + position: absolute; + width: 95%; + height: 95%; + padding: 2.5%; + + ul { + list-style: disc !important; + + > li { + display: list-item; + } + } + } + } +} + +.wizard { + .content { + min-height: 245px; + @include border-radius(0); + overflow-y: auto; + + .body { + padding: 15px; + } + } + + .steps { + a { + @include border-radius(0); + @include transition(0.5s); + + &:active, + &:focus, + &:hover { + @include border-radius(0); + } + } + + .done { + a { + background-color: rgba(#009688, 0.6); + + &:hover, + &:active, + &:focus { + background-color: rgba(#009688, 0.5); + } + } + } + + .error { + a { + background-color: #f44336 !important; + } + } + + .current { + a { + background-color: #009688; + + &:active, + &:focus, + &:hover { + background-color: #009688; + } + } + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_imagegallery.scss b/MyOffice.SPA/src/assets/scss/plugins/_imagegallery.scss new file mode 100644 index 0000000..9586c44 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_imagegallery.scss @@ -0,0 +1,22 @@ +/* + * Document : _imagegallery.scss + * Author : RedStar Template + * Description: This scss file for image gallery style classes + */ +.group-1, +.group-2 { + border: 1px solid #ffffff; + border-radius: 5px; + display: table; + margin-bottom: 20px; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.group-1 img, +.group-2 img { + cursor: pointer; + width: 300px; + height: 300px; + padding: 10px; + border-radius: 10px; +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_maps.scss b/MyOffice.SPA/src/assets/scss/plugins/_maps.scss new file mode 100644 index 0000000..3d4aef8 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_maps.scss @@ -0,0 +1,9 @@ +/* + * Document : _maps.scss + * Author : RedStar Template + * Description: This scss file for maps style classes + */ +/* Google Maps */ +agm-map { + height: 300px; +} diff --git a/MyOffice.SPA/src/assets/scss/plugins/_tables.scss b/MyOffice.SPA/src/assets/scss/plugins/_tables.scss new file mode 100644 index 0000000..46c0189 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/plugins/_tables.scss @@ -0,0 +1,910 @@ +/* + * Document : _tables.scss + * Author : RedStar Template + * Description: This scss file for tables style classes + */ + + .table { + tbody { + border-top: none !important; + tr { + td, + th { + padding: 10px; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; + vertical-align: middle; + + ul { + margin: 0; + } + .tbl-user-img-small { + margin: 0px 5px; + border-radius: 5px; + height: 30px; + width: 30px; + } + } + } + + tr.primary { + td, + th { + background-color: #1f91f3; + color: #fff; + } + } + + tr.success { + td, + th { + background-color: #2b982b; + color: #fff; + } + } + + tr.info { + td, + th { + background-color: #00b0e4; + color: #fff; + } + } + + tr.warning { + td, + th { + background-color: #ff9600; + color: #fff; + } + } + + tr.danger { + td, + th { + background-color: #fb483a; + color: #fff; + } + } + } + + thead { + tr { + th { + padding: 0 10px; + height: 50px; + vertical-align: middle; + background-color: #f5f5f5; + color: #666; + font-weight: 500; + border: none; + } + } + } + + .tbl-pdf { + color: #f96332; + font-size: 20px; + cursor: pointer; + } + .tbl-action-btn { + height: 40px; + width: 40px; + display: inline-flex; + align-items: center; + } +} + +.table-bordered { + border-top: 1px solid #eee; + + tbody { + tr { + td, + th { + padding: 10px; + border: 1px solid #eee; + } + } + } + + thead { + tr { + th { + padding: 10px; + border: 1px solid #eee; + } + } + } +} + +.table-img { + img { + border-radius: 5px; + height: 33px; + width: 33px; + background: #fff; + position: inherit; + } +} + +.btn-tbl-edit { + background-color: #96a2b4; + height: 30px !important; + width: 30px !important; + margin: 2px !important; + line-height: 30px !important; + color: #fff; + + // box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2) !important; + .material-icons { + font-size: 16px !important; + } + + &:hover { + background-color: #888; + color: $white; + } + + &:focus { + background-color: #888; + } +} + +.btn-tbl-delete { + background-color: #ff944f; + height: 30px !important; + width: 30px !important; + margin: 2px !important; + line-height: 30px !important; + color: #fff; + + // box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2) !important; + .material-icons { + font-size: 16px !important; + } + + &:hover { + background-color: #ff9600; + color: $white; + } + + &:focus { + background-color: #ff9600; + } +} +.btn-tbl-confirm { + background-color: #3fa3f3; + height: 30px !important; + width: 30px !important; + margin: 2px !important; + line-height: 30px !important; + color: #fff; + + // box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2) !important; + .material-icons { + font-size: 16px !important; + } + + &:hover { + background-color: #62b5f8; + color: $white; + } + + &:focus { + background-color: #62b5f8; + } +} +.btn-tbl-reject { + background-color: #f96333; + height: 30px !important; + width: 30px !important; + margin: 2px !important; + line-height: 30px !important; + color: #fff; + + // box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2) !important; + .material-icons { + font-size: 16px !important; + } + + &:hover { + background-color: #f87346; + color: $white; + } + + &:focus { + background-color: #f87346; + } +} + +.tbl-fav-edit { + color: #6777ef; + display: inline !important; + .feather { + height: 20px !important; + width: 20px !important; + } +} +.tbl-fav-delete { + color: #ff5200; + display: inline !important; + .feather { + height: 20px !important; + width: 20px !important; + } +} +.avatar { + position: relative; + width: 30px; + white-space: nowrap; + border-radius: 1000px; + vertical-align: bottom; + display: inline-block; + + img { + width: 100%; + max-width: 100%; + height: auto; + border: 0; + border-radius: 1000px; + } +} + +.avatar-sm { + width: 32px; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.spinner { + display: inline-block; +} + +.spinner-reverse { + display: inline-block; +} + +.order-list li { + img { + border: 2px solid #ffffff; + box-shadow: 0 2px 10px 0 rgba(107, 111, 130, 0.3); + } + + + li { + margin-left: -14px; + } + + .badge { + background: rgba(255, 255, 255, 0.8); + color: #6b6f82; + margin-bottom: 6px; + } +} + +.buttons-copy { + background-color: #666 !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), + 0 13px 24px -11px rgba(233, 30, 99, 0.6); + color: #fff !important; + border-radius: 30px !important; + border: 0px !important; + height: 30px; + width: 60px; + cursor: pointer; +} + +.buttons-excel { + background-color: #59bf70 !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), + 0 13px 24px -11px rgba(233, 30, 99, 0.6); + color: #fff !important; + border-radius: 30px !important; + border: 0px !important; + height: 30px; + width: 60px; + cursor: pointer; +} + +.buttons-csv { + background-color: #2ab9d0 !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), + 0 13px 24px -11px rgba(233, 30, 99, 0.6); + color: #fff !important; + border-radius: 30px !important; + border: 0px !important; + height: 30px; + width: 60px; + cursor: pointer; +} + +.buttons-pdf { + background-color: #e91e63 !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), + 0 13px 24px -11px rgba(233, 30, 99, 0.6); + color: #fff !important; + border-radius: 30px !important; + border: 0px !important; + height: 30px; + width: 60px; + cursor: pointer; +} + +.buttons-print { + background-color: #6563ef !important; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.2), 0 13px 24px -11px #6563ef; + color: #fff !important; + border-radius: 30px !important; + border: 0px !important; + height: 30px; + width: 60px; + cursor: pointer; +} + +tr.group, +tr.group:hover { + background-color: #ddd !important; +} + +.tableBody { + font-size: 14px; + color: #555; + padding: 0px 15px 0px 15px; +} + +.tbl-checkbox { + text-align: center; +} + +.ngx-datatable.material { + background: #fff; + border: 1px solid #f5f5f5; + box-shadow: none !important; + + .datatable-footer { + .datatable-pager { + .pager li a { + margin: 5px; + + .datatable-icon-right, + .datatable-icon-skip, + .datatable-icon-left, + .datatable-icon-prev { + line-height: 30px; + } + } + + li.active a { + background-color: #0d8df3; + transition: 0.25s ease; + color: #ffffff; + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + } + } +} + +.ngx-table-form-select { + margin-top: 0px; + padding-left: 40px !important; +} + +.ngx-search { + margin: 15px auto !important; + width: 30% !important; + font-size: 13px !important; +} + +.ngx-datatable { + .datatable-body-cell { + display: table !important; + } + + .datatable-body-cell-label { + display: table-cell !important; + vertical-align: middle !important; + } + + .datatable-header-cell { + line-height: 2.4 !important; + } +} + +.ngxTableHeader { + padding: 10px 20px; + display: flex; + align-items: center; + border-color: #e6e9ed; + background-color: #ebebee; + min-width: 800px; + height: 70px; + + .header-buttons { + position: absolute; + right: 30px; + list-style: none; + margin-bottom: 0px; + + li { + display: inline-block; + } + + .material-icons { + color: #000000; + vertical-align: text-bottom; + } + } + + .header-buttons-left { + position: absolute; + list-style: none; + + li { + display: inline-block; + } + + .searchbox { + position: relative; + } + + ::placeholder { + color: rgba(0, 0, 0, 0.54); + } + + .search-icon { + position: absolute; + top: 10px; + padding-left: 10px; + color: rgba(0, 0, 0, 0.54); + } + + input.search-field { + font-weight: 500; + color: rgba(0, 0, 0, 0.54); + border-radius: 25px; + border: 0; + height: 45px; + padding: 8px 8px 8px 50px; + width: 250px; + background: #ffffff; + transition: background 0.2s, width 0.2s; + + &:hover { + background: #fffefe; + } + + &:focus { + outline: none; + width: 380px; + color: #212121; + + &:hover { + background: #f7f7f7; + } + } + } + } + + h2 { + margin: 0; + font-size: 16px; + font-weight: normal; + color: #5b626b; + } +} + +.mat-table { + .mat-header-cell { + font-size: 14px; + font-weight: 600; + } +} + +.ngx-datatable.material { + background: #fff; + border: 1px solid #f5f5f5; + box-shadow: none !important; + + .datatable-footer { + .datatable-pager { + .pager li a { + margin: 5px; + + .datatable-icon-right, + .datatable-icon-skip, + .datatable-icon-left, + .datatable-icon-prev { + line-height: 30px; + } + } + + li.active a { + background-color: #0d8df3; + transition: 0.25s ease; + color: #ffffff; + box-shadow: 4px 3px 6px 0 rgba(0, 0, 0, 0.2); + } + } + } +} + +.ngx-table-form-select { + margin-top: 0px; + padding-left: 40px !important; +} + +.ngx-search { + margin: 15px auto !important; + width: 30% !important; + font-size: 13px !important; +} + +.ngx-datatable { + .datatable-body-cell { + display: table !important; + } + + .datatable-body-cell-label { + display: table-cell !important; + vertical-align: middle !important; + } + + .datatable-header-cell { + line-height: 2.4 !important; + } +} + +.materialTableHeader { + -webkit-box-align: center; + align-items: center; + border-color: #dae1f3; + background-color: #dae1f3; + display: flex; + flex-wrap: wrap; + text-align: center; + // min-width: 800px; + + .left { + flex: 35%; + height: 60px; + display: table-cell; + vertical-align: middle; + text-align: center; + } + .center { + flex: 40%; + height: 60px; + display: table-cell; + vertical-align: middle; + text-align: left; + } + + .right { + flex: 10%; + height: 60px; + display: table-cell; + vertical-align: middle; + text-align: center; + } + + .tbl-export-btn { + float: right; + margin: 0px 15px; + list-style: none; + height: 100%; + display: flex; + align-items: center; + + li { + display: inline-block; + position: relative; + } + } + + .header-buttons-left { + list-style: none; + height: 100%; + display: flex; + align-items: center; + + .tbl-search-box { + position: relative; + margin-left: 10px; + } + .tbl-title { + vertical-align: middle; + margin-left: 20px; + + h2 { + font-weight: 500; + } + } + .tbl-header-btn { + position: relative; + } + + li { + display: inline-block; + } + + .searchbox { + position: relative; + } + + ::placeholder { + color: rgba(0, 0, 0, 0.54); + } + + .search-icon { + position: absolute; + top: 10px; + padding-left: 10px; + color: rgba(0, 0, 0, 0.54); + } + + input.search-field { + font-weight: 500; + color: rgba(0, 0, 0, 0.54); + border-radius: 5px; + border: 0; + height: 45px; + padding: 8px 8px 8px 50px; + width: 250px; + background: #ffffff; + + &:hover { + background: #fffefe; + } + + // &:focus { + // outline: none; + // width: 380px; + // color: #212121; + // &:hover { + // background: #f7f7f7; + // } + // } + } + } + + .header-buttons-right { + text-align: right; + list-style: none; + height: 100%; + margin: 0px 10px; + + .tbl-header-btn { + position: relative; + top: 12px; + } + + li { + display: inline-block; + } + + h2 { + margin: 0; + font-size: 16px; + font-weight: normal; + color: #5b626b; + } + } + + h2 { + margin: 0; + font-size: 16px; + font-weight: normal; + color: #5b626b; + } +} + +.tbl-checkbox label { + margin-bottom: 0px; +} +.column-nowrap { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.table-tab a { + background-color: #e2e2e2 !important; + color: #868788 !important; + border-radius: 30px; + &.active { + background-color: #7274e2 !important; + box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.2), 0 4px 13px -11px #7197b5; + color: #ffffff !important; + border-radius: 30px; + } +} +.app-list { + display: flex; + flex-direction: row; + align-items: center; + border-radius: 10px; + vertical-align: middle; + padding: 10px; + margin-bottom: 10px; + background: #f9f9f9; + img { + margin: 0px 10px; + } + .set-flex { + flex: 1; + } +} +.lecture-list { + display: flex; + flex-direction: row; + align-items: center; + border-radius: 5px; + vertical-align: middle; + padding: 10px; + margin-bottom: 10px; + background: #f6f4ff; +} +.report-list { + display: flex; + align-items: center; + margin-bottom: 20px; + padding: 12px 12px; + border: 1px dashed #bfc9d4; + border-radius: 6px; + .file-style { + font-size: 20px; + margin: 0px 10px; + } + .ms-auto { + margin-right: 10px; + cursor: pointer; + i { + padding-right: 10px; + } + } +} +.patient-group-list { + display: flex; + align-items: center; + padding: 12px 12px; + border-radius: 6px; + .lbl-bedge { + padding: 10px; + margin: 0px 5px; + border-radius: 50%; + position: relative; + display: inline-block; + width: 38px; + height: 38px; + font-size: 14px; + font-weight: 600; + letter-spacing: 1px; + .lbl-bedge-title { + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + } + + .group-details { + line-height: 38px; + padding-left: 10px; + font-weight: 500; + } +} +.tbl-spinner { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} +.doc-file-type { + .d-flex { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #d5dadb; + } + .img-icon { + width: 46px; + height: 46px; + line-height: 46px; + border-radius: 10%; + font-size: 23px; + text-align: center; + margin: 0px 15px; + } + .set-flex { + display: block; + color: #000; + height: 30px; + } + .ms-auto { + margin-right: 10px; + cursor: pointer; + } +} +.primary-rgba { + background-color: rgba(110, 129, 220, 0.1); +} +.success-rgba { + background-color: rgba(95, 194, 126, 0.1); +} +.danger-rgba { + background-color: rgba(244, 68, 85, 0.1); +} +.info-rgba { + background-color: rgba(114, 208, 251, 0.1); +} +.timetable-block { + border-radius: 4px; + border: 1px solid #f4f4f4; + padding: 10px; + margin-bottom: 20px; + box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), + 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), + 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), + 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1); + background: #ffffff; +} +.medicine-list td { + padding: 0.4rem; +} +mat-cell > span.truncate-text { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.export-button { + cursor: pointer; + img { + height: 32px; + width: 32px; + } +} + +.mobile-label { + display: none; +} + +@media (max-width: 600px) { + .mobile-label { + width: 100px; + display: inline-block; + font-weight: bold; + } + + .mat-mdc-table .mdc-data-table__header-row { + display: none; + } + + .mat-mdc-table .mdc-data-table__row { + flex-direction: column; + align-items: start; + padding: 8px 24px; + } + mat-cell:first-of-type, + mat-header-cell:first-of-type, + mat-footer-cell:first-of-type { + padding-left: 10px; + } + .flex-item-right, + .flex-item-left { + flex: 100%; + } + .tbl-col-width-per-6 { + max-width: 100%; + } + .tbl-col-width-per-7 { + max-width: 100%; + } +} diff --git a/MyOffice.SPA/src/assets/scss/style.scss b/MyOffice.SPA/src/assets/scss/style.scss new file mode 100644 index 0000000..7cd7eee --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/style.scss @@ -0,0 +1,104 @@ +/* + * Document : style.css + * Author : RedStar Template + * Description: This is a main style scss file for import all scss files. + * + * Structure (with shortcodes): + [1. Common ] + [2. Fonts ] + [3. Components ] + [4. Apps] + [5. Pages ] + [6. Pugins ] + [7. UI ] + [8. Browser ] + + +/* [1. Common ] */ + +/* Importing Bootstrap SCSS file. */ +@import "common/_variables"; +@import "common/_mixins"; +@import "common/_customanimate.scss"; +@import "common/_general.scss"; +@import "common/_demo.scss"; +@import "common/_helpers.scss"; +@import "common/_media.scss"; +@import "common/_animation.scss"; +@import "common/_rtl.scss"; + +/* [2. Fonts] */ +@import "fonts/fontawesome/fontawesome.scss"; +@import "fonts/fontawesome/regular.scss"; +@import "fonts/fontawesome/solid.scss"; +@import "fonts/fontawesome/brands.scss"; + +/* [3. Components ] */ +@import "components/_breadcrumbs.scss"; +@import "components/_checkboxradio.scss"; +@import "components/_dropdownmenu.scss"; +@import "components/_feed.scss"; +@import "components/_formcomponents.scss"; +@import "components/_infobox.scss"; +@import "components/_inputformgroup.scss"; +@import "components/_labels.scss"; +@import "components/_leftsidebaroverlay.scss"; +@import "components/_navbar"; +@import "components/_navtabs.scss"; +@import "components/_noticeboard.scss"; +@import "components/_rightsidebar.scss"; +@import "components/_searchbar.scss"; +@import "components/_switch.scss"; +@import "components/_thumbnails.scss"; +@import "components/_todo.scss"; +@import "components/_settingSidebar.scss"; + +/* [4. Apps] */ +@import "apps/_calendar.scss"; +@import "apps/_chat.scss"; +@import "apps/_contactlist.scss"; +@import "apps/_contactgrid.scss"; +@import "apps/_dragdrop.scss"; +@import "apps/_task.scss"; + +/* [5. Pages ] */ +@import "pages/_dashboard.scss"; +@import "pages/_inbox.scss"; +@import "pages/_pricing.scss"; +@import "pages/_profile.scss"; +@import "pages/_timeline.scss"; +@import "pages/_projects.scss"; +@import "pages/_auth.scss"; + +/* [6. Pugins ] */ +@import "plugins/_carousel.scss"; +@import "plugins/_charts.scss"; +@import "plugins/_formwizard.scss"; +@import "plugins/_imagegallery.scss"; +@import "plugins/_maps.scss"; +@import "plugins/_tables.scss"; + +/* [7. UI ] */ +@import "ui/_alerts.scss"; +@import "ui/_badgelistgroupitem.scss"; +@import "ui/_buttons.scss"; +@import "ui/_card.scss"; +@import "ui/_collapse.scss"; +@import "ui/_dialogs.scss"; +@import "ui/_expansion.scss"; +@import "ui/_mediaobject.scss"; +@import "ui/_modals.scss"; +@import "ui/_pageloader.scss"; +@import "ui/_pagination.scss"; +@import "ui/_panels.scss"; +@import "ui/_preloaders.scss"; +@import "ui/_progressbars.scss"; +@import "ui/_slider.scss"; +@import "ui/_snackbar.scss"; +@import "ui/_tabs.scss"; +@import "ui/_tooltippopovers.scss"; +@import "ui/_listItems.scss"; + +/* [8. Browser ] */ +@import "browser/_ie10.scss"; +@import "browser/_ie11.scss"; diff --git a/MyOffice.SPA/src/assets/scss/theme/_dark.scss b/MyOffice.SPA/src/assets/scss/theme/_dark.scss new file mode 100644 index 0000000..7481525 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/theme/_dark.scss @@ -0,0 +1,1741 @@ +/* + * Document : dark.scss + * Author : RedStar Template + * Description: This scss file for dark theme style classes + */ +.dark { + background-color: #232b3e; + color: #96a2b4; + padding-top: 1px; + + body, + html { + background-color: #232b3e; + } + + input { + color: #96a2b4 !important; + } + + .dark-font-col { + color: #96a2b4; + } + + .card { + --bs-card-bg: #1a202e; + --bs-card-color: #96a2b4; + --bs-card-border-color: rgba(255, 255, 255, 0.08); + background: #1a202e; + color: #96a2b4; + border: none; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); + + .body { + color: #96a2b4; + background: transparent; + } + + .header { + color: #96a2b4; + + h2 { + color: #96a2b4; + } + } + + .card-content { + color: #96a2b4; + } + + .card-statistic-3 { + .card-content { + color: #ffffff; + } + } + } + .card-bnner { + background: #1a202e; + } + + .course-card { + background: #1a202e; + + .bg-body-light { + background-color: #292c31; + } + } + + .plain-card { + background: #1a202e; + } + + .people-list { + .chat-list { + li { + &:hover { + background: #3f4650; + cursor: pointer; + } + + &.active { + background: #262b33; + } + } + } + } + + .chat { + .chat-history { + .message-data-time { + color: #96a2b4; + } + } + } + + .contact-usertitle-name { + color: #fff; + } + + .demo-skin { + color: #eaeaea; + + .form-check-label { + color: #eaeaea; + } + } + + .demo-settings { + color: #eaeaea; + } + + .block-header { + padding-bottom: 0px; + + h2 { + color: #96a2b4 !important; + } + } + + .select-wrapper { + input.select-dropdown { + color: #fff; + } + } + + .breadcrumb { + li { + &.active { + color: #a5abb1; + } + } + } + + .dropzone { + border: 1px solid #afacac !important; + background-color: #1a202e !important; + + .dz-message { + min-height: 150px !important; + background: #1a202e !important; + color: white; + } + } + + .sl-item { + .sl-content { + p { + color: #96a2b4; + } + } + } + + .info-box5 { + background-color: #1a202e; + } + + .form-check { + color: #96a2b4; + } + + label { + color: #96a2b4; + } + + .input-field { + input, + textarea { + color: #96a2b4; + } + } + + .input-group { + .input-group-addon { + .material-icons { + color: #96a2b4; + } + } + + input[type="text"], + .form-control { + color: #96a2b4; + } + } + + .container-login100 { + .form-group { + .form-control { + background: transparent; + } + } + } + + .form-group { + .form-control { + color: #96a2b4; + background: #1a202e; + } + + input.form-control { + color: #96a2b4; + border-bottom: 1px solid #9e9e9e; + } + } + + .form-check { + .form-check-sign { + .check { + border: 1px solid #96a2b4; + } + } + } + + .form-control { + color: #96a2b4; + background: #1a202e; + } + + .right-sidebar { + background: #1a202e; + } + + .ms-container { + .ms-selectable, + .ms-selection { + li.ms-hover { + color: #96a2b4 !important; + background-color: #46484e !important; + } + } + } + + .nav-tabs { + li { + a.active { + color: #fff !important; + } + } + } + + .bootstrap-tagsinput { + background-color: #343840 !important; + } + + .to-do-list { + li { + background: #1a202e; + } + } + + #mail-nav { + li { + a { + color: #96a2b4; + } + } + } + + .max-texts { + a { + color: #96a2b4; + } + } + + .pricingTable { + background: #0c0c0c; + + .pricingTable-header { + background: #36373c; + } + } + + .profile-tab-box { + background: #1a202e; + } + + .cd-timeline-content { + background: #10131c; + + .timelineLabelColor strong { + color: #ffffff !important; + } + } + + .timeline > li > .timeline-panel { + border: 1px solid #444444; + } + + .cd-timeline-content h2 { + color: #96a2b4; + } + + .chart-note { + color: #96a2b4; + } + + .chart-statis { + .index { + color: #96a2b4; + } + } + + .dataTables_wrapper { + input[type="search"] { + color: #96a2b4; + } + } + + .page-item.disabled .page-link { + color: #96a2b4; + pointer-events: none; + cursor: auto; + background-color: #1a202e; + } + + .table { + // Bootstrap 5 paints cell backgrounds via --bs-table-bg (defaults light). + --bs-table-bg: transparent; + --bs-table-color: #96a2b4; + --bs-table-striped-bg: rgba(255, 255, 255, 0.04); + --bs-table-hover-bg: rgba(255, 255, 255, 0.06); + --bs-table-border-color: rgba(255, 255, 255, 0.1); + --bs-table-striped-color: #96a2b4; + --bs-table-hover-color: #96a2b4; + background-color: transparent; + color: #96a2b4; + + > :not(caption) > * > * { + background-color: var(--bs-table-bg); + color: #96a2b4; + border-bottom-color: var(--bs-table-border-color); + box-shadow: none; + } + + tbody { + tr { + color: #96a2b4; + background-color: transparent; + + td, + th { + border-top: 1px solid rgb(21 24 29); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background-color: transparent !important; + color: #96a2b4; + } + } + } + + thead { + tr { + color: #96a2b4; + + th { + border-bottom: 1px solid #15181d; + border-top: 1px solid #15181d; + background-color: #15181d !important; + color: #abaaaa; + } + } + } + } + + .table-bordered { + border: 1px solid rgba(120, 130, 140, 0.5); + + tbody { + tr { + td, + th { + border-right: 1px solid rgba(120, 130, 140, 0.5); + border-bottom: none; + } + } + } + + thead { + tr { + th { + border: 1px solid rgba(120, 130, 140, 0.5); + } + } + } + + th { + border: 1px solid rgba(120, 130, 140, 0.5); + color: #96a2b4; + } + } + + tr.group, + tr.group:hover { + background-color: #282d35 !important; + } + + .tableBody { + color: #96a2b4; + } + + $theme-black: #1a202e; + + .card .header h2 strong { + color: #96a2b4 !important; + } + + .breadcrumb-main { + .page-title { + color: #a5abb1; + } + .breadcrumb-icon .feather { + color: #a5abb1 !important; + } + .breadcrumb-item { + color: #a5abb1; + ::before { + color: #a5abb1; + } + } + } + + .counter-box, + .box-part { + background: #1a202e !important; + color: #96a2b4; + } + + .tblActnBtn i { + color: #96a2b4; + } + + .chat .chat-history { + .other-message { + background: #3b4453; + + &:after { + border-bottom-color: #3b4454; + } + } + + .message { + color: #96a2b4; + } + + .my-message { + background: #343840; + + &:after { + border-bottom-color: #343840; + } + } + } + + .task-box { + border-bottom: solid 1px #5e5e5f; + color: #96a2b4; + background: #141a27; + } + + .task-list { + border: solid 1px #10141d; + background: #141a27; + } + + .mat-drawer { + background-color: #141a27; + color: #96a2b4; + border-color: #121315 !important; + } + + .mat-drawer-container { + background-color: #1a202e; + } + + .taskbar .card-footer { + background: #1a202e !important; + } + + .move { + background: #1a202e; + } + + .board { + background-color: #1a202e; + + .drop-card { + background-color: #404754; + } + .list { + .header { + background: #10131c; + } + .project-title { + color: #96a2b4; + } + .project { + background: #10131c; + border: 1px solid #363b47; + } + } + } + + .list-group-item { + background: #1a202e; + } + + .product-grid .product-content { + background-color: #3b4453; + } + + .ibox-title, + .ibox-content { + background: #1a202e; + border-color: #6c757d; + } + + .collapsible-header { + background: #1a202e; + } + + .card .card-inside-title { + color: #96a2b4; + } + + .left-timeline > li { + &:nth-child(odd) { + .left-label { + background: #202529; + + &:after { + border-right-color: #202428; + } + } + + .left-time span:last-child { + color: #eee; + } + } + + .left-label { + background: #303438; + + p { + color: #96a2b4; + } + + &:after { + border-right-color: #303438; + } + } + + .empty span { + color: #96a2b4; + } + + .left-time span { + &:first-child { + color: #96a2b4; + } + + &:last-child { + color: #eae9e9; + } + } + } + + .font-icon .icon-preview i { + color: #96a2b4; + } + + .nav-tabs .nav-link.active, + .nav-tabs .nav-item.show .nav-link { + background-color: transparent; + } + + .panel-group { + .panel-primary .panel-title a { + color: #96a2b4; + background: #32363c; + } + + .panel .panel-body { + color: #96a2b4; + } + } + + .btn-outline-primary { + color: #96a2b4 !important; + border: 1px solid #96a2b4 !important; + } + + .chip { + background-color: #31353a; + color: #96a2b4; + } + + .dropdown-content { + background-color: #31353a; + + li > span { + color: #96a2b4; + } + } + + .autocomplete { + font-size: 13px !important; + color: #96a2b4; + caret-color: #fff; + + &:focus { + color: #96a2b4; + } + } + + .autocomplete-content li { + .highlight { + color: #fff; + } + + :hover { + background-color: #343d44; + } + } + + //select 2 + .select2-container--default { + .select2-selection--single { + background: #1a202e; + color: #96a2b4; + border-bottom: 1px solid #9e9e9e; + + .select2-selection__rendered { + background: #1a202e; + color: #96a2b4; + border-bottom: 1px solid #9e9e9e; + } + } + + .select2-selection--multiple { + background: #1a202e; + color: #96a2b4; + border-bottom: 1px solid #9e9e9e; + + .select2-selection__choice { + background-color: #444141; + border: 1px solid #656464; + } + } + + &.select2-container--focus .select2-selection--multiple { + background: #1a202e; + color: #96a2b4; + border-bottom: 1px solid #9e9e9e; + } + + .select2-results > .select2-results__options { + background-color: #282d35; + } + } + + .select2-search--dropdown { + background-color: #1c1f25; + } + + .flatpickr-input { + border-bottom: 1px solid #9e9e9e !important; + } + + .ngx-datatable.material { + background: #1a202e; + color: #96a2b4; + + .datatable-body .datatable-body-row .datatable-body-cell { + color: #96a2b4; + } + + .datatable-header-cell-label { + color: #96a2b4; + } + + :not(.cell-selection) .datatable-body-row:hover .datatable-row-group { + background-color: #1c1f25; + } + } + + .pagination > li > a:hover { + background-color: #fff; + color: #000; + } + + .dataTables_length .custom-select { + background-color: #1a202e; + color: white; + } + + .ngx-search { + color: #96a2b4; + } + + .ngx-datatable { + &.material { + background: #1a202e; + border: 1px solid #404755; + + .datatable-header { + .datatable-header-cell { + background: #1a202e; + } + + .resize-handle { + border-right: solid 1px #1a202e; + } + } + + .datatable-footer { + color: #fff; + + .datatable-pager { + a { + color: #fff; + } + + li.disabled a { + color: #fff !important; + } + } + } + + .datatable-body .datatable-body-row .datatable-body-cell { + color: #96a2b4; + } + + &:not(.cell-selection) .datatable-body-row:hover { + background: #32383e; + + .datatable-row-group { + background: #32383e; + } + } + + .datatable-body-cell, + .datatable-header-cell { + border: 1px solid #404755; + } + } + + &.fixed-header + .datatable-header + .datatable-header-inner + .datatable-header-cell { + color: #96a2b4; + } + } + + ::-webkit-input-placeholder { + color: #96a2b4; + opacity: 1 !important; + /* for older chrome versions. may no longer apply. */ + } + + :-moz-placeholder { + /* Firefox 18- */ + color: #96a2b4; + opacity: 1 !important; + } + + ::-moz-placeholder { + /* Firefox 19+ */ + color: #96a2b4; + opacity: 1 !important; + } + + :-ms-input-placeholder { + color: #96a2b4; + } + + input, + textarea { + color: #96a2b4; + } + + .flatPicker { + color: #96a2b4; + } + + .modal { + .modal-content { + background: #1a202e; + + .modal-body { + color: #96a2b4; + } + } + + .modal-close-button { + color: #96a2b4; + } + } + + .ngx-search { + color: #96a2b4; + } + + .ngx-datatable { + background: #434f5a; + border: 1px solid #343d45; + + &.material { + background: #1a202e; + + .datatable-header { + .datatable-header-cell { + background: #313742; + } + + .resize-handle { + border-right: solid 1px #434f5a; + } + } + + .datatable-footer { + color: #fff; + + .datatable-pager { + a { + color: #fff; + } + + li.disabled a { + color: #fff !important; + } + } + } + + .datatable-body .datatable-body-row .datatable-body-cell { + color: #96a2b4; + } + + &:not(.cell-selection) .datatable-body-row:hover { + background: #32383e; + + .datatable-row-group { + background: #32383e; + } + } + } + + &.fixed-header + .datatable-header + .datatable-header-inner + .datatable-header-cell { + color: #96a2b4; + } + } + + .ngxTableHeader { + background-color: #242931; + + .header-buttons-left strong, + .header-buttons { + color: #96a2b4; + + .material-icons { + color: #96a2b4; + } + + .dropdown-menu li span { + color: #000000; + } + } + .header-buttons-left { + input.search-field { + background-color: #000000; + } + .search-icon { + color: rgba(255, 255, 255, 0.55); + } + input.search-field:focus:hover { + background: #000000; + } + } + } + + .navbar-nav { + .dropdown-menu { + background-color: #1a202e; + border: 1px solid #292a2a; + + &::after { + border-bottom: 6px solid #10131c; + } + + .header { + color: #96a2b4; + border-bottom: 1px solid #232a31; + } + + ul.menu { + .menu-info .menu-title { + color: #fff; + } + + li a { + border-bottom: 1px solid #232a31; + + &:hover { + background-color: #141820; + } + } + .msg-unread { + background-color: #141820; + } + .menu-info .menu-desc { + color: rgb(255, 255, 255, 0.5); + .material-icons { + color: rgb(255, 255, 255, 0.5); + } + } + } + .nfc-read-more { + color: #96a2b4; + } + } + + .user_dw_menu li { + border-bottom: 1px solid #232a31; + + a { + color: #96a2b4; + } + } + } + .fc .fc-col-header-cell-cushion { + color: #fff; + } + + .fc-daygrid-day-top .fc-daygrid-day-number { + color: #96a2b4; + } + .fc-theme-standard .fc-scrollgrid { + border: 1px solid #5c5c5c; + } + .fc-view > table td { + color: #96a2b4; + } + .fc-unthemed td { + &.fc-today { + background: transparent; + } + + &.fc-day-top .fc-day-number { + color: #ffffff; + } + } + + .fc-button-primary { + background-color: #151414 !important; + border-color: #4a4a4a !important; + color: #fff !important; + } + + .fc-view > table { + td { + border-color: #65686d; + } + + th { + color: #fff; + border-color: #65686d; + } + } + + /* Material Design Form style */ + + .mat-checkbox-frame { + border-color: #96a2b4; + } + + .mat-radio-outer-circle { + border-color: #96a2b4; + } + + .mat-datepicker-content { + .mat-calendar-next-button { + color: #96a2b4; + } + + .mat-calendar-previous-button { + color: #96a2b4; + } + + .time-container { + background-color: #12161f; + } + + .actions { + background-color: #12161f; + + .mat-button-wrapper { + .material-icons { + color: white; + } + } + + .mat-stroked-button:not([disabled]) { + border-color: rgba(255, 255, 255, 0.37); + } + } + } + + .mat-datepicker-toggle { + color: #96a2b4; + } + + .mat-select-value { + color: #96a2b4; + } + + .mat-form-field-appearance-legacy { + .mat-hint { + color: #96a2b4; + } + + .mat-form-field-label { + color: #96a2b4; + } + + .mat-form-field-underline { + background-color: #96a2b4; + } + } + + .mat-form-field-appearance-outline { + .mat-form-field-outline-thick { + color: #b7b7b7; + } + + .mat-form-field-outline { + color: #96a2b4; + } + + &.mat-focused .mat-form-field-outline-thick { + color: #96a2b4; + } + } + + .mat-stepper-horizontal { + background-color: #1a202e; + } + + .mat-stepper-vertical { + background-color: #1a202e; + } + + .mat-horizontal-stepper-header::after { + border-top-color: rgba(255, 255, 255, 0.12); + } + + .mat-horizontal-stepper-header::before { + border-top-color: rgba(255, 255, 255, 0.12); + } + + .mat-stepper-horizontal-line { + border-top-color: rgba(255, 255, 255, 0.12); + } + + .mat-form-field-ripple { + background-color: #96a2b4; + } + + .mat-select-arrow { + color: #96a2b4; + } + + input { + caret-color: #96a2b4; + } + + .mat-input-element { + caret-color: #96a2b4; + } + + .mat-hint { + color: #96a2b4; + } + + .mat-mdc-table { + background: #1a202e; + } + + .mat-mdc-cell { + color: #96a2b4; + border-bottom-color: rgba(234, 229, 229, 0.12); + } + + .mat-mdc-footer-cell { + color: #96a2b4; + } + + .mat-mdc-header-cell { + color: #96a2b4; + border-bottom: 1px solid #2a3040; + border-top: 1px solid #2a3040; + background-color: #2a3040; + font-weight: 500; + } + + .mat-mdc-paginator { + color: #96a2b4; + background: #1a202e; + } + + .mat-form-field-type-mat-native-select .mat-form-field-infix::after { + color: #96a2b4; + } + + .mat-form-field.mat-focused .mat-form-field-label { + color: #96a2b4; + } + + .mat-form-field-appearance-fill { + .mat-form-field-flex { + background-color: rgba(0, 0, 0, 0.25); + } + + .mat-form-field-underline::before { + background-color: #96a2b4; + } + } + + .mat-step-header .mat-step-label.mat-step-label-active { + color: #96a2b4; + } + + .mat-paginator-page-size .mat-select-trigger { + color: #96a2b4; + } + + .mat-dialog-container { + background: #2d364a; + color: #c5cdd8; + border: 1px solid #4a5568; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55); + } + + .mat-expansion-panel { + background: #101217; + color: #96a2b4; + border-bottom: 1px solid #31384b; + } + + .mat-expansion-panel-header-title { + color: #96a2b4; + } + + .mat-expansion-panel-header-description { + color: #96a2b4; + } + + .mat-card, + .mat-mdc-card { + background: #12161f; + color: #96a2b4; + } + + .mat-card-subtitle, + .mat-mdc-card-subtitle { + color: #96a2b4; + } + + .mat-list-base .mat-list-option { + color: #96a2b4; + } + + .mat-list-base .mat-list-item { + color: #96a2b4; + } + + .list-group { + a.list-group-item { + color: #96a2b4; + } + + button.list-group-item { + color: #96a2b4; + } + + .list-group-item:hover { + background-color: rgba(10, 10, 10, 0.5); + } + } + + .mat-tab-label, + .mat-tab-link { + color: #96a2b4; + } + + .materialTableHeader { + background-color: #020910; + + h2 { + color: #96a2b4; + } + + ::placeholder { + /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: #96a2b4; + opacity: 1; + /* Firefox */ + } + + :-ms-input-placeholder { + /* Internet Explorer 10-11 */ + color: #96a2b4; + } + + ::-ms-input-placeholder { + /* Microsoft Edge */ + color: #96a2b4; + } + + .header-buttons-left { + input.search-field { + background: #12161f; + color: #96a2b4; + } + + .search-icon { + color: #96a2b4; + } + } + } + + .mat-menu-panel { + background: #12161f; + } + + .mat-menu-item { + color: #96a2b4; + + .mat-icon { + color: #96a2b4; + } + } + + .mat-menu-item-submenu-trigger::after { + color: #96a2b4; + } + + // Angular Material 15+ / MDC: menu colors come from CSS variables on html. + // Overlay panels inherit from body.dark when dark theme is active. + --mat-menu-container-color: #12161f; + --mat-menu-item-label-text-color: #e0e6ed; + --mat-menu-item-icon-color: #e0e6ed; + --mat-menu-item-hover-state-layer-color: rgba(255, 255, 255, 0.08); + --mat-menu-item-focus-state-layer-color: rgba(255, 255, 255, 0.08); + --mat-menu-divider-color: rgba(255, 255, 255, 0.12); + + .mat-datepicker-content { + .mat-calendar { + background: #12161f; + color: #b8bbbd; + + .mat-calendar-body-cell-content { + color: #b8bbbd; + } + + .mat-calendar-table-header { + color: #b8bbbd; + } + + .mat-calendar-body-label { + color: #b8bbbd; + } + + .mat-calendar-body-today:not(.mat-calendar-body-selected) { + border-color: rgba(255, 255, 255, 0.32); + } + + .mat-calendar-arrow { + border-top-color: #b8bbbd; + } + } + } + + .mat-select-panel { + background: #12161f; + color: #b8bbbd; + + .mat-option { + color: #b8bbbd; + } + + .mat-option.mat-active { + color: #b8bbbd; + background: rgba(0, 0, 0, 0.4); + } + } + + .mat-pseudo-checkbox { + color: #b8bbbd; + } + + .mat-autocomplete-panel { + background: #12161f; + color: #b8bbbd; + + .mat-option-text { + color: #b8bbbd; + } + } + + .container-login100 { + background: #12161f; + } + + .login100-form { + background-color: #1a202e; + } + + .login100-form-title, + .error-header, + .error-subheader { + color: #ffffff; + } + + .txt1 { + color: #96a2b4; + } + + .apexcharts-legend-text { + color: #96a2b4 !important; + } + + .mat-button[disabled], + .mat-icon-button[disabled], + .mat-stroked-button[disabled], + .mat-flat-button[disabled], + .mat-raised-button[disabled], + .mat-fab[disabled], + .mat-mini-fab[disabled], + .mat-flat-button[disabled], + .mat-raised-button[disabled], + .mat-fab[disabled], + .mat-mini-fab[disabled], + .mat-flat-button[disabled], + .mat-raised-button[disabled], + .mat-fab[disabled], + .mat-mini-fab[disabled] { + color: rgba(255, 255, 255, 0.23); + } + + .mat-stroked-button:not([disabled]) { + border-color: rgba(255, 255, 255, 0.3); + } + + .mat-bottom-sheet-container { + background: #12161f; + } + + .mat-row { + border-bottom-color: rgba(255, 255, 255, 0.12); + } + // .material-icons-two-tone { + // filter: invert(99%) sepia(95%) saturate(4461%) hue-rotate(181deg) + // brightness(134%) contrast(83%); + // } + .appointment-tab-box { + background-color: #11141b; + } + .owl-dt-calendar-table { + .owl-dt-calendar-cell { + color: #96a2b4; + } + .owl-dt-calendar-header { + color: #96a2b4; + } + } + .owl-dt-container { + background: #11141b; + } + .owl-dt-calendar-control { + color: #ffffff; + } + .owl-dt-timer-content .owl-dt-timer-input { + border: 1px solid #717070; + background-color: #000000; + } + .owl-dt-container-buttons { + color: #ffffff; + } + .show-pwd-icon { + color: #96a2b4; + } + .mat-form-field-prefix, + .mat-form-field-suffix { + color: #96a2b4; + } + .app-list { + background: #181c27; + } + .lecture-list { + background: #181c27; + } + .media .media-body { + color: #96a2b4; + } + .auth-form-section { + background-color: #11141b; + } + .lang-item .lang-item-list { + color: #96a2b4; + &.active { + background-color: #10131c; + } + &:hover { + background-color: #171a21; + span.mdc-list-item__primary-text { + color: #ffffff; + } + } + } + .settingSidebar { + background: #1a202e; + .setting-panel-header { + color: #ffffff; + border: 1px solid #000000; + background: #000000; + } + .border-bottom { + border-bottom: 1px solid #3b434a !important; + } + } + .breadcrumb { + color: #96a2b4; + } + .breadcrumb-icon .feather { + color: #96a2b4; + } + .contact-details-field .color-icon { + filter: invert(68%) sepia(6%) saturate(787%) hue-rotate(177deg) + brightness(94%) contrast(86%); + } + .material-icons-two-tone { + filter: invert(68%) sepia(6%) saturate(787%) hue-rotate(177deg) + brightness(94%) contrast(86%); + } + .doc-file-type { + .d-flex { + border-bottom: 1px solid #15181d; + } + .set-flex { + color: #96a2b4; + } + } + .feed-element .well { + border: 1px solid #10131c; + background: #10131c; + } + .feed-activity-list .feed-element { + border-bottom: 1px solid #525252; + } + .project-activity { + border: 1px solid #525252; + margin-top: 20px; + } + .project-card-header { + color: #96a2b4; + } + .navbar-nav.navbar-right .user_profile span { + color: #fff; + } + + .mat-mdc-dialog-container { + .mdc-dialog__surface { + background: #2d364a; + color: #c5cdd8; + border: 1px solid #4a5568; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55); + } + .mdc-dialog__content { + background: #2d364a; + color: #c5cdd8 !important; + } + } + .mdc-text-field--outlined:not(.mdc-text-field--disabled) + .mdc-notched-outline__leading, + .mdc-text-field--outlined:not(.mdc-text-field--disabled) + .mdc-notched-outline__notch, + .mdc-text-field--outlined:not(.mdc-text-field--disabled) + .mdc-notched-outline__trailing { + border-color: rgba(255, 255, 255, 0.38); + } + + .mdc-text-field--outlined:not(.mdc-text-field--disabled):not( + .mdc-text-field--focused + ):hover + .mdc-notched-outline + .mdc-notched-outline__leading, + .mdc-text-field--outlined:not(.mdc-text-field--disabled):not( + .mdc-text-field--focused + ):hover + .mdc-notched-outline + .mdc-notched-outline__notch, + .mdc-text-field--outlined:not(.mdc-text-field--disabled):not( + .mdc-text-field--focused + ):hover + .mdc-notched-outline + .mdc-notched-outline__trailing { + border-color: #96a2b4; + } + + .mdc-text-field--outlined:not(.mdc-text-field--disabled):not( + .mdc-text-field--focused + ):hover + .mdc-notched-outline + .mdc-notched-outline__notch { + border-left: none; + } + .mat-mdc-dialog-container .mdc-dialog__title { + color: #e2e6ed; + } + .mat-button-toggle-appearance-standard { + color: #96a2b4; + background: #1d1d1d; + } + .mat-button-toggle-checked { + background: #000000 !important; + color: #96a2b4; + } + .mat-button-toggle-standalone.mat-button-toggle-appearance-standard, + .mat-button-toggle-group-appearance-standard { + border: solid 1px #4a4a4a; + } + .mat-button-toggle-group-appearance-standard + .mat-button-toggle + + .mat-button-toggle { + border: solid 1px #4a4a4a; + } + .mat-button-toggle-disabled .mat-button-toggle-button { + background-color: #565656; + color: white; + } + + .mdc-text-field:not(.mdc-text-field--disabled) .mdc-floating-label { + color: #96a2b4; + } + .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__input { + color: #96a2b4; + } + .mat-mdc-select-value { + color: #96a2b4; + } + .mdc-text-field--filled:not(.mdc-text-field--disabled) { + background-color: #181b24; + } + .mat-mdc-checkbox + .mdc-checkbox + .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not( + [data-indeterminate="true"] + ) + ~ .mdc-checkbox__background { + border-color: #96a2b4; + } + .mat-mdc-checkbox + .mdc-checkbox:hover + .mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not( + [data-indeterminate="true"] + ) + ~ .mdc-checkbox__background { + border-color: #96a2b4; + } + + .mat-mdc-paginator-icon { + fill: #96a2b4; + } + .mat-mdc-radio-button + .mdc-radio + .mdc-radio__native-control:enabled:not(:checked) + + .mdc-radio__background + .mdc-radio__outer-circle { + border-color: #96a2b4; + } + .mat-mdc-select-arrow { + color: #96a2b4; + } + .mat-mdc-icon-button[disabled] .mat-mdc-paginator-icon { + fill: rgba(255, 255, 255, 0.15); + } + .mat-mdc-tab:not(.mat-mdc-tab-disabled) .mdc-tab__text-label { + color: #96a2b4; + } + + .mat-mdc-tab-group, + .mat-mdc-tab-header, + .mat-mdc-tab-body-wrapper, + .mat-mdc-tab-body-content, + .mat-mdc-tab-body.mat-mdc-tab-body-active { + background: #1a202e; + color: #96a2b4; + } + + .mat-mdc-tab-header { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + + .mdc-tab-indicator__content--underline { + border-color: #2196f3; + } + .order-list li { + .badge { + background: rgb(12, 12, 12, 0.8); + } + img { + border: 2px solid #545454; + } + } + .mat-mdc-raised-button:disabled { + color: #7c7c7c; + } + + .nfc-menu { + .nfc-dropdown { + background-color: #1b1e27; + .menu { + button { + &:hover { + background-color: #10131c; + } + } + .msg-read { + background-color: #1b1e27; + border-bottom: 1px solid #5e5e5e; + } + .menu-info { + h4 { + color: #96a2b4; + } + .menu-title { + color: #96a2b4; + } + p { + color: rgba(255, 255, 255, 0.55); + } + .menu-desc { + color: rgba(255, 255, 255, 0.55); + .material-icons { + color: #979797; + } + } + } + .msg-unread { + background-color: #000000; + border-bottom: 1px solid #5e5e5e; + } + } + } + .nfc-footer { + background-color: #1b1e27; + border-top: 1px solid #363636; + .nfc-read-all { + color: rgb(181 181 181); + } + } + } + .user_dw_menu { + .mat-mdc-menu-item.mdc-list-item { + background-color: #1b1e27; + color: #e0e6ed; + + &:hover { + background-color: #10131c; + } + } + .mat-mdc-menu-item .mdc-list-item__primary-text { + color: #e0e6ed !important; + } + .user-menu-icons, + .user-menu-icons .feather { + color: #e0e6ed !important; + stroke: #e0e6ed; + } + } + .mat-mdc-select-panel-above .mdc-menu-surface.mat-mdc-select-panel { + background-color: #10131c; + } + .mat-mdc-option.mdc-list-item--selected:not(.mat-mdc-option-multiple):not( + .mdc-list-item--disabled + ) { + background: rgb(0 0 0); + } + .mat-mdc-option.mat-mdc-option-active { + background: rgb(0 0 0); + } + .mat-mdc-option.mdc-list-item { + background-color: #10131c; + } + .mdc-menu-surface.mat-mdc-select-panel { + background-color: #10131c; + } + .mat-mdc-option:hover:not(.mdc-list-item--disabled) { + background: rgb(0 0 0); + } + .mat-mdc-option .mdc-list-item__primary-text { + color: #d5d5d5 !important; + } + .mat-mdc-menu-panel.mat-mdc-menu-panel { + background-color: var(--mat-menu-container-color, #10131c); + } + .mat-mdc-menu-item { + color: var(--mat-menu-item-label-text-color, #e0e6ed) !important; + } + .mat-mdc-menu-item .mdc-list-item__primary-text { + color: var(--mat-menu-item-label-text-color, #e0e6ed) !important; + } + .mat-mdc-menu-item[disabled] .mat-icon-no-color { + color: #d5d5d5; + } + .mat-mdc-menu-item .mat-icon { + color: var(--mat-menu-item-icon-color, #e0e6ed) !important; + } + .profile-menu { + .mat-mdc-menu-item, + .mat-mdc-menu-item .mdc-list-item__primary-text, + .user-menu-icons, + .user-menu-icons .feather { + color: #e0e6ed !important; + } + .user-menu-icons .feather { + stroke: #e0e6ed; + } + } + nav.navbar { + box-shadow: 0 -18px 1px 5px rgb(35 43 62); + } + .app-dropdown { + .app-icons:hover { + background-color: #000000; + } + p { + color: #d5d5d5; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/theme/_theme-black.scss b/MyOffice.SPA/src/assets/scss/theme/_theme-black.scss new file mode 100644 index 0000000..b7b27b4 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/theme/_theme-black.scss @@ -0,0 +1,235 @@ +/* + * Document : theme-black.scss + * Author : RedStar Template + * Description: This scss file for black theme style classes + */ +@import "_theme-color-variables"; + +.theme-black { + .navbar { + background-color: transparent; + .icon-color { + color: #fff; + } + .navbar-nav.navbar-right .user_profile span { + color: #fff; + } + } + + .navbar.active { + background: #485563; + /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #1c212d, #1a202e); + /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #1c212d, #1a202e); + + .nav { + > li { + > a { + color: #fff; + } + } + } + + .collapse-menu-icon .mat-icon { + color: #fff; + } + .collapse-menu-icon .feather { + color: #fff; + } + .nav-notification-icons .mat-icon { + color: #fff; + } + .nav-notification-icons .feather { + color: #fff; + } + } + + .navbar-brand { + color: $theme-black-navbar-brand; + + &:hover { + color: $theme-black-navbar-brand_hover; + } + + &:active { + color: $theme-black-navbar-brand_active; + } + + &:focus { + color: $theme-black-navbar-brand_focus; + } + } + + .nav { + > li { + > a { + &:hover { + background-color: $theme-black-nav-anchor_hover; + text-decoration: none; + } + + &:focus { + background-color: $theme-black-nav-anchor_focus; + text-decoration: none; + } + } + } + + .open { + > a { + background-color: $theme-black-nav-anchor-opened; + + &:hover { + background-color: $theme-black-nav-anchor-opened_hover; + } + + &:focus { + background-color: $theme-black-nav-anchor-opened_focus; + } + } + } + } + + .bars { + color: $theme-black-bar; + } + + .sidebar { + .menu { + .list { + li { + &.active { + background-color: $theme-black-menu-list-active; + } + + a { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + + i, + span { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + } + + &:hover { + color: $theme-black-sidebar-menu-hover; + // i, + // span { + // color: $theme-black-sidebar-menu-hover; + // } + } + } + } + + .ml-menu { + background-color: $theme-black-menu-list-submenu; + } + } + } + + .legal { + background-color: $theme-black-legal-bg; + + .copyright { + a { + color: $theme-black !important; + } + } + } + } + + .breadcrumb li a { + color: $theme-black !important; + } + + .page-item.active .page-link { + background-color: $theme-black; + border-color: $theme-black; + color: #ffffff; + border-radius: 50%; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + } + + .btn-primary { + background-color: $theme-black-button-color !important; + color: #fff !important; + border-color: $theme-black !important; + + &:hover { + background-color: $theme-black !important; + color: #fff !important; + } + + &:active { + background-color: $theme-black !important; + color: #fff !important; + } + + &:focus { + background-color: $theme-black !important; + color: #fff !important; + } + + &:disabled { + background-color: $theme-black !important; + color: #fff !important; + } + } + + .btn-outline-primary { + background: 0 0 !important; + color: $theme-black !important; + border: 1px solid $theme-black !important; + + &:hover { + background: $theme-black !important; + color: #fff !important; + border: 1px solid $theme-black !important; + } + } + + .timelineLabelColor strong { + color: $theme-black !important; + } + + .top-sidebar { + .horizontal-menu { + li { + &.active { + > a { + color: $theme-black; + } + } + + a { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + + &:hover { + color: $theme-black; + } + } + } + } + } + + .nav-tabs > li > a:before { + border-bottom: 2px solid $theme-black; + } +} + +/*Logo Header Background Color*/ +.logo-black { + .navbar-header { + background-color: #1a202e; + border-right: 1px solid #323538; + } +} diff --git a/MyOffice.SPA/src/assets/scss/theme/_theme-color-variables.scss b/MyOffice.SPA/src/assets/scss/theme/_theme-color-variables.scss new file mode 100644 index 0000000..8c5a655 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/theme/_theme-color-variables.scss @@ -0,0 +1,132 @@ +//Theme white +$theme-white: #fff; +$theme-white-active-color: #00bcd4; +$theme-white-sidemenu-active-color: #ffffff; +$theme-white-legal-bg: #2c303b; +$theme-white-bar: #2c303b; +$theme-white-navbar-brand: #2c303b; +$theme-white-navbar-brand_active: #2c303b; +$theme-white-navbar-brand_hover: #2c303b; +$theme-white-navbar-brand_focus: #2c303b; +$theme-white-nav-anchor: #3a2c70; +$theme-white-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-white-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-white-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-white-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-white-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-white-menu-list-active: rgba(0, 0, 0, 0); +$theme-white-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-white-menu-list-submenu: rgba(0, 0, 0, 0); +$theme-white-sidebar-menu-active: #5783c7; +$theme-white-sidebar-menu-hover: #5783c7; + +//Theme Black +$theme-black: #1a202e; +$theme-black-legal-bg: #fff; +$theme-black-bar: #fff; +$theme-black-button-color: #5783c7; +$theme-black-navbar-brand: #fff; +$theme-black-navbar-brand_active: #fff; +$theme-black-navbar-brand_hover: #fff; +$theme-black-navbar-brand_focus: #fff; +$theme-black-nav-anchor: #fff; +$theme-black-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-black-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-black-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-black-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-black-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-black-menu-list-active: rgba(0, 0, 0, 0); +$theme-black-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-black-menu-list-submenu: rgba(0, 0, 0, 0); +$theme-black-sidebar-menu-active: #5783c7; +$theme-black-sidebar-menu-hover: #5783c7; + +//Theme Purple +$theme-purple: #909de4; +$theme-purple-legal-bg: #fff; +$theme-purple-bar: #fff; +$theme-purple-navbar-brand: #fff; +$theme-purple-navbar-brand_active: #fff; +$theme-purple-navbar-brand_hover: #fff; +$theme-purple-navbar-brand_focus: #fff; +$theme-purple-nav-anchor: #fff; +$theme-purple-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-purple-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-purple-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-purple-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-purple-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-purple-menu-list-active: rgba(0, 0, 0, 0); +$theme-purple-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-purple-menu-list-submenu: rgba(0, 0, 0, 0); + +//Theme Blue +$theme-blue: #03a9f3; +$theme-blue-legal-bg: #fff; +$theme-blue-bar: #fff; +$theme-blue-navbar-brand: #fff; +$theme-blue-navbar-brand_active: #fff; +$theme-blue-navbar-brand_hover: #fff; +$theme-blue-navbar-brand_focus: #fff; +$theme-blue-nav-anchor: #fff; +$theme-blue-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-blue-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-blue-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-blue-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-blue-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-blue-menu-list-active: rgba(0, 0, 0, 0); +$theme-blue-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-blue-menu-list-submenu: rgba(0, 0, 0, 0); + +//Theme Cyan +$theme-cyan: #01d8da; +$theme-cyan-legal-bg: #fff; +$theme-cyan-bar: #fff; +$theme-cyan-navbar-brand: #fff; +$theme-cyan-navbar-brand_active: #fff; +$theme-cyan-navbar-brand_hover: #fff; +$theme-cyan-navbar-brand_focus: #fff; +$theme-cyan-nav-anchor: #fff; +$theme-cyan-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-cyan-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-cyan-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-cyan-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-cyan-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-cyan-menu-list-active: rgba(0, 0, 0, 0); +$theme-cyan-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-cyan-menu-list-submenu: rgba(0, 0, 0, 0); + +//Theme Green +$theme-green: #11a37d; +$theme-green-legal-bg: #fff; +$theme-green-bar: #fff; +$theme-green-navbar-brand: #fff; +$theme-green-navbar-brand_active: #fff; +$theme-green-navbar-brand_hover: #fff; +$theme-green-navbar-brand_focus: #fff; +$theme-green-nav-anchor: #fff; +$theme-green-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-green-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-green-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-green-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-green-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-green-menu-list-active: rgba(0, 0, 0, 0); +$theme-green-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-green-menu-list-submenu: rgba(0, 0, 0, 0); + +//Theme Orange +$theme-orange: #f16735; +$theme-orange-legal-bg: #fff; +$theme-orange-bar: #fff; +$theme-orange-navbar-brand: #fff; +$theme-orange-navbar-brand_active: #fff; +$theme-orange-navbar-brand_hover: #fff; +$theme-orange-navbar-brand_focus: #fff; +$theme-orange-nav-anchor: #fff; +$theme-orange-nav-anchor_hover: rgba(0, 0, 0, 0); +$theme-orange-nav-anchor_focus: rgba(0, 0, 0, 0); +$theme-orange-nav-anchor-opened: rgba(0, 0, 0, 0); +$theme-orange-nav-anchor-opened_hover: rgba(0, 0, 0, 0); +$theme-orange-nav-anchor-opened_focus: rgba(0, 0, 0, 0); +$theme-orange-menu-list-active: rgba(0, 0, 0, 0); +$theme-orange-menu-list-toggled: rgba(0, 0, 0, 0); +$theme-orange-menu-list-submenu: rgba(0, 0, 0, 0); diff --git a/MyOffice.SPA/src/assets/scss/theme/_theme-white.scss b/MyOffice.SPA/src/assets/scss/theme/_theme-white.scss new file mode 100644 index 0000000..4651044 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/theme/_theme-white.scss @@ -0,0 +1,231 @@ +/* + * Document : theme-white.scss + * Author : RedStar Template + * Description: This scss file for white theme style classes + */ +@import "_theme-color-variables"; + +.theme-white { + .navbar { + background-color: transparent; + .header-icon .feather { + color: black; + } + } + .navbar-right .user_profile span { + color: rgb(0, 0, 0) !important; + } + .navbar.active { + background-color: $theme-white !important; + .nav { + > li { + > a { + color: #3a2c70; + } + } + } + .collapse-menu-icon .mat-icon { + color: #3a2c70; + } + .nav-notification-icons .mat-icon { + color: #3a2c70; + } + } + + .navbar-brand { + color: $theme-white-navbar-brand; + + &:hover { + color: $theme-white-navbar-brand_hover; + } + + &:active { + color: $theme-white-navbar-brand_active; + } + + &:focus { + color: $theme-white-navbar-brand_focus; + } + } + + .nav { + > li { + > a { + &:hover { + background-color: $theme-white-nav-anchor_hover; + text-decoration: none; + } + + &:focus { + background-color: $theme-white-nav-anchor_focus; + text-decoration: none; + } + } + } + + .open { + > a { + background-color: $theme-white-nav-anchor-opened; + + &:hover { + background-color: $theme-white-nav-anchor-opened_hover; + } + + &:focus { + background-color: $theme-white-nav-anchor-opened_focus; + } + } + } + } + + .bars { + color: $theme-white-bar; + } + + .sidebar { + .menu { + .list { + li { + &.active { + background-color: $theme-white-menu-list-active; + } + a { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + i, + span { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + } + } + } + + .ml-menu { + background-color: $theme-white-menu-list-submenu; + + // li.active a:not(.menu-toggle):before { + // content: "\f068"; + // font-family: "Font Awesome 5 Free"; + // font-size: 11px; + // display: block; + // width: 7px; + // height: 7px; + // position: absolute; + // left: 10%; + // font-weight: 900; + // } + } + } + } + + .legal { + background-color: $theme-white-legal-bg; + + .copyright { + a { + color: $theme-white !important; + } + } + } + } + + .breadcrumb li a { + color: $theme-white-active-color !important; + } + .page-item.active .page-link { + background-color: $theme-white-active-color; + border-color: $theme-white-active-color; + border-radius: 50%; + margin: 5px; + box-shadow: 0 4px 5px 0 #d4d8da, 0 1px 10px 0 #d4d8da, + 0 2px 4px -1px #d4d8da; + padding: 0px 12px; + min-width: 30px; + line-height: 30px; + color: #ffffff; + text-transform: uppercase; + } + + .btn-primary { + background-color: $theme-white-active-color !important; + color: #fff !important; + border-color: $theme-white-active-color !important; + &:hover { + background-color: $theme-white-active-color !important; + color: #fff !important; + } + &:active { + background-color: $theme-white-active-color !important; + color: #fff !important; + } + &:focus { + background-color: $theme-white-active-color !important; + color: #fff !important; + } + &:disabled { + background-color: $theme-white-active-color !important; + color: #fff !important; + } + } + .btn-outline-primary { + background: 0 0 !important; + color: $theme-white-bar !important; + border: 1px solid $theme-white-active-color !important; + &:hover { + background: $theme-white-active-color !important; + color: #fff !important; + border: 1px solid $theme-white !important; + } + } + .timelineLabelColor strong { + color: $theme-white-active-color !important; + } + .top-sidebar { + .horizontal-menu { + li { + &.active { + > a { + color: $theme-white-active-color; + } + } + a { + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + &:hover { + color: $theme-white-active-color; + } + } + } + } + } + .demo-choose-skin li.actived:after { + color: #000; + } + + .nav-tabs > li > a:before { + border-bottom: 2px solid $theme-white-active-color; + } + .search-box input#search { + color: gray; + background: #edecec; + } + .settingSidebar ul.choose-theme li.active div::after { + color: #000; + } +} +/*Logo Header Background Color*/ +.logo-white { + .navbar-header { + background-color: $theme-white; + + .logo-name { + color: #000; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/theme/all-themes.scss b/MyOffice.SPA/src/assets/scss/theme/all-themes.scss new file mode 100644 index 0000000..bd016b3 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/theme/all-themes.scss @@ -0,0 +1,7 @@ +/* + * Light / dark RedStar skins only (Phase 3.6 — color skins removed). + */ + +@import "theme-black"; +@import "theme-white"; +@import "dark"; diff --git a/MyOffice.SPA/src/assets/scss/ui/_alerts.scss b/MyOffice.SPA/src/assets/scss/ui/_alerts.scss new file mode 100644 index 0000000..96edb3e --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_alerts.scss @@ -0,0 +1,42 @@ +/* + * Document : alert.scss + * Author : RedStar Template + * Description: This scss file for alert style classes + */ +.alert { + @include border-radius(0); + @include box-shadow(none); + border: none; + color: #fff !important; + + .alert-link { + color: #fff; + text-decoration: underline; + font-weight: bold; + } +} + +.alert-success { + background-color: rgba(24, 206, 15, 0.8); +} + +.alert-info { + background-color: rgba(44, 168, 255, 0.8); +} + +.alert-warning { + background-color: rgba(255, 178, 54, 0.8); +} + +.alert-danger { + background-color: rgba(255, 54, 54, 0.8); +} + +.alert-dismissible { + .close { + color: #fff; + opacity: 1; + border: none; + text-shadow: none; + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_badgelistgroupitem.scss b/MyOffice.SPA/src/assets/scss/ui/_badgelistgroupitem.scss new file mode 100644 index 0000000..10db563 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_badgelistgroupitem.scss @@ -0,0 +1,238 @@ +/* + * Document : badgelistgroupitem.scss + * Author : RedStar Template + * Description: This scss file for badge style classes + */ +.badge { + padding: 5px 8px; + line-height: 12px; + border: 1px solid; + font-weight: 400; + font-size: 13px; +} + +.list-group-item { + @include border-radius(0); + @include transition(0.5s); +} + +.list-group { + .active, + .list-group-item.active { + background-color: #2196f3; + border-color: #2196f3; + + &:hover, + &:focus, + &:active { + background-color: #2196f3; + border-color: #2196f3; + } + + .list-group-item-text { + color: #dfe9f1; + font-size: 13px; + + &:hover, + &:active, + &:focus { + color: #dfe9f1; + } + } + } + a, + button { + &.list-group-item { + color: #555; + } + &.list-group-item.active { + color: #dfe9f1; + } + } + + .list-group-item.active { + &:hover, + &:focus, + &:active { + .list-group-item-text { + color: #dfe9f1; + } + } + } + + .list-group-item { + &:first-child, + &:last-child { + @include border-radius(0); + } + + .list-group-item-heading { + font-weight: bold; + font-size: 17px; + } + text-align: left; + &:focus, + &:hover { + background-color: #f5f5f5; + } + } + + .list-group-item-success { + background-color: #2b982b; + border: none; + color: #fff; + + &:hover, + &:focus { + background-color: #2b982b; + color: #fff; + opacity: 0.8; + } + } + + .list-group-item-info { + background-color: #00b0e4; + border: none; + color: #fff; + + &:hover, + &:focus { + background-color: #00b0e4; + color: #fff; + opacity: 0.8; + } + } + + .list-group-item-warning { + background-color: #ff9600; + border: none; + color: #fff; + + &:hover, + &:focus { + background-color: #ff9600; + color: #fff; + opacity: 0.8; + } + } + + .list-group-item-danger { + background-color: #fb483a; + border: none; + color: #fff; + + &:hover, + &:focus { + background-color: #fb483a; + color: #fff; + opacity: 0.8; + } + } + + @each $key, $val in $colors { + .pl-#{$key} { + stroke: $val; + } + + .list-group-bg-#{$key} { + background-color: $val; + border: none; + color: #fff; + + &:hover, + &:focus { + background-color: $val; + color: #fff; + opacity: 0.8; + } + } + } +} +span.badge { + min-width: auto; + float: none; + font-size: 13px; +} +.list-group span.badge { + float: right; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.badge-solid-red { + color: #f11541; + background-color: rgba(241, 21, 65, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-purple { + color: #6f42c1; + background-color: rgba(111, 66, 193, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-green { + color: #198754; + background-color: rgba(25, 135, 84, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-blue { + color: #0d6efd; + background-color: rgba(13, 110, 253, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-pink { + color: #fd0dfd; + background-color: rgba(253, 13, 253, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-orange { + color: #fd7e14; + background-color: rgba(253, 126, 20, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-cyan { + color: #0dcaf0; + background-color: rgba(13, 202, 240, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} +.badge-solid-brown { + color: #964b00; + background-color: rgba(150, 75, 0, 0.15); + border: none; + padding: 5px 12px; + font-weight: 500; + line-height: 1.2; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_buttons.scss b/MyOffice.SPA/src/assets/scss/ui/_buttons.scss new file mode 100644 index 0000000..40d73d4 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_buttons.scss @@ -0,0 +1,44 @@ +/* + * Document : _buttons.scss + * Author : RedStar Template + * Description: This scss file for button style classes + */ +.example-button-row button, +.example-button-row a { + margin-right: 8px; +} + +.btn:focus, +.btn.focus { + box-shadow: none; +} + +.btn-space { + margin-right: 10px !important; +} + +/* Dialog Cancel: readable on dark surfaces, secondary to Save */ +.dialog-btn-cancel.mat-mdc-unelevated-button, +.dialog-btn-cancel.mat-mdc-button, +.dialog-btn-cancel { + background-color: #5a6578 !important; + color: rgba(255, 255, 255, 0.5) !important; + --mdc-filled-button-container-color: #5a6578; + --mdc-filled-button-label-text-color: rgba(255, 255, 255, 0.5); + --mdc-text-button-label-text-color: rgba(255, 255, 255, 0.5); + + .mdc-button__label { + color: rgba(255, 255, 255, 0.5) !important; + } + + &:hover { + background-color: #6b7689 !important; + --mdc-filled-button-container-color: #6b7689; + } +} + +.big-button { + width: 200px !important; + min-width: unset !important; + height: 45px; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_card.scss b/MyOffice.SPA/src/assets/scss/ui/_card.scss new file mode 100644 index 0000000..f5fb0be --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_card.scss @@ -0,0 +1,320 @@ +/* + * Document : _card.scss + * Author : RedStar Template + * Description: This scss file for card style classes + */ +.card { + background: #fff; + min-height: 50px; + position: relative; + margin-bottom: 24px; + border: 1px solid #f2f4f9; + border-radius: 10px; + box-shadow: 0 0 10px 0 rgba(183, 192, 206, 0.2); + -webkit-box-shadow: 0 0 10px 0 rgba(183, 192, 206, 0.2); + + .card-inside-title { + margin-top: 25px; + margin-bottom: 15px; + display: block; + font-size: 15px; + color: #000; + + small { + color: #999; + display: block; + font-size: 11px; + margin-top: 5px; + + a { + color: #777; + font-weight: bold; + } + } + } + + .card-inside-title:first-child { + margin-top: 0; + } + + .bg-red, + .bg-pink, + .bg-purple, + .bg-indigo, + .bg-blue, + .bg-cyan, + .bg-teal, + .bg-green, + .bg-yellow, + .bg-orange, + .bg-deep-orange, + .bg-brown, + .bg-grey, + .bg-black { + border-bottom: none !important; + color: #fff !important; + + h2, + small, + .material-icons { + color: #fff !important; + } + + .badge { + background-color: #fff; + color: #555; + } + } + + .header { + position: relative; + display: flex; + width: 100%; + color: #555; + padding: 10px 15px; + line-height: 30px; + border-bottom: 1px solid; + border-color: rgba(82, 63, 105, 0.06); + + .header-dropdown { + position: absolute; + top: 0px; + right: 0px; + list-style: none; + + .dropdown-menu { + li { + display: block !important; + } + } + + li { + display: inline-block; + } + + i { + font-size: 20px; + color: #999; + @include transition(all 0.5s); + + &:hover { + color: #000; + } + } + } + + h2 { + margin: 0; + color: #5b626b; + font-size: 17px; + line-height: 28px; + padding-right: 10px; + font-weight: 500; + + small { + display: block; + font-size: 12px; + margin-top: 5px; + color: #999; + line-height: 15px; + + a { + font-weight: bold; + color: #777; + } + } + } + + .col-xs-12 { + h2 { + margin-top: 5px; + } + } + } + + .body { + font-size: 14px; + color: #555; + padding: 15px; + + @for $i from 1 through 12 { + .col-xs-#{$i}, + .col-sm-#{$i}, + .col-md-#{$i}, + .col-lg-#{$i} { + margin-bottom: 20px; + } + } + } + .list-body { + padding: 0px 10px; + } + &.card-statistic-1 .card-header, + &.card-statistic-2 .card-header { + border-color: transparent; + padding-bottom: 0; + height: auto; + min-height: auto; + display: block; + } + + &.card-statistic-1 .card-icon { + width: 30px; + height: 30px; + margin: 10px 0px 0px 20px; + border-radius: 3px; + line-height: 78px; + text-align: center; + float: left; + font-size: 30px; + } + + &.card-statistic-1 .card-header h4, + &.card-statistic-2 .card-header h4 { + line-height: 1.2; + color: color(muted); + } + + &.card-statistic-1 .card-body, + &.card-statistic-2 .card-body { + padding-top: 0; + } + + &.card-statistic-1 .card-body, + &.card-statistic-2 .card-body { + font-size: 26px; + font-weight: 700; + color: color(fontdark); + padding-bottom: 0; + } + + &.card-statistic-1, + &.card-statistic-2 { + display: inline-block; + width: 100%; + } + + &.card-statistic-1 .card-icon, + &.card-statistic-2 .card-icon { + width: 80px; + height: 80px; + margin: 10px; + border-radius: 3px; + line-height: 94px; + text-align: center; + float: left; + border-radius: 50px; + margin-right: 15px; + + .ion, + .fas, + .far, + .fab, + .fal { + font-size: 22px; + color: #fff; + } + } + + &.card-statistic-1 .card-icon { + line-height: 90px; + } + + &.card-statistic-2 .card-icon { + width: 50px; + height: 50px; + line-height: 50px; + font-size: 22px; + margin: 25px; + box-shadow: 5px 3px 10px 0 rgba(21, 15, 15, 0.3); + border-radius: 10px; + background: #6777ef; + } + &.card-statistic-2 .card-icon-only { + font-size: 35px; + margin: 20px; + } + + &.card-statistic-1 .card-header, + &.card-statistic-2 .card-header { + padding-bottom: 0; + padding-top: 25px; + } + + &.card-statistic-2 .card-header + .card-body, + &.card-statistic-2 .card-body + .card-header { + padding-top: 0; + } + + &.card-statistic-1 .card-header h4, + &.card-statistic-2 .card-header h4 { + font-weight: 600; + font-size: 13px; + letter-spacing: 0.5px; + } + + &.card-statistic-1 .card-header h4 { + margin-bottom: 0; + } + + &.card-statistic-2 .card-header h4 { + text-transform: none; + margin-bottom: 0; + } + + &.card-statistic-1 .card-body { + font-size: 20px; + } + + &.card-statistic-2 { + .card-chart { + margin-left: -10px; + margin-right: -1px; + margin-bottom: -7px; + + canvas { + height: 70px !important; + } + } + .card-right { + float: right; + margin: 15px 15px 15px 0px; + } + } +} +.plain-card { + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.1); + padding: 20px; + border-radius: 10px; + background: #fff; + overflow: hidden; + margin: 0.5rem 0 1rem 0; +} +.card-inner .progress { + height: 10px; + margin: 0px; +} +.col-block { + margin-left: 5px; +} +.card-height-100 { + height: 100px; +} +.doc-card-title { + color: #00bdf2; + font-size: 16px; +} +.doc-card-image { + background: #fff; + position: inherit; + padding: 2px; + box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.2); +} +.card-spacing { + padding-right: 0px; +} +.card-bnner { + background: white; + padding: 15px; + border-radius: 10px; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_ckeditor.scss b/MyOffice.SPA/src/assets/scss/ui/_ckeditor.scss new file mode 100644 index 0000000..2aa06c8 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_ckeditor.scss @@ -0,0 +1,18 @@ +.ck.ck-content ul, +.ck.ck-content ul li { + list-style-type: inherit; +} + +.ck.ck-content ul { + /* Default user agent stylesheet, you can change it to your needs. */ + padding-left: 40px; +} +.ck.ck-content ol, +.ck.ck-content ol li { + list-style-type: decimal; +} + +.ck.ck-content ol { + /* Default user agent stylesheet, you can change it to your needs. */ + padding-left: 40px; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_collapse.scss b/MyOffice.SPA/src/assets/scss/ui/_collapse.scss new file mode 100644 index 0000000..3de6549 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_collapse.scss @@ -0,0 +1,16 @@ +/* + * Document : _collapse.scss + * Author : RedStar Template + * Description: This scss file for collapse style classes + */ +.collapse, +.collapse.in, +.collapsing { + .well { + @include border-radius(0); + margin-bottom: 0; + } + &.show { + display: block; + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_dialogs.scss b/MyOffice.SPA/src/assets/scss/ui/_dialogs.scss new file mode 100644 index 0000000..06d4c71 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_dialogs.scss @@ -0,0 +1,29 @@ +/* + * Document : _dialogs.scss + * Author : RedStar Template + * Description: This scss file for dialogs style classes + */ +.sweet-alert { + @include border-radius(0 !important); + + p { + font-size: 14px !important; + } + + .sa-input-error { + top: 23px !important; + right: 13px !important; + } + + h2 { + font-size: 18px !important; + margin: 0 0 5px 0 !important; + line-height: 40px; + } + + button { + font-size: 15px !important; + @include border-radius(0 !important); + padding: 5px 20px !important; + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_expansion.scss b/MyOffice.SPA/src/assets/scss/ui/_expansion.scss new file mode 100644 index 0000000..cc8000b --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_expansion.scss @@ -0,0 +1,13 @@ +.main-headers-align .mat-expansion-panel-header-title, +.main-headers-align .mat-expansion-panel-header-description { + flex-basis: 0; +} + +.main-headers-align .mat-expansion-panel-header-description { + justify-content: space-between; + align-items: center; +} + +.main-headers-align .mat-form-field + .mat-form-field { + margin-left: 8px; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_listItems.scss b/MyOffice.SPA/src/assets/scss/ui/_listItems.scss new file mode 100644 index 0000000..aaa37bc --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_listItems.scss @@ -0,0 +1,26 @@ +.icon-box { + position: relative; + line-height: 1; + padding: 8px; + border-radius: 5px; +} +.icon-box-orange { + background-color: #ffe3db; +} +.icon-box-green { + background-color: #e1fbe2; +} +.icon-box-purple { + background-color: #dfceff; +} +.icon-box-blue { + background-color: #cbe8ff; +} +.icon-box-red { + background-color: #ffe5e4; +} +.amount-section { + .material-icons { + font-size: 10px; + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_mediaobject.scss b/MyOffice.SPA/src/assets/scss/ui/_mediaobject.scss new file mode 100644 index 0000000..f416071 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_mediaobject.scss @@ -0,0 +1,23 @@ +/* + * Document : _mediaobject.scss + * Author : RedStar Template + * Description: This scss file for media object style classes + */ +.media { + margin-bottom: 25px; + margin-top: 15px; + + .media-left { + padding-right: 10px; + } + .media-body { + color: #777; + font-size: 13px; + + .media-heading { + font-size: 16px; + font-weight: bold; + color: #333; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_modals.scss b/MyOffice.SPA/src/assets/scss/ui/_modals.scss new file mode 100644 index 0000000..6e9d8ed --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_modals.scss @@ -0,0 +1,109 @@ +/* + * Document : _modals.scss + * Author : RedStar Template + * Description: This scss file for modals style classes + */ +.modal { + background-color: rgba(238, 238, 238, 0.41); + //padding: 100px 0 0; + max-height: 100%; + width: 100%; + .modal-header { + border: none; + padding: 0px; + border-bottom: 1px solid #eee; + + .modal-title { + font-weight: bold; + font-size: 16px; + } + } + + .modal-content { + border-radius: 10px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.31) !important; + border: none; + padding: 10px; + + .modal-body { + color: #777; + padding: 15px 25px; + } + } + + .modal-footer { + border: none; + background-color: transparent; + .btn { + margin: 10px; + } + } +} +.addContainer { + display: flex; + flex-direction: column; + overflow: hidden; + .form { + display: flex; + padding-top: 6px; + } + + .mat-form-field { + flex-grow: 1; + } + .modalHeader img { + border-radius: 50%; + } +} + +.modalHeader { + display: flex; + align-items: flex-start; + // justify-content: space-between; + padding: 0px 10px 0px 10px; + margin: 10px 10px 0px 10px; + .modal-about { + padding: 5px; + font-weight: 500; + font-size: 16px; + } +} +.modal-close-button { + background-color: transparent !important; + box-shadow: none !important; + color: #161d38; + right: 10px; + position: absolute !important; +} + +@each $key, $val in $colors { + .modal-col-#{$key} { + background-color: $val; + + .modal-body, + .modal-title { + color: #fff !important; + } + + .modal-footer { + background-color: rgba(0, 0, 0, 0.12); + + .btn-link { + color: #fff !important; + + &:hover, + &:active, + &:focus { + background-color: rgba(0, 0, 0, 0.12); + } + } + } + } +} +.cdk-overlay-connected-position-bounding-box { + z-index: 1052 !important; +} +.mat-dialog-content { + margin: 0px !important; + padding: 0px !important; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_pageloader.scss b/MyOffice.SPA/src/assets/scss/ui/_pageloader.scss new file mode 100644 index 0000000..0246382 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_pageloader.scss @@ -0,0 +1,30 @@ +/* + * Document : _pageloader.scss + * Author : RedStar Template + * Description: This scss file for page loader style classes + */ +.page-loader-wrapper { + z-index: 99999999; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + background: #eee; + overflow: hidden; + text-align: center; + + p { + font-size: 13px; + margin-top: 10px; + font-weight: bold; + color: #444; + } + + .loader { + position: relative; + // top: calc(50% - 30px); + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_pagination.scss b/MyOffice.SPA/src/assets/scss/ui/_pagination.scss new file mode 100644 index 0000000..d07c9a2 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_pagination.scss @@ -0,0 +1,169 @@ +/* + * Document : _pagination.scss + * Author : RedStar Template + * Description: This scss file for pagination style classes + */ +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; + + li { + display: inline; + + > a { + @include border-radius(0); + border: none; + background-color: transparent; + color: #222; + font-weight: bold; + display: inline-block; + padding: 5px 14px; + } + + a:focus, + a:active { + background-color: transparent; + } + } + .previous { + > a, + > span { + float: left; + } + } + .next { + > a, + > span { + float: right; + } + } +} + +.pagination { + margin: 20px 0; + + .disabled { + a, + a:hover, + a:focus, + a:active { + color: #bbb; + } + } + + li.active { + background-color: transparent !important; + a { + background-color: #ffc107; + border-color: #ffc107; + color: #ffffff; + border-radius: 50%; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + } + } + + li { + @include border-radius(0); + + // a:focus, + // a:active { + // background-color: transparent; + // color: #555; + // } + } + + > li { + > a { + border: none; + font-weight: bold; + color: #555; + font-size: 14px; + margin: 0px 3px; + color: gray; + margin: 5px; + border-radius: 50%; + color: gray; + min-width: 30px; + text-transform: uppercase; + padding: 0.5rem 0.75rem; + line-height: 1.25; + } + } + > li { + > a:hover { + font-weight: bold; + background-color: transparent; + color: #8c8b8b; + border-radius: 50%; + font-size: 14px; + } + } + + > li:first-child, + > li:last-child { + > a { + width: auto; + height: 32px; + @include border-radius(0); + + .material-icons { + position: relative; + bottom: 0px; + } + } + } +} + +.pagination-sm { + > li:first-child, + > li:last-child { + > a { + width: 28px; + height: 28px; + + .material-icons { + position: relative; + top: 4px; + left: -6px; + font-size: 20px; + } + } + } + > li > a, + > li > span { + padding: 5px 10px; + font-size: 12px; + } +} + +.pagination-lg { + > li:first-child, + > li:last-child { + > a { + width: 44px; + height: 44px; + + .material-icons { + font-size: 30px; + position: relative; + top: 0px; + left: -10px; + } + } + } + > li > a, + > li > span { + padding: 10px 16px; + font-size: 18px; + } +} +.page-link:focus, +.page-link:hover { + color: #65686b; + text-decoration: none; + background-color: #e3eaf1; + border-color: #e3eaf1; + border-radius: 50%; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_panels.scss b/MyOffice.SPA/src/assets/scss/ui/_panels.scss new file mode 100644 index 0000000..8ecbc63 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_panels.scss @@ -0,0 +1,149 @@ +/* + * Document : _panels.scss + * Author : RedStar Template + * Description: This scss file for panels style classes + */ +.panel-group { + @each $key, $val in $colors { + .panel-col-#{$key} { + border: 1px solid $val; + + .panel-title { + background-color: $val !important; + color: #fff; + } + + .panel-body { + border-top-color: transparent !important; + } + } + } + + .panel { + @include border-radius(0); + margin-top: 5px; + + .panel-title { + margin-bottom: 0; + font-size: 16px; + .material-icons { + float: left; + line-height: 16px; + margin-right: 8px; + } + > a, + > small, + > .small, + > small > a, + > .small > a { + color: #fff; + } + } + + .panel-heading { + padding: 0; + @include border-radius(0); + + a { + display: block; + padding: 10px 15px; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + } + + .panel-body { + color: #555; + border-top: 1px solid #ddd; + padding: 15px; + } + } + + .panel-primary { + border: none; + + .panel-title { + background-color: #f5f5f5; + a { + color: #757575; + font-weight: 400; + font-size: 16px; + } + } + } + + .panel-success { + border: 1px solid #2b982b; + + .panel-title { + background-color: #2b982b; + color: #fff; + } + } + + .panel-warning { + border: 1px solid #ff9600; + + .panel-title { + background-color: #ff9600; + color: #fff; + } + } + + .panel-danger { + border: 1px solid #fb483a; + + .panel-title { + background-color: #fb483a; + color: #fff; + } + } +} + +.full-body { + @each $key, $val in $colors { + .panel-col-#{$key} { + .panel-body { + border-top-color: #fff !important; + background-color: $val; + color: #fff; + } + } + } + + .panel-primary { + .panel-body { + border-top-color: #fff !important; + background-color: #7861a9; + color: #fff; + } + } + + .panel-success { + .panel-body { + border-top-color: #fff !important; + background-color: #2b982b; + color: #fff; + } + } + + .panel-warning { + .panel-body { + border-top-color: #fff !important; + background-color: #ff9600; + color: #fff; + } + } + + .panel-danger { + .panel-body { + border-top-color: #fff !important; + background-color: #fb483a; + color: #fff; + } + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_preloaders.scss b/MyOffice.SPA/src/assets/scss/ui/_preloaders.scss new file mode 100644 index 0000000..7a850c8 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_preloaders.scss @@ -0,0 +1,378 @@ +/* + * Document : _preloaders.scss + * Author : RedStar Template + * Description: This scss file for preloaders style classes + */ +.md-preloader { + @each $key, $val in $colors { + .pl-#{$key} { + stroke: $val; + } + } +} + +.preloader { + display: inline-block; + position: relative; + width: 50px; + height: 50px; + -webkit-animation: container-rotate 1568ms linear infinite; + -moz-animation: container-rotate 1568ms linear infinite; + -o-animation: container-rotate 1568ms linear infinite; + animation: container-rotate 1568ms linear infinite; + + &.pl-size-xl { + width: 75px; + height: 75px; + } + + &.pl-size-l { + width: 60px; + height: 60px; + } + + &.pl-size-md { + width: 50px; + height: 50px; + } + + &.pl-size-sm { + width: 40px; + height: 40px; + } + + &.pl-size-xs { + width: 25px; + height: 25px; + } +} + +.spinner-layer { + position: absolute; + width: 100%; + height: 100%; + border-color: #f44336; + -ms-opacity: 1; + opacity: 1; + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) + infinite both; + -moz-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) + infinite both; + -o-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + + @each $key, $val in $colors { + &.pl-#{$key} { + border-color: $val; + } + } +} + +.right { + float: right !important; +} + +.gap-patch { + position: absolute; + top: 0; + left: 45%; + width: 10%; + height: 100%; + overflow: hidden; + border-color: inherit; + + &.circle { + width: 1000%; + left: -450%; + } +} + +.circle-clipper { + display: inline-block; + position: relative; + width: 50%; + height: 100%; + overflow: hidden; + border-color: inherit; + + .circle { + width: 200%; + height: 100%; + border-width: 3px; + border-style: solid; + border-color: inherit; + border-bottom-color: transparent !important; + -ms-border-radius: 50%; + border-radius: 50%; + -webkit-animation: none; + animation: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + } + + &.left { + .circle { + left: 0; + border-right-color: transparent !important; + -webkit-transform: rotate(129deg); + -moz-transform: rotate(129deg); + -ms-transform: rotate(129deg); + -o-transform: rotate(129deg); + transform: rotate(129deg); + -webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + -moz-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + -o-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + } + + &.right { + .circle { + left: -100%; + border-left-color: transparent !important; + -webkit-transform: rotate(-129deg); + -moz-transform: rotate(-129deg); + -ms-transform: rotate(-129deg); + -o-transform: rotate(-129deg); + transform: rotate(-129deg); + -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + -moz-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite + both; + -o-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + } +} + +@-webkit-keyframes container-rotate { + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes container-rotate { + to { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@-webkit-keyframes fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + transform: rotate(135deg); + } + + 25% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); + } + + 37.5% { + -webkit-transform: rotate(405deg); + transform: rotate(405deg); + } + + 50% { + -webkit-transform: rotate(540deg); + transform: rotate(540deg); + } + + 62.5% { + -webkit-transform: rotate(675deg); + transform: rotate(675deg); + } + + 75% { + -webkit-transform: rotate(810deg); + transform: rotate(810deg); + } + + 87.5% { + -webkit-transform: rotate(945deg); + transform: rotate(945deg); + } + + to { + -webkit-transform: rotate(1080deg); + transform: rotate(1080deg); + } +} + +@keyframes fill-unfill-rotate { + 12.5% { + transform: rotate(135deg); + } + + 25% { + transform: rotate(270deg); + } + + 37.5% { + transform: rotate(405deg); + } + + 50% { + transform: rotate(540deg); + } + + 62.5% { + transform: rotate(675deg); + } + + 75% { + transform: rotate(810deg); + } + + 87.5% { + transform: rotate(945deg); + } + + to { + transform: rotate(1080deg); + } +} + +@-webkit-keyframes left-spin { + from { + -webkit-transform: rotate(130deg); + -moz-transform: rotate(130deg); + -ms-transform: rotate(130deg); + -o-transform: rotate(130deg); + transform: rotate(130deg); + } + + 50% { + -webkit-transform: rotate(-5deg); + -moz-transform: rotate(-5deg); + -ms-transform: rotate(-5deg); + -o-transform: rotate(-5deg); + transform: rotate(-5deg); + } + + to { + -webkit-transform: rotate(130deg); + -moz-transform: rotate(130deg); + -ms-transform: rotate(130deg); + -o-transform: rotate(130deg); + transform: rotate(130deg); + } +} + +@keyframes left-spin { + from { + -moz-transform: rotate(130deg); + -ms-transform: rotate(130deg); + -o-transform: rotate(130deg); + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } + + 50% { + -moz-transform: rotate(-5deg); + -ms-transform: rotate(-5deg); + -o-transform: rotate(-5deg); + -webkit-transform: rotate(-5deg); + transform: rotate(-5deg); + } + + to { + -moz-transform: rotate(130deg); + -ms-transform: rotate(130deg); + -o-transform: rotate(130deg); + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } +} + +@-webkit-keyframes right-spin { + from { + -webkit-transform: rotate(-130deg); + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + transform: rotate(-130deg); + } + + 50% { + -webkit-transform: rotate(5deg); + -moz-transform: rotate(5deg); + -ms-transform: rotate(5deg); + -o-transform: rotate(5deg); + transform: rotate(5deg); + } + + to { + -webkit-transform: rotate(-130deg); + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + transform: rotate(-130deg); + } +} + +@-moz-keyframes right-spin { + from { + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } + + 50% { + -moz-transform: rotate(5deg); + -ms-transform: rotate(5deg); + -o-transform: rotate(5deg); + -webkit-transform: rotate(5deg); + transform: rotate(5deg); + } + + to { + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } +} + +@keyframes right-spin { + from { + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } + + 50% { + -moz-transform: rotate(5deg); + -ms-transform: rotate(5deg); + -o-transform: rotate(5deg); + -webkit-transform: rotate(5deg); + transform: rotate(5deg); + } + + to { + -moz-transform: rotate(-130deg); + -ms-transform: rotate(-130deg); + -o-transform: rotate(-130deg); + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_progressbars.scss b/MyOffice.SPA/src/assets/scss/ui/_progressbars.scss new file mode 100644 index 0000000..c597502 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_progressbars.scss @@ -0,0 +1,126 @@ +/* + * Document : _progressbars.scss + * Author : RedStar Template + * Description: This scss file for progress bars style classes + */ + +.mat-mdc-progress-bar { + &.progress-xs { + height: 4px; + } + &.progress-s { + height: 5px; + } + &.progress-m { + height: 10px; + .mdc-linear-progress__bar-inner { + border-top-width: var(--mdc-linear-progress-track-height, 10px); + } + } + &.progress-l { + height: 15px; + .mdc-linear-progress__bar-inner { + border-top-width: 15px; + } + } + + &.progress-round { + border-radius: 7px; + } + + &.green-progress .mdc-linear-progress__bar-inner { + border-color: #0af30a !important; + } + &.sky-progress .mdc-linear-progress__bar-inner { + border-color: #248afd !important; + } + &.orange-progress .mdc-linear-progress__bar-inner { + border-color: #ffc100 !important; + } + &.red-progress .mdc-linear-progress__bar-inner { + border-color: #ff4747 !important; + } + + &.l-green-progress .mdc-linear-progress__bar-inner { + background: #11998e; /* fallback for old browsers */ + background: -webkit-linear-gradient( + to right, + #38ef7d, + #11998e + ); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient( + to right, + #38ef7d, + #11998e + ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + } + &.l-sky-progress .mat-progress-bar-fill::after { + background: #36d1dc; /* fallback for old browsers */ + background: -webkit-linear-gradient( + to right, + #5b86e5, + #36d1dc + ); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient( + to right, + #5b86e5, + #36d1dc + ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + } + &.l-orange-progress .mat-progress-bar-fill::after { + background: #f12711; /* fallback for old browsers */ + background: -webkit-linear-gradient( + to right, + #f5af19, + #f12711 + ); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient( + to right, + #f5af19, + #f12711 + ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + } + &.l-red-progress .mat-progress-bar-fill::after { + background: #ff416c; /* fallback for old browsers */ + background: -webkit-linear-gradient( + to right, + #ff4b2b, + #ff416c + ); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient( + to right, + #ff4b2b, + #ff416c + ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + } + &.l-purple-progress .mat-progress-bar-fill::after { + background: #7f00ff; /* fallback for old browsers */ + background: -webkit-linear-gradient( + to right, + #e100ff, + #7f00ff + ); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient( + to right, + #e100ff, + #7f00ff + ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + } + + &.progress-shadow { + box-shadow: 0.4rem 0.4rem 0.8rem rgb(0 0 0 / 10%); + } +} + +.progress-list { + position: relative; + + .status { + display: inline-block; + font-size: 12px; + padding: 6px; + position: absolute; + right: 0; + top: 0; + } +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_slider.scss b/MyOffice.SPA/src/assets/scss/ui/_slider.scss new file mode 100644 index 0000000..a7b85e1 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_slider.scss @@ -0,0 +1,3 @@ +mat-slider { + width: 300px; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_snackbar.scss b/MyOffice.SPA/src/assets/scss/ui/_snackbar.scss new file mode 100644 index 0000000..c845b43 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_snackbar.scss @@ -0,0 +1,19 @@ +.mat-mdc-snack-bar-container.snackbar-success .mdc-snackbar__surface { + background-color: rgba(24, 206, 15, 0.8); + color: #ffffff; +} + +.mat-mdc-snack-bar-container.snackbar-info .mdc-snackbar__surface { + background-color: rgba(44, 168, 255, 0.8); + color: #ffffff; +} + +.mat-mdc-snack-bar-container.snackbar-warning .mdc-snackbar__surface { + background-color: rgba(255, 230, 0, 0.8); + color: #ffffff; +} + +.mat-mdc-snack-bar-container.snackbar-danger .mdc-snackbar__surface { + background-color: rgba(255, 0, 0, 0.8); + color: #ffffff; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_tabs.scss b/MyOffice.SPA/src/assets/scss/ui/_tabs.scss new file mode 100644 index 0000000..ff4da90 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_tabs.scss @@ -0,0 +1,29 @@ +.example-small-box, +.example-large-box { + display: flex; + align-items: center; + justify-content: center; + margin: 16px; + padding: 16px; + border-radius: 8px; +} + +.example-small-box { + height: 100px; + width: 100px; +} + +.example-large-box { + height: 300px; + width: 300px; +} +.appointment-tab-box { + background-color: #eff1f7; + width: 100%; + padding: 10px; +} +.tab-header { + color: #555; + padding: 15px; + position: relative; +} diff --git a/MyOffice.SPA/src/assets/scss/ui/_tooltippopovers.scss b/MyOffice.SPA/src/assets/scss/ui/_tooltippopovers.scss new file mode 100644 index 0000000..56e4d01 --- /dev/null +++ b/MyOffice.SPA/src/assets/scss/ui/_tooltippopovers.scss @@ -0,0 +1,30 @@ +/* + * Document : _tooltippopovers.scss + * Author : RedStar Template + * Description: This scss file for tooltip pop overs style classes + */ +.tooltip { + font-size: 13px; + + .tooltip-inner { + @include border-radius(0); + } +} + +.popover { + @include border-radius(0); + border: 1px solid rgba(0, 0, 0, 0.08); + + .popover-title { + font-weight: bold; + @include border-radius(0); + background-color: #e9e9e9; + border-bottom: 1px solid #ddd; + } + + .popover-content { + font-size: 13px; + color: #777; + @include border-radius(0); + } +} diff --git a/MyOffice.SPA/src/environments/environment.docker.ts b/MyOffice.SPA/src/environments/environment.docker.ts new file mode 100644 index 0000000..cfff301 --- /dev/null +++ b/MyOffice.SPA/src/environments/environment.docker.ts @@ -0,0 +1,18 @@ +// Browser → host ports (defaults; Docker build may overwrite via API_PUBLIC_URL). +export const environment = { + production: true, + apiUrl: 'http://localhost:32081', + identityServer: 'http://localhost:32081/', + allowedUrls: ['http://localhost:32081'], + // Plain HTTP demo stack — do not require HTTPS for OIDC. + requireHttps: false, + externalLogins: { + google: { + clientId: '' + }, + auth0: { + clientId: '', + domain: '' + } + } +}; diff --git a/MyOffice.SPA/src/environments/environment.proxmox.ts b/MyOffice.SPA/src/environments/environment.proxmox.ts new file mode 100644 index 0000000..943991a --- /dev/null +++ b/MyOffice.SPA/src/environments/environment.proxmox.ts @@ -0,0 +1,18 @@ +// Same-origin Proxmox deploy: API serves SPA from wwwroot (one public URL). +// Empty apiUrl / identityServer / allowedUrls → resolved at runtime from window.location.origin. +export const environment = { + production: true, + apiUrl: '', + identityServer: '', + allowedUrls: [] as string[], + requireHttps: false, + externalLogins: { + google: { + clientId: '' + }, + auth0: { + clientId: '', + domain: '' + } + } +}; diff --git a/MyOffice.SPA/src/environments/environment.sample.ts b/MyOffice.SPA/src/environments/environment.sample.ts new file mode 100644 index 0000000..66395af --- /dev/null +++ b/MyOffice.SPA/src/environments/environment.sample.ts @@ -0,0 +1,16 @@ +// Copy to environment.ts for local development (environment.ts is gitignored). +export const environment = { + production: false, + apiUrl: 'http://localhost:9100', + identityServer: 'http://localhost:9100/', + allowedUrls: ['http://localhost:9100'], + externalLogins: { + google: { + clientId: '' + }, + auth0: { + clientId: '', + domain: '' + } + } +}; diff --git a/MyOffice.SPA/src/favicon.ico b/MyOffice.SPA/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/MyOffice.SPA/src/favicon.ico differ diff --git a/MyOffice.SPA/src/index.html b/MyOffice.SPA/src/index.html new file mode 100644 index 0000000..89b374c --- /dev/null +++ b/MyOffice.SPA/src/index.html @@ -0,0 +1,21 @@ + + + + + MyOffice + + + + + + + + + + + diff --git a/MyOffice.SPA/src/main.ts b/MyOffice.SPA/src/main.ts new file mode 100644 index 0000000..2a48f56 --- /dev/null +++ b/MyOffice.SPA/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; + +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +bootstrapApplication(AppComponent, appConfig) + .catch(err => console.error(err)); diff --git a/MyOffice.SPA/src/polyfills.ts b/MyOffice.SPA/src/polyfills.ts new file mode 100644 index 0000000..f85b963 --- /dev/null +++ b/MyOffice.SPA/src/polyfills.ts @@ -0,0 +1,53 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes recent versions of Safari, Chrome (including + * Opera), Edge on the desktop, and iOS and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/MyOffice.SPA/src/proxy.conf.js b/MyOffice.SPA/src/proxy.conf.js new file mode 100644 index 0000000..62ba391 --- /dev/null +++ b/MyOffice.SPA/src/proxy.conf.js @@ -0,0 +1,15 @@ +const PROXY_CONFIG = [ + { + context: [ + '/api', + '/authorize', + '/.well-known', + '/connect', + '/weatherforecast', + ], + target: 'http://localhost:9100', + secure: false + } +]; + +module.exports = PROXY_CONFIG; diff --git a/MyOffice.SPA/src/silent-refresh.html b/MyOffice.SPA/src/silent-refresh.html new file mode 100644 index 0000000..0c2ab58 --- /dev/null +++ b/MyOffice.SPA/src/silent-refresh.html @@ -0,0 +1,31 @@ + + + + + + + diff --git a/MyOffice.SPA/src/styles.scss b/MyOffice.SPA/src/styles.scss new file mode 100644 index 0000000..008c901 --- /dev/null +++ b/MyOffice.SPA/src/styles.scss @@ -0,0 +1,68 @@ +/* You can add global styles to this file, and also import other style files */ + +.mdc-text-field--disabled.mdc-text-field--filled { + background-color: #4c4f57; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +.spinner:before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + border-radius: 50%; + border: 2px solid #ffffff; + border-top-color: #000000; + animation: spinner .8s linear infinite; +} + +.mat-dialog-actions { + display: grid !important; + grid-template-columns: 1fr auto !important; + align-items: center !important; +} + +.mat-dialog-actions { + display: grid !important; + grid-template-columns: 1fr auto !important; + align-items: center !important; +} + +.mat-dialog-left { + display: flex !important; + align-items: center !important; +} + +.error-icon { + margin-right: 8px !important; +} + +.error-message { + margin: 0 !important; + overflow: auto !important; +} + +.mat-dialog-right { + display: flex !important; + justify-content: flex-end !important; + align-items: center !important; +} + +.button-bottom { + display: flex !important; + align-items: center !important; +} + +.button-bottom button { + margin-left: 8px !important; +} diff --git a/MyOffice.SPA/src/styles/_variables.scss b/MyOffice.SPA/src/styles/_variables.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/styles/global.scss b/MyOffice.SPA/src/styles/global.scss new file mode 100644 index 0000000..6c384d0 --- /dev/null +++ b/MyOffice.SPA/src/styles/global.scss @@ -0,0 +1,277 @@ +@import '@angular/material/theming'; +@include mat-core(); + +/* You can add global styles to this file, and also import other style files */ +html, body { + height: 100%; +} + +body { + margin: 0; + font-family: Roboto, "Helvetica Neue", sans-serif; +} + +.app-header { + justify-content: space-between; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 2; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12); +} + +.login-wrapper { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.mat-card-content-center { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.primary-content { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + min-width: 600px; + padding: 30px 40px 30px 40px; +} + +.box { + position: relative; + top: 0; + opacity: 1; + float: left; + padding: 30px 40px 30px 40px; + width: 100%; + background: #fff; + border-radius: 10px; + transform: scale(1); + -webkit-transform: scale(1); + -ms-transform: scale(1); + z-index: 5; + max-width: 330px; +} + +.box.back { + transform: scale(.95); + -webkit-transform: scale(.95); + -ms-transform: scale(.95); + top: -20px; + opacity: .8; + z-index: -1; +} + +.box:before { + content: ""; + width: 100%; + height: 30px; + border-radius: 10px; + position: absolute; + top: -10px; + background: rgba(255, 255, 255, .6); + left: 0; + transform: scale(.95); + -webkit-transform: scale(.95); + -ms-transform: scale(.95); + z-index: -1; +} + +mat-form-field { + width: 350px !important; +} + +.mat-mdc-form-field-infix { +} + +/* margins */ +.m-5 { + margin: 5px; +} + +.m-10 { + margin: 10px; +} + +.m-15 { + margin: 15px; +} + +.m-20 { + margin: 20px; +} + +.m-25 { + margin: 25px; +} + +.m-30 { + margin: 30px; +} + +.mt-5 { + margin-top: 5px; +} + +.mt-10 { + margin-top: 10px; +} + +.mt-15 { + margin-top: 15px; +} + +.mt-20 { + margin-top: 20px; +} + +.mt-25 { + margin-top: 25px; +} + +.mt-30 { + margin-top: 30px; +} + +.mb-5 { + margin-bottom: 5px; +} + +.mb-10 { + margin-bottom: 10px; +} + +.mb-15 { + margin-bottom: 15px; +} + +.mb-20 { + margin-bottom: 20px; +} + +.mb-25 { + margin-bottom: 25px; +} + +.mb-30 { + margin-bottom: 30px; +} + +.ml-5 { + margin-left: 5px; +} + +.ml-10 { + margin-left: 10px; +} + +.ml-15 { + margin-left: 15px; +} + +.ml-20 { + margin-left: 20px; +} + +.ml-25 { + margin-left: 25px; +} + +.ml-30 { + margin-left: 30px; +} + +.mr-5 { + margin-right: 5px; +} + +.mr-10 { + margin-right: 10px; +} + +.mr-15 { + margin-right: 15px; +} + +.mr-20 { + margin-right: 20px; +} + +.mr-25 { + margin-right: 25px; +} + +.mr-30 { + margin-right: 30px; +} + +.ml-a { + margin-left: auto; +} +/* paddings */ + +/* min-width */ +.w-50 { + width: 50px; +} + +.w-75 { + width: 75px; +} + +.w-100 { + width: 100px; +} + +.w-100p { + width: 100%; +} + +/* min-width */ +.mw-50 { + min-width: 50px; +} + +.mw-75 { + min-width: 75px; +} + +.mw-100 { + min-width: 100px; +} + +.mw-50-imp { + min-width: 50px !important; +} + +.mw-75-imp { + min-width: 75px !important; +} + +.mw-100-imp { + min-width: 100px !important; +} + +.fl-l { + float: left +} + +.fl-r { + float: right +} + +/* colors */ + +$primary: mat-palette($mat-blue,800); + +.color-warn { + color: mat-color($primary, default); +} diff --git a/MyOffice.SPA/src/styles/reset.scss b/MyOffice.SPA/src/styles/reset.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/styles/styles.scss b/MyOffice.SPA/src/styles/styles.scss new file mode 100644 index 0000000..b794247 --- /dev/null +++ b/MyOffice.SPA/src/styles/styles.scss @@ -0,0 +1,18 @@ +@import "src/styles/_variables"; + +// Import functions, variables, and mixins needed by other Bootstrap files +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/variables"; +@import "bootstrap/scss/maps"; +@import "bootstrap/scss/mixins"; + +// Import Bootstrap Reboot +@import "bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files +@import "bootstrap/scss/reboot"; + +@import "bootstrap/scss/containers"; // Add .container and .container-fluid classes +@import "bootstrap/scss/grid"; // Add the grid system + +@import "src/styles/reset"; + +@import "src/styles/global"; diff --git a/MyOffice.SPA/src/test.ts b/MyOffice.SPA/src/test.ts new file mode 100644 index 0000000..ffa954b --- /dev/null +++ b/MyOffice.SPA/src/test.ts @@ -0,0 +1,14 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); diff --git a/MyOffice.SPA/tsconfig.app.json b/MyOffice.SPA/tsconfig.app.json new file mode 100644 index 0000000..90880d5 --- /dev/null +++ b/MyOffice.SPA/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/MyOffice.SPA/tsconfig.json b/MyOffice.SPA/tsconfig.json new file mode 100644 index 0000000..1c3bc1b --- /dev/null +++ b/MyOffice.SPA/tsconfig.json @@ -0,0 +1,35 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "module": "es2020", + "lib": [ + "es2020", + "dom" + ], + "useDefineForClassFields": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/MyOffice.SPA/tsconfig.spec.json b/MyOffice.SPA/tsconfig.spec.json new file mode 100644 index 0000000..c1fdd7e --- /dev/null +++ b/MyOffice.SPA/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/MyOffice.Services/Account/AccountAcl.cs b/MyOffice.Services/Account/AccountAcl.cs new file mode 100644 index 0000000..f1b5eef --- /dev/null +++ b/MyOffice.Services/Account/AccountAcl.cs @@ -0,0 +1,18 @@ +namespace MyOffice.Services.Account; + +using Data.Models.Accounts; + +/// +/// Account access checks shared by service mutations (motions, settings, invites). +/// +public static class AccountAcl +{ + public static AccountAccess? GetUserAccess(Account account, Guid userId) => + account.AccessRights?.FirstOrDefault(x => x.UserId == userId); + + public static bool CanWrite(Account account, Guid userId) => + GetUserAccess(account, userId)?.IsAllowWrite == true; + + public static bool CanManage(Account account, Guid userId) => + GetUserAccess(account, userId)?.IsAllowManage == true; +} diff --git a/MyOffice.Services/Account/AccountMotionBalancing.cs b/MyOffice.Services/Account/AccountMotionBalancing.cs new file mode 100644 index 0000000..66870f9 --- /dev/null +++ b/MyOffice.Services/Account/AccountMotionBalancing.cs @@ -0,0 +1,23 @@ +namespace MyOffice.Services.Account; + +/// +/// Rules for counterparty (transfer) motion amounts when balancing between accounts. +/// +public static class AccountMotionBalancing +{ + /// + /// When the primary motion is income (Plus != 0), balancing is expense on the other account. + /// Otherwise balancing is income on the other account. + /// + public static (decimal Plus, decimal Minus) ResolveAmounts(decimal primaryPlus, decimal amountBalancing) + { + if (primaryPlus != 0) + { + return (0, amountBalancing); + } + + return (amountBalancing, 0); + } + + public static string BalancingItemName(string accountName) => $"+{accountName}"; +} diff --git a/MyOffice.Services/Account/AccountService.Access.cs b/MyOffice.Services/Account/AccountService.Access.cs new file mode 100644 index 0000000..6b8fc4a --- /dev/null +++ b/MyOffice.Services/Account/AccountService.Access.cs @@ -0,0 +1,214 @@ +namespace MyOffice.Services.Account; + +using System.Collections.Generic; + +using Core; +using Core.Extensions; +using Data.Models.Accounts; +using Domain; + +public partial class AccountService +{ + public async Task> AccessInviteAsync( + Guid userId, + Guid accountId, + string email, + bool isAllowWrite, + CancellationToken cancellationToken = default) + { + if (email == null) + throw new ArgumentNullException(nameof(email)); + + var result = new Exec(AccessInviteStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(AccessInviteStatus.account_not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(AccessInviteStatus.forbidden); + } + + if (account.AccessRights!.Any(x => x.User!.Email.EqualsIgnoreCase(email))) + { + return result.Set(AccessInviteStatus.access_exists); + } + + var access = await _accountAccessInviteRepository.GetAsync(userId, email, cancellationToken); + if (access != null) + { + return result.Set(AccessInviteStatus.invite_exists); + } + + var invite = new AccountAccessInvite + { + Id = Guid.NewGuid(), + CreatedOn = DateTime.UtcNow, + UserId = userId, + AccountId = account.Id, + Email = email.SafeTrim()!, + IsAllowWrite = isAllowWrite, + }; + + await _accountAccessInviteRepository.AddAsync(invite, cancellationToken); + + return result.Set(_mapper.Map(invite)); + } + + public async Task> AccessUpdateAsync( + Guid userId, + Guid accountId, + List accesses, + CancellationToken cancellationToken = default) + { + if (accesses == null) + throw new ArgumentNullException(nameof(accesses)); + + var result = new Exec(GeneralExecStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(GeneralExecStatus.forbidden); + } + + foreach (var access in account.AccessRights!) + { + var newAccess = accesses.FirstOrDefault(x => x.UserId == access.UserId); + if (newAccess != null && newAccess.IsAllowWrite != access.IsAllowWrite) + { + access.IsAllowWrite = newAccess.IsAllowWrite; + await _accountAccessRepository.UpdateAsync(access, cancellationToken); + } + } + + account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + + return result.Set(_mapper.Map(account)); + } + + public async Task> AccessDeleteAsync( + Guid userId, + Guid accountId, + Guid accessUserId, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null || !account.AccessRights!.Any() || account.AccessRights!.Count() == 1) + { + return result.Set(GeneralExecStatus.not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(GeneralExecStatus.forbidden); + } + + var access = account.AccessRights!.FirstOrDefault(x => x.UserId == accessUserId); + if (access == null) + { + return result.Set(GeneralExecStatus.not_found); + } + if (access.OwnerId == access.UserId) + { + return result.Set(GeneralExecStatus.not_found); + } + + await _accountAccessRepository.DeleteAsync(access, cancellationToken); + + account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + + return result.Set(_mapper.Map(account)); + + } + + public async Task> InvitesGetAsync(string email, CancellationToken cancellationToken = default) + { + var invites = await _accountAccessInviteRepository.GetActiveAsync(email, cancellationToken); + + return _mapper.Map>(invites); + } + + public async Task> InviteAcceptAsync( + Guid userId, + Guid id, + string name, + CancellationToken cancellationToken = default) + { + var result = new Exec(InviteAcceptStatus.success); + + var invite = await _accountAccessInviteRepository.GetAsync(id, cancellationToken); + if (invite == null + || !invite.Email.EqualsIgnoreCase(_contextProvider.User.Email) + || invite.AcceptedOn.HasValue + || invite.RejectedOn.HasValue + ) + { + return result.Set(InviteAcceptStatus.invite_not_found); + } + + var account = await _accountRepository.GetAsync(invite.UserId, invite.AccountId, cancellationToken); + if (account == null) + { + return result.Set(InviteAcceptStatus.account_not_found); + } + + if (account.AccessRights!.Any(x => x.UserId == userId)) + { + result.Set(InviteAcceptStatus.already_accepted); + } + else + { + await _accountAccessRepository.AddAsync(new AccountAccess + { + UserId = userId, + AccountId = account.Id, + OwnerId = invite.UserId, + Name = name, + IsAllowRead = true, + IsAllowWrite = invite.IsAllowWrite, + IsAllowManage = false, + Type = AccountAccessTypeEnum.external, + }, cancellationToken); + } + + invite.AcceptedOn = DateTime.UtcNow; + await _accountAccessInviteRepository.UpdateAsync(invite, cancellationToken); + + account = await _accountRepository.GetAsync(userId, account.Id, cancellationToken); + + return result.Set(_mapper.Map(account)); + } + + public async Task> InviteRejectAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var invite = await _accountAccessInviteRepository.GetAsync(id, cancellationToken); + if (invite == null + || !invite.Email.EqualsIgnoreCase(_contextProvider.User.Email) + || invite.AcceptedOn.HasValue + || invite.RejectedOn.HasValue + ) + { + return result.Set(GeneralExecStatus.not_found); + } + + invite.RejectedOn = DateTime.UtcNow; + await _accountAccessInviteRepository.UpdateAsync(invite, cancellationToken); + + return result.Set(_mapper.Map(invite)); + } +} diff --git a/MyOffice.Services/Account/AccountService.AccessRights.cs b/MyOffice.Services/Account/AccountService.AccessRights.cs new file mode 100644 index 0000000..ea0f42f --- /dev/null +++ b/MyOffice.Services/Account/AccountService.AccessRights.cs @@ -0,0 +1,15 @@ +namespace MyOffice.Services.Account; + +using Data.Models.Accounts; + +public partial class AccountService +{ + private static AccountAccess? GetUserAccess(Account account, Guid userId) => + AccountAcl.GetUserAccess(account, userId); + + private static bool CanWrite(Account account, Guid userId) => + AccountAcl.CanWrite(account, userId); + + private static bool CanManage(Account account, Guid userId) => + AccountAcl.CanManage(account, userId); +} diff --git a/MyOffice.Services/Account/AccountService.Account.cs b/MyOffice.Services/Account/AccountService.Account.cs new file mode 100644 index 0000000..13f9a47 --- /dev/null +++ b/MyOffice.Services/Account/AccountService.Account.cs @@ -0,0 +1,270 @@ +namespace MyOffice.Services.Account; + +using MyOffice.Core; +using MyOffice.Core.Extensions; +using MyOffice.Data.Models.Accounts; +using MyOffice.Services.Account.Domain; + +public partial class AccountService +{ + public async Task> GetAllAccountsAsync(Guid userId, CancellationToken cancellationToken = default) + { + var accounts = await _accountRepository.GetAllAsync(userId, cancellationToken); + + return accounts.ToDto(_mapper); + } + + public async Task> GetByCategoryAsync(Guid userId, Guid categoryId, CancellationToken cancellationToken = default) + { + var accounts = await _accountRepository.GetByCategoryAsync(userId, categoryId, cancellationToken); + + return accounts.ToDto(_mapper); + } + + public async Task> FindAccountsAsync(Guid userId, string term, CancellationToken cancellationToken = default) + { + var accounts = await _accountRepository.FindAccountsAsync(userId, term, cancellationToken); + + return accounts.ToDto(_mapper); + } + + public List GetByCategoryDetailed(Guid userId, Guid categoryId) + { + return _accountRepository + .GetByCategoryDetailed(userId, categoryId) + .ToDto(_mapper); + } + + public async Task> GetByCategoryDetailedAsync( + Guid userId, + Guid categoryId, + CancellationToken cancellationToken = default + ) + { + var list = await _accountRepository.GetByCategoryDetailedAsync(userId, categoryId, cancellationToken); + return list.ToDto(_mapper); + } + + public Exec GetByIdDetailed(Guid userId, Guid id) + { + var result = new Exec(GeneralExecStatus.success); + + var account = _accountRepository.GetByIdDetailed(userId, id); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + return result.Set(account.ToDto(_mapper));; + } + + public async Task> GetByIdDetailedAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default + ) + { + var result = new Exec(GeneralExecStatus.success); + + var account = await _accountRepository.GetByIdDetailedAsync(userId, id, cancellationToken); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + return result.Set(account.ToDto(_mapper)); + } + + public async Task> AccountAddAsync( + Guid userId, + AccountAdd input, + CancellationToken cancellationToken = default + ) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(AccountAddStatus.success); + + var category = await _accountCategoryRepository.GetAsync(userId, input.CategoryId, cancellationToken); + if (category == null) + { + return result.Set(AccountAddStatus.category_not_found); + } + var currency = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyId, cancellationToken); + if (currency == null) + { + return result.Set(AccountAddStatus.currency_not_found); + } + + var account = new Account + { + Id = Guid.NewGuid(), + Name = input.Name, + CurrencyGlobalId = currency.CurrencyGlobalId, + OwnerId = userId, + }; + + account.Categories = new List + { + new() + { + AccountId = account.Id, + CategoryId = category.Id, + } + }; + account.AccessRights = new List + { + new() + { + AccountId = account.Id, + UserId = userId, + OwnerId = userId, + IsAllowManage = true, + IsAllowRead = true, + IsAllowWrite = true, + } + }; + + if (!await _accountRepository.AddAsync(account, cancellationToken)) + { + return result.Set(AccountAddStatus.failure); + } + // TODO: Add category refactoring + if (!await _accountAccountCategoryRepository.AddAsync(account.Categories.FirstOrDefault()!, cancellationToken)) + { + return result.Set(AccountAddStatus.failure); + } + // TODO: Add access refactoring + if (!await _accountAccessRepository.AddAsync(account.AccessRights.FirstOrDefault()!, cancellationToken)) + { + return result.Set(AccountAddStatus.failure); + } + + // TODO: Add account update AccessRights ??? + var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId); + if (access != null && access.Type.ToString() != input.Type) + { + if (Enum.TryParse(input.Type, out var enumType)) + { + access.Type = enumType; + await _accountAccessRepository.UpdateAsync(access, cancellationToken); + } + } + + return result.Set(account.ToDto(_mapper)); + } + + public async Task> AccountDeleteAsync( + Guid userId, + string id, + CancellationToken cancellationToken = default + ) + { + var result = new Exec(GeneralExecStatus.success); + + var account = await _accountRepository.GetAsync(userId, id.AsGuid(), cancellationToken); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(GeneralExecStatus.forbidden); + } + + if (account.Motions!.Any()) + { + return result.Set(GeneralExecStatus.not_found); + } + + await _accountRepository.DeleteAsync(account, cancellationToken); + result.Set(account.ToDto(_mapper)); + + return result; + } + + public async Task> AccountUpdateAsync( + Guid userId, + Guid accountId, + AccountEdit input, + CancellationToken cancellationToken = default + ) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(AccountEditStatus.success); + + AccountCategory? category = null; + if (input.CategoryId.HasValue) + { + category = await _accountCategoryRepository.GetAsync(userId, input.CategoryId.Value, cancellationToken); + if (category == null) + { + return result.Set(AccountEditStatus.category_not_found); + } + } + + var currency = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyId, cancellationToken); + if (currency == null) + { + return result.Set(AccountEditStatus.currency_not_found); + } + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(AccountEditStatus.not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(AccountEditStatus.forbidden); + } + + account.Name = input.Name; + if (account.CurrencyGlobalId != currency.CurrencyGlobalId) + { + account.CurrencyGlobalId = currency.CurrencyGlobalId; + } + + AccountAccountCategory? addedLink = null; + if (category != null && account.Categories!.All(x => x.CategoryId != category.Id)) + { + addedLink = new AccountAccountCategory + { + AccountId = account.Id, + CategoryId = category.Id, + Category = category, + }; + await _accountAccountCategoryRepository.AddAsync(addedLink, cancellationToken); + } + + if (!await _accountRepository.UpdateAsync(account, cancellationToken)) + { + return result.Set(AccountEditStatus.failure); + } + + var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId); + if (access != null && access.Type.ToString() != input.Type) + { + if (Enum.TryParse(input.Type, out var enumType)) + { + access.Type = enumType; + await _accountAccessRepository.UpdateAsync(access, cancellationToken); + } + } + + // AddAsync detaches the link and does not refresh Account.Categories — keep response in sync. + if (addedLink != null && (account.Categories?.All(x => x.CategoryId != addedLink.CategoryId) ?? true)) + { + account.Categories = (account.Categories ?? Enumerable.Empty()) + .Append(addedLink) + .ToList(); + } + + return result.Set(account.ToDto(_mapper)); + } +} diff --git a/MyOffice.Services/Account/AccountService.AccountCategory.cs b/MyOffice.Services/Account/AccountService.AccountCategory.cs new file mode 100644 index 0000000..028ed68 --- /dev/null +++ b/MyOffice.Services/Account/AccountService.AccountCategory.cs @@ -0,0 +1,147 @@ +namespace MyOffice.Services.Account; + +using MyOffice.Core; +using MyOffice.Data.Models.Accounts; +using MyOffice.Services.Account.Domain; + +public partial class AccountService +{ + public async Task> GetAllCategoriesAsync(Guid userId, CancellationToken cancellationToken = default) + { + var categories = await _accountCategoryRepository.GetAllAsync(userId, cancellationToken); + + return _mapper.Map>(categories); + } + + public async Task> GetCategoryAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var category = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken); + + if (category == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + return result.Set(_mapper.Map(category)); + } + + public async Task> CategoryAddAsync( + Guid userId, + AccountCategoryDto input, + CancellationToken cancellationToken = default) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(GeneralExecStatus.success); + + var category = new AccountCategory + { + Id = Guid.NewGuid(), + UserId = userId, + Name = input.Name, + }; + + if (!await _accountCategoryRepository.AddAsync(category, cancellationToken)) + { + return result.Set(GeneralExecStatus.failure); + } + + return result.Set(_mapper.Map(category)); + } + + public async Task> CategoryUpdateAsync( + Guid userId, + Guid id, + AccountCategoryDto input, + CancellationToken cancellationToken = default) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(GeneralExecStatus.success); + + var exists = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken); + if (exists == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + exists.Name = input.Name; + + if (!await _accountCategoryRepository.UpdateAsync(exists, cancellationToken)) + { + return result.Set(GeneralExecStatus.failure); + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task> CategoryRemoveAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default) + { + var result = new Exec(AccountCategoryRemoveResult.success); + + var exists = await _accountCategoryRepository.GetAsync(userId, id, cancellationToken); + if (exists == null) + { + return result.Set(AccountCategoryRemoveResult.not_found); + } + + if (exists.Accounts?.Any() == true) + { + return result.Set(AccountCategoryRemoveResult.accounts_exists); + } + if (!await _accountCategoryRepository.RemoveAsync(exists, cancellationToken)) + { + return result.Set(AccountCategoryRemoveResult.failure); + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task> AccountCategoryRemoveAsync( + Guid userId, + Guid accountId, + Guid categoryId, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + if (!CanManage(account, userId)) + { + return result.Set(GeneralExecStatus.forbidden); + } + + var links = await _accountAccountCategoryRepository.GetAsync(userId, accountId, categoryId, cancellationToken); + if (links.Count == 0) + { + return result.Set(GeneralExecStatus.not_found); + } + + foreach (var link in links) + { + await _accountAccountCategoryRepository.RemoveAsync(link, cancellationToken); + } + + // Account stays tracked after Remove; filter in-memory so the response is not stale. + account.Categories = account.Categories? + .Where(c => c.CategoryId != categoryId) + .ToList(); + + return result.Set(account.ToDto(_mapper)); + } +} diff --git a/MyOffice.Services/Account/AccountService.Motion.cs b/MyOffice.Services/Account/AccountService.Motion.cs new file mode 100644 index 0000000..ec030ce --- /dev/null +++ b/MyOffice.Services/Account/AccountService.Motion.cs @@ -0,0 +1,212 @@ +namespace MyOffice.Services.Account; + +using System.Collections.Generic; + +using Core; +using Core.Extensions; +using Data.Models.Accounts; +using Domain; +using Item.Domain; + +public partial class AccountService +{ + public async Task, MotionAddStatus>> MotionAddAsync( + Guid userId, + Guid accountId, + MotionAddUpdate motion, + CancellationToken cancellationToken = default + ) + { + if (motion == null) + throw new ArgumentNullException(nameof(motion)); + if (motion.Item == null) + throw new ArgumentNullException(nameof(motion.Item)); + + var result = new Exec, MotionAddStatus>(MotionAddStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(MotionAddStatus.account_not_found); + } + + if (!CanWrite(account, userId)) + { + return result.Set(MotionAddStatus.forbidden); + } + + var itemExec = await _itemService.GetOrCreateAsync(userId, motion.Item, cancellationToken); + if (itemExec.Status != ItemGetOrAddResult.success) + { + return result.Set(MotionAddStatus.failure); + } + + var motionDb = new Motion + { + Id = Guid.NewGuid(), + CreatedOn = DateTime.UtcNow, + DateTime = motion.Date, + AccountId = account.Id, + ItemId = itemExec.Result!.Id, + Description = motion.Description, + AmountPlus = motion.Plus, + AmountMinus = motion.Minus, + }; + + if (!await _motionRepository.AddAsync(motionDb, cancellationToken)) + { + return result.Set(MotionAddStatus.failure); + } + + result.Set(new List()); + result.Result!.Add(_mapper.Map(motionDb)); + + if (motion.AccountId.IsPresent() && motion.AmountBalancing != 0) + { + var accountBalancing = await _accountRepository.GetAsync( + userId, + motion.AccountId!.AsGuid(), + cancellationToken); + if (accountBalancing != null && CanWrite(accountBalancing, userId)) + { + var balancingName = AccountMotionBalancing.BalancingItemName(account.Name); + var itemBalancingExec = await _itemService.GetOrCreateAsync( + userId, + balancingName, + cancellationToken); + var (plus, minus) = AccountMotionBalancing.ResolveAmounts(motion.Plus, motion.AmountBalancing); + var motionBalancing = new Motion + { + Id = Guid.NewGuid(), + CreatedOn = DateTime.UtcNow, + DateTime = motion.Date, + AccountId = accountBalancing.Id, + ItemId = itemBalancingExec.Result!.Id, + Description = motion.Description, + AmountPlus = plus, + AmountMinus = minus, + }; + + if (await _motionRepository.AddAsync(motionBalancing, cancellationToken)) + { + result.Result!.Add(_mapper.Map(motionBalancing)); + } + } + } + + motionDb.Item = itemExec.Result!; + + return result; + } + + public async Task> MotionUpdateAsync( + Guid userId, + Guid accountId, + Guid motionId, + MotionAddUpdate motion, + CancellationToken cancellationToken = default + ) + { + if (motion == null) + throw new ArgumentNullException(nameof(motion)); + if (motion.Item == null) + throw new ArgumentNullException(nameof(motion.Item)); + + var result = new Exec(MotionUpdateStatus.success); + + var exists = await _motionRepository.GetAsync(userId, motionId, cancellationToken); + if (exists == null || exists.AccountId != accountId) + { + return result.Set(MotionUpdateStatus.not_found); + } + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(MotionUpdateStatus.not_found); + } + + if (!CanWrite(account, userId)) + { + return result.Set(MotionUpdateStatus.forbidden); + } + + var itemExec = await _itemService.GetOrCreateAsync(userId, motion.Item, cancellationToken); + if (itemExec.Status != ItemGetOrAddResult.success) + { + return result.Set(MotionUpdateStatus.failure); + } + + exists.Item.Id = itemExec.Result!.Id; + exists.DateTime = motion.Date; + exists.Description = motion.Description; + exists.AmountPlus = motion.Plus; + exists.AmountMinus = motion.Minus; + + if (!await _motionRepository.UpdateAsync(exists, cancellationToken)) + { + return result.Set(MotionUpdateStatus.failure); + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task, GeneralExecStatus>> GetMotionsAsync( + Guid userId, + Guid accountId, + DateTime dateFrom, + DateTime dateTo, + CancellationToken cancellationToken = default + ) + { + var result = new Exec, GeneralExecStatus>(GeneralExecStatus.success); + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + var motions = await _motionRepository.GetByAccountAsync( + accountId, + dateFrom.ToUtc(), + dateTo.ToUtc(), + cancellationToken); + + return result.Set(_mapper.Map>(motions)); + } + + public async Task> MotionRemoveAsync( + Guid userId, + Guid accountId, + Guid motionId, + CancellationToken cancellationToken = default + ) + { + var result = new Exec(MotionDeleteStatus.success); + + var exists = await _motionRepository.GetAsync(userId, motionId, cancellationToken); + if (exists == null || exists.AccountId != accountId) + { + return result.Set(MotionDeleteStatus.not_found); + } + + var account = await _accountRepository.GetAsync(userId, accountId, cancellationToken); + if (account == null) + { + return result.Set(MotionDeleteStatus.not_found); + } + + if (!CanWrite(account, userId)) + { + return result.Set(MotionDeleteStatus.forbidden); + } + + if (!await _motionRepository.RemoveAsync(exists, cancellationToken)) + { + return result.Set(MotionDeleteStatus.failure); + } + + return result.Set(_mapper.Map(exists)); + } +} diff --git a/MyOffice.Services/Account/AccountService.cs b/MyOffice.Services/Account/AccountService.cs new file mode 100644 index 0000000..361d2f1 --- /dev/null +++ b/MyOffice.Services/Account/AccountService.cs @@ -0,0 +1,58 @@ +namespace MyOffice.Services.Account; + +using AutoMapper; +using Microsoft.Extensions.Logging; + +using Identity; +using Data.Repositories.Account; +using Data.Repositories.Currency; +using Data.Repositories.Item; +using Item; + +public partial class AccountService +{ + private ILogger _logger; + private readonly IMapper _mapper; + private readonly IAccountCategoryRepository _accountCategoryRepository; + private readonly IAccountAccessRepository _accountAccessRepository; + private readonly IAccountAccessInviteRepository _accountAccessInviteRepository; + private readonly IAccountRepository _accountRepository; + private readonly ICurrencyRepository _currencyRepository; + private readonly IAccountAccountCategoryRepository _accountAccountCategoryRepository; + private readonly IMotionRepository _motionRepository; + private readonly IItemRepository _itemRepository; + private readonly IItemGlobalRepository _itemGlobalRepository; + private readonly ItemService _itemService; + private readonly IContextProvider _contextProvider; + + public AccountService( + ILogger logger, + IMapper mapper, + IAccountCategoryRepository accountCategoryRepository, + IAccountAccessRepository accountAccessRepository, + IAccountAccessInviteRepository accountAccessInviteRepository, + IAccountRepository accountRepository, + ICurrencyRepository currencyRepository, + IAccountAccountCategoryRepository accountAccountCategoryRepository, + IMotionRepository motionRepository, + IItemRepository itemRepository, + IItemGlobalRepository itemGlobalRepository, + ItemService itemService, + IContextProvider contextProvider + ) + { + _logger = logger; + _mapper = mapper; + _accountCategoryRepository = accountCategoryRepository; + _accountAccessRepository = accountAccessRepository; + _accountAccessInviteRepository = accountAccessInviteRepository; + _accountRepository = accountRepository; + _currencyRepository = currencyRepository; + _accountAccountCategoryRepository = accountAccountCategoryRepository; + _motionRepository = motionRepository; + _itemRepository = itemRepository; + _itemGlobalRepository = itemGlobalRepository; + _itemService = itemService; + _contextProvider = contextProvider; + } +} diff --git a/MyOffice.Services/Account/Domain/AccessInviteStatus.cs b/MyOffice.Services/Account/Domain/AccessInviteStatus.cs new file mode 100644 index 0000000..f058a87 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccessInviteStatus.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain; + +public enum AccessInviteStatus +{ + success, + account_not_found, + access_exists, + invite_exists, + forbidden, +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountAccessDto.cs b/MyOffice.Services/Account/Domain/AccountAccessDto.cs new file mode 100644 index 0000000..a103cd0 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountAccessDto.cs @@ -0,0 +1,53 @@ +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using Data.Models.Accounts; +using MyOffice.Services.Identity; + +public class AccountAccessDto +{ + public Guid AccountId { get; set; } + public AccountDto? Account { get; set; } + + /// + /// User can access to account + /// + public Guid UserId { get; set; } + public UserDto? User { get; set; } + + public bool IsAllowRead { get; set; } + public bool IsAllowWrite { get; set; } + public bool IsAllowManage { get; set; } + public bool IsOwner { get; set; } + public AccountAccessTypeEnum Type { get; set; } + /// + /// Who add access + /// + public Guid OwnerId { get; set; } + public UserDto? Owner { get; set; } + public string? Name { get; set; } +} + +public class AccountAccessDtoProfile : Profile +{ + public AccountAccessDtoProfile() + { + CreateMap() + .AfterMap() + ; + } +} + +public class AccountAccessDtoMappingAction : IMappingAction +{ + private readonly IContextProvider _contextProvider; + public AccountAccessDtoMappingAction(IContextProvider contextProvider) + { + _contextProvider = contextProvider; + } + + public void Process(AccountAccess source, AccountAccessDto destination, ResolutionContext context) + { + destination.IsOwner = destination.OwnerId == _contextProvider.UserId; + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountAccessInviteDto.cs b/MyOffice.Services/Account/Domain/AccountAccessInviteDto.cs new file mode 100644 index 0000000..8145d76 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountAccessInviteDto.cs @@ -0,0 +1,22 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using Data.Models.Accounts; + +public class AccountAccessInviteDto +{ + public string Id { get; set; } = null!; + public string Account { get; set; } = null!; + public bool IsAllowWrite { get; set; } +} + +public class AccountAccessInviteDtoProfile: Profile +{ + public AccountAccessInviteDtoProfile() + { + CreateMap() + .ForMember(x => x.Account, o => o.MapFrom(x => x.Account!.Name)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountAccountCategoryDto.cs b/MyOffice.Services/Account/Domain/AccountAccountCategoryDto.cs new file mode 100644 index 0000000..1867802 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountAccountCategoryDto.cs @@ -0,0 +1,19 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Accounts; + +public class AccountAccountCategoryDto +{ + public Guid CategoryId { get; set; } + public AccountCategoryDto? Category { get; set; } = null!; +} + +public class AccountAccountCategoryDtoProfile: Profile +{ + public AccountAccountCategoryDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountAdd.cs b/MyOffice.Services/Account/Domain/AccountAdd.cs new file mode 100644 index 0000000..1077830 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountAdd.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain +{ + public class AccountAdd + { + public string Name { get; set; } = null!; + public string CurrencyId { get; set; } = null!; + public Guid CategoryId { get; set; } + public string Type { get; set; } = null!; + } +} diff --git a/MyOffice.Services/Account/Domain/AccountAddStatus.cs b/MyOffice.Services/Account/Domain/AccountAddStatus.cs new file mode 100644 index 0000000..b50b045 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountAddStatus.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain +{ + public enum AccountAddStatus + { + success, + failure, + category_not_found, + currency_not_found, + } +} diff --git a/MyOffice.Services/Account/Domain/AccountCategoryDto.cs b/MyOffice.Services/Account/Domain/AccountCategoryDto.cs new file mode 100644 index 0000000..545115a --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountCategoryDto.cs @@ -0,0 +1,24 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Accounts; + +public class AccountCategoryDto +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Name { get; set; } = null!; + public bool AllowDelete { get; set; } +} + +public class AccountCategoryDtoProfile: Profile +{ + public AccountCategoryDtoProfile() + { + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(x => x.Id)) + .ForMember(x => x.AllowDelete, o => o.MapFrom(x => !x.Accounts!.Any())) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountCategoryRemoveResult.cs b/MyOffice.Services/Account/Domain/AccountCategoryRemoveResult.cs new file mode 100644 index 0000000..8f9909c --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountCategoryRemoveResult.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain +{ + public enum AccountCategoryRemoveResult + { + success, + failure, + not_found, + accounts_exists, + } +} diff --git a/MyOffice.Services/Account/Domain/AccountDetailedDto.cs b/MyOffice.Services/Account/Domain/AccountDetailedDto.cs new file mode 100644 index 0000000..4ed39a6 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountDetailedDto.cs @@ -0,0 +1,22 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Core; +using MyOffice.Data.Models.Accounts; + +public class AccountDetailedDto: IDataModelDto +{ + public AccountDto Account { get; set; } = null!; + public decimal TotalPlus { get; set; } + public decimal TotalMinus { get; set; } + public decimal Rest => TotalPlus - TotalMinus; +} + +public class AccountDetailedDtoProfile: Profile +{ + public AccountDetailedDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountDto.cs b/MyOffice.Services/Account/Domain/AccountDto.cs new file mode 100644 index 0000000..ab816d7 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountDto.cs @@ -0,0 +1,79 @@ +namespace MyOffice.Services.Account.Domain; + +using System; +using Data.Models.Accounts; +using MyOffice.Services.Currency.Domain; +using AutoMapper; +using MyOffice.Services.Identity; +using MyOffice.Core; +using MyOffice.Services.Mapper; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] +public class GenerateMappedDtoAttribute: Attribute +{ + public GenerateMappedDtoAttribute() + { + } +} + +[GenerateMappedDto] +public class AccountDto : IDataModelDto +{ + #region Account properties + + public Guid Id { get; set; } + public string CurrencyGlobalId { get; set; } = null!; + public CurrencyGlobalDto? CurrencyGlobal { get; set; } + public string Name { get; set; } = null!; + public Guid OwnerId { get; set; } + public UserDto? Owner { get; set; } + + #endregion Account properties + + #region Dto properties + + public Guid CurrencyId { get; set; } + public CurrencyDto? Currency { get; set; } + public string Type { get; set; } = null!; + public List? Categories { get; set; } + + public bool HasMotions { get; set; } + + #endregion Dto properties + + #region Permissions + + public List? AccessRights { get; set; } + public bool AllowRead { get; set; } + public bool AllowWrite { get; set; } + public bool AllowDelete { get; set; } + public bool AllowManage { get; set; } + + #endregion Permissions +} + +public class AccountDtoProfile : BaseProfile +{ + public AccountDtoProfile() + { + Mapping + .ForMember(x => x.HasMotions, o => o.MapFrom(x => x.Motions!.Any())) + .AfterMap(); + } +} + +public class AccountDtoMappingAction : BaseMappingAction, IMappingAction +{ + public AccountDtoMappingAction(IContextProvider contextProvider) : base(contextProvider) { } + + public void Process(Account source, AccountDto destination, ResolutionContext context) + { + var accessRight = destination.AccessRights?.FirstOrDefault(x => x.UserId == ContextProvider.UserId); + destination.AllowRead = accessRight?.IsAllowRead ?? false; + destination.AllowWrite = accessRight?.IsAllowWrite ?? false; + destination.AllowManage = accessRight?.IsAllowManage ?? false; + destination.AllowDelete = (accessRight?.IsOwner ?? false) && !destination.HasMotions; + destination.Name = accessRight?.Name ?? destination.Name; + destination.Type = accessRight != null ? accessRight.Type.ToString() : destination.Type; + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountEdit.cs b/MyOffice.Services/Account/Domain/AccountEdit.cs new file mode 100644 index 0000000..8a19d69 --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountEdit.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain; + +public class AccountEdit +{ + public string Name { get; set; } = null!; + public string CurrencyId { get; set; } = null!; + public Guid? CategoryId { get; set; } + public Guid? UserId { get; set; } + public string? Type { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/AccountEditStatus.cs b/MyOffice.Services/Account/Domain/AccountEditStatus.cs new file mode 100644 index 0000000..f74f74a --- /dev/null +++ b/MyOffice.Services/Account/Domain/AccountEditStatus.cs @@ -0,0 +1,11 @@ +namespace MyOffice.Services.Account.Domain; + +public enum AccountEditStatus +{ + success, + failure, + not_found, + category_not_found, + currency_not_found, + forbidden, +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/InviteAcceptStatus.cs b/MyOffice.Services/Account/Domain/InviteAcceptStatus.cs new file mode 100644 index 0000000..cfec492 --- /dev/null +++ b/MyOffice.Services/Account/Domain/InviteAcceptStatus.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Account.Domain +{ + public enum InviteAcceptStatus + { + success, + invite_not_found, + account_not_found, + already_accepted, + } +} diff --git a/MyOffice.Services/Account/Domain/ItemCategoryDto.cs b/MyOffice.Services/Account/Domain/ItemCategoryDto.cs new file mode 100644 index 0000000..c7b76a3 --- /dev/null +++ b/MyOffice.Services/Account/Domain/ItemCategoryDto.cs @@ -0,0 +1,23 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Items; + +public class ItemCategoryDto +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public UserDto User { get; set; } = null!; + public string Name { get; set; } = null!; + public List Items { get; set; } = null!; + public bool IsInternal { get; set; } +} + +public class ItemCategoryDtoProfile: Profile +{ + public ItemCategoryDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/ItemDto.cs b/MyOffice.Services/Account/Domain/ItemDto.cs new file mode 100644 index 0000000..357c601 --- /dev/null +++ b/MyOffice.Services/Account/Domain/ItemDto.cs @@ -0,0 +1,27 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Items; + +public class ItemDto +{ + public Guid Id { get; set; } + public string? Name { get; set; } + public bool AllowDelete { get; set; } + + public Guid CategoryId { get; set; } + public ItemCategoryDto? Category { get; set; } +} + +public class ItemDtoProfile: Profile +{ + public ItemDtoProfile() + { + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(x => x.ItemGlobalId)) + .ForMember(x => x.Name, o => o.MapFrom(x => x.ItemGlobal.Name)) + .ForMember(x => x.AllowDelete, o => o.MapFrom(x => !x.Motions!.Any())) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/MotionAddStatus.cs b/MyOffice.Services/Account/Domain/MotionAddStatus.cs new file mode 100644 index 0000000..b06969a --- /dev/null +++ b/MyOffice.Services/Account/Domain/MotionAddStatus.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Services.Account.Domain; + +public enum MotionAddStatus +{ + success, + failure, + account_not_found, + forbidden, +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/MotionAddUpdate.cs b/MyOffice.Services/Account/Domain/MotionAddUpdate.cs new file mode 100644 index 0000000..473dbc9 --- /dev/null +++ b/MyOffice.Services/Account/Domain/MotionAddUpdate.cs @@ -0,0 +1,22 @@ +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; + +public class MotionAddUpdate +{ + public DateTime Date { get; set; } + public string Item { get; set; } = null!; + public string? ItemId { get; set; } = null!; + public string? AccountId { get; set; } = null!; + public string? Description { get; set; } + public decimal Plus { get; set; } + public decimal Minus { get; set; } + public decimal AmountBalancing { get; set; } +} + +public class MotionAddUpdateProfile: Profile +{ + public MotionAddUpdateProfile() + { + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/MotionDeleteStatus.cs b/MyOffice.Services/Account/Domain/MotionDeleteStatus.cs new file mode 100644 index 0000000..d049b94 --- /dev/null +++ b/MyOffice.Services/Account/Domain/MotionDeleteStatus.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Services.Account.Domain; + +public enum MotionDeleteStatus +{ + success, + failure, + not_found, + forbidden, +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/MotionDto.cs b/MyOffice.Services/Account/Domain/MotionDto.cs new file mode 100644 index 0000000..cc2aaed --- /dev/null +++ b/MyOffice.Services/Account/Domain/MotionDto.cs @@ -0,0 +1,27 @@ +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Accounts; + +public class MotionDto +{ + public Guid Id { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime DateTime { get; set; } + public Guid AccountId { get; set; } + public AccountDto Account { get; set; } = null!; + public int ItemId { get; set; } + public ItemDto Item { get; set; } = null!; + public string? Description { get; set; } + public decimal AmountPlus { get; set; } + public decimal AmountMinus { get; set; } + public DateTime? DeletedOn { get; set; } +} + +public class MotionDtoProfile: Profile +{ + public MotionDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/MotionUpdateStatus.cs b/MyOffice.Services/Account/Domain/MotionUpdateStatus.cs new file mode 100644 index 0000000..f7f1985 --- /dev/null +++ b/MyOffice.Services/Account/Domain/MotionUpdateStatus.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Services.Account.Domain; + +public enum MotionUpdateStatus +{ + success, + failure, + not_found, + forbidden, +} \ No newline at end of file diff --git a/MyOffice.Services/Account/Domain/UserDto.cs b/MyOffice.Services/Account/Domain/UserDto.cs new file mode 100644 index 0000000..b6939e9 --- /dev/null +++ b/MyOffice.Services/Account/Domain/UserDto.cs @@ -0,0 +1,28 @@ +/// +namespace MyOffice.Services.Account.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Users; +using MyOffice.Services.Currency.Domain; + +public class UserDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } = null!; + public string Email { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? FullName { get; set; } + public string? Phone { get; set; } + + public string CurrencyId { get; set; } = null!; + public CurrencyGlobalDto? Currency { get; set; } +} + +public class UserDtoProfile : Profile +{ + public UserDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/CurrencyService.cs b/MyOffice.Services/Currency/CurrencyService.cs new file mode 100644 index 0000000..4662147 --- /dev/null +++ b/MyOffice.Services/Currency/CurrencyService.cs @@ -0,0 +1,202 @@ +namespace MyOffice.Services.Currency; + +using AutoMapper; + +using Core; +using Data.Models.Currencies; +using Data.Repositories.Currency; +using Domain; + +public class CurrencyService +{ + private readonly ICurrencyGlobalRepository _currencyGlobalRepository; + private readonly ICurrencyRepository _currencyRepository; + private readonly ICurrencyRateRepository _currencyRateRepository; + private readonly IMapper _mapper; + + public CurrencyService( + ICurrencyGlobalRepository currencyGlobalRepository, + ICurrencyRepository currencyRepository, + ICurrencyRateRepository currencyRateRepository, + IMapper mapper + ) + { + _currencyGlobalRepository = currencyGlobalRepository; + _currencyRepository = currencyRepository; + _currencyRateRepository = currencyRateRepository; + _mapper = mapper; + } + + public async Task> GetGlobalAllAsync(CancellationToken cancellationToken = default) + { + return _mapper.Map>(await _currencyGlobalRepository.GetAllAsync(cancellationToken)); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return _mapper.Map>(await _currencyRepository.GetAllAsync(userId, cancellationToken)); + } + + public async Task> GetAllWithRatesAsync(Guid userId, CancellationToken cancellationToken = default) + { + var list = await _currencyRepository.GetAllAsync(userId, cancellationToken); + + var result = new List(list.Count); + foreach (var currency in list) + { + var rates = await _currencyRateRepository.GetLastRatesAsync(currency.Id, cancellationToken: cancellationToken); + result.Add(new CurrencyWithRateDto + { + Currency = _mapper.Map(currency), + Rate = _mapper.Map(rates.FirstOrDefault()), + }); + } + + return result; + } + + public async Task> CurrencyAddAsync( + Guid userId, + CurrencyDto input, + CancellationToken cancellationToken = default) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(CurrencyAddStatus.success); + + var exists = await _currencyRepository.GetByGlobalCurrencyAsync(userId, input.CurrencyGlobalId, cancellationToken); + if (exists != null) + { + return result.Set(_mapper.Map(exists), CurrencyAddStatus.exists); + } + + var currency = new Currency + { + Id = Guid.NewGuid(), + UserId = userId, + CurrencyGlobalId = input.CurrencyGlobalId, + Name = input.Name, + ShortName = input.ShortName, + }; + + if (!await _currencyRepository.AddAsync(currency, cancellationToken)) + { + return result.Set(CurrencyAddStatus.failed); + } + + return result.Set(_mapper.Map(currency)); + } + + public async Task> CurrencyUpdateAsync( + Guid userId, + Guid currencyId, + CurrencyEdit currency, + CancellationToken cancellationToken = default) + { + if (currency == null) + throw new ArgumentNullException(nameof(currency)); + + var result = new Exec(CurrencyEditStatus.success); + + var exists = await _currencyRepository.GetAsync(userId, currencyId, cancellationToken); + if (exists == null) + { + return result.Set(CurrencyEditStatus.not_found); + } + + exists.Name = currency.Name; + exists.ShortName = currency.ShortName; + exists.IsPrimary = currency.IsPrimary; + + if (!await _currencyRepository.UpdateAsync(exists, cancellationToken)) + { + return result.Set(CurrencyEditStatus.failed); + } + + if (exists.IsPrimary) + { + var primaries = await _currencyRepository.GetPrimariesAsync(userId, cancellationToken); + foreach (var primary in primaries) + { + if (primary.Id != exists.Id) + { + primary.IsPrimary = false; + await _currencyRepository.UpdateAsync(primary, cancellationToken); + } + } + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task> CurrencyRateAddAsync( + Guid userId, + Guid currencyId, + CurrencyRateDto input, + CancellationToken cancellationToken = default) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(CurrencyAddRateStatus.success); + + var currency = await _currencyRepository.GetAsync(userId, currencyId, cancellationToken); + if (currency == null) + { + return result.Set(CurrencyAddRateStatus.not_found); + } + + var currencyRate = new CurrencyRate + { + CurrencyId = currency.Id, + DateTime = input.DateTime.Date, + Rate = input.Rate, + Quantity = input.Quantity, + }; + + var rates = await _currencyRateRepository.GetAtDateAsync(currencyRate.CurrencyId, currencyRate.DateTime, cancellationToken); + var rate = rates.Find(x => x.Rate == currencyRate.Rate); + + if (rate == null) + { + if (!await _currencyRateRepository.AddRateAsync(currencyRate, cancellationToken)) + { + return result.Set(CurrencyAddRateStatus.failed); + } + + // TODO: more logic to fix current rate + if (currencyRate.DateTime.Date == DateTime.UtcNow.Date || !currency.CurrentRateId.HasValue) + { + currency.CurrencyGlobal = null; + currency.CurrentRateId = currencyRate.Id; + await _currencyRepository.UpdateAsync(currency, cancellationToken); + } + + rate = currencyRate; + } + + return result.Set(_mapper.Map(rate)); + } + + public async Task> RemoveAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var currency = await _currencyRepository.GetAsync(userId, id, cancellationToken); + if (currency == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + if (!await _currencyRepository.RemoveAsync(currency, cancellationToken)) + { + return result.Set(GeneralExecStatus.failure); + } + + return result.Set(_mapper.Map(currency)); + } +} diff --git a/MyOffice.Services/Currency/Domain/CurrencyAddRateStatus.cs b/MyOffice.Services/Currency/Domain/CurrencyAddRateStatus.cs new file mode 100644 index 0000000..088ac18 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyAddRateStatus.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Services.Currency.Domain; + +public enum CurrencyAddRateStatus +{ + not_found, + success, + failed, +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyAddStatus.cs b/MyOffice.Services/Currency/Domain/CurrencyAddStatus.cs new file mode 100644 index 0000000..0c375ce --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyAddStatus.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Services.Currency.Domain +{ + public enum CurrencyAddStatus + { + success, + failed, + exists, + } +} diff --git a/MyOffice.Services/Currency/Domain/CurrencyDto.cs b/MyOffice.Services/Currency/Domain/CurrencyDto.cs new file mode 100644 index 0000000..dbedf23 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyDto.cs @@ -0,0 +1,32 @@ +/// +namespace MyOffice.Services.Currency.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Currencies; +using MyOffice.Services.Account.Domain; + +public class CurrencyDto +{ + public Guid Id { get; set; } + public string CurrencyGlobalId { get; set; } = null!; + public CurrencyGlobalDto? CurrencyGlobal { get; set; } + public Guid UserId { get; set; } + public UserDto? User { get; set; } + + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public List? Rates { get; set; } + + public int? CurrentRateId { get; set; } + public CurrencyRateDto? CurrentRate { get; set; } + + public bool IsPrimary { get; set; } +} + +public class CurrencyDtoProfile: Profile +{ + public CurrencyDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyEdit.cs b/MyOffice.Services/Currency/Domain/CurrencyEdit.cs new file mode 100644 index 0000000..ddbdad7 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyEdit.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Services.Currency.Domain; + +public class CurrencyEdit +{ + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public bool IsPrimary { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyEditStatus.cs b/MyOffice.Services/Currency/Domain/CurrencyEditStatus.cs new file mode 100644 index 0000000..6ca41e4 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyEditStatus.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Services.Currency.Domain; + +public enum CurrencyEditStatus +{ + success, + not_found, + failed, +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyGlobalDto.cs b/MyOffice.Services/Currency/Domain/CurrencyGlobalDto.cs new file mode 100644 index 0000000..7cdaa7f --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyGlobalDto.cs @@ -0,0 +1,21 @@ +/// +namespace MyOffice.Services.Currency.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Currencies; + +public class CurrencyGlobalDto +{ + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + public string Symbol { get; set; } = null!; + public int DefaultQuantity { get; set; } +} + +public class CurrencyGlobalDtoProfile: Profile +{ + public CurrencyGlobalDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyRateDto.cs b/MyOffice.Services/Currency/Domain/CurrencyRateDto.cs new file mode 100644 index 0000000..60e2f07 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyRateDto.cs @@ -0,0 +1,24 @@ +/// +namespace MyOffice.Services.Currency.Domain; + +using AutoMapper; +using MyOffice.Data.Models.Currencies; + +public class CurrencyRateDto +{ + public int Id { get; set; } + public Guid CurrencyId { get; set; } + public CurrencyDto? Currency { get; set; } + public DateTime DateTime { get; set; } + public int Quantity { get; set; } + public decimal Rate { get; set; } + public List? Currencies { get; set; } +} + +public class CurrencyRateDtoProfile : Profile +{ + public CurrencyRateDtoProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Services/Currency/Domain/CurrencyWithRateDto.cs b/MyOffice.Services/Currency/Domain/CurrencyWithRateDto.cs new file mode 100644 index 0000000..3aba6e2 --- /dev/null +++ b/MyOffice.Services/Currency/Domain/CurrencyWithRateDto.cs @@ -0,0 +1,9 @@ +namespace MyOffice.Services.Currency.Domain; + +using MyOffice.Data.Models.Currencies; + +public class CurrencyWithRateDto +{ + public CurrencyDto Currency { get; set; } = null!; + public CurrencyRateDto? Rate { get; set; } +} diff --git a/MyOffice.Services/Dashboard/DashboardService.cs b/MyOffice.Services/Dashboard/DashboardService.cs new file mode 100644 index 0000000..a09d500 --- /dev/null +++ b/MyOffice.Services/Dashboard/DashboardService.cs @@ -0,0 +1,160 @@ +namespace MyOffice.Services.Dashboard; + +using Core.Extensions; +using Data.Repositories.Account; +using Data.Repositories.Currency; +using MyOffice.Data.Models.Accounts; +using MyOffice.Services.Dashboard.Domain; + +public class DashboardService +{ + private readonly IAccountRepository _accountRepository; + private readonly ICurrencyRateRepository _currencyRateRepository; + + public DashboardService( + IAccountRepository accountRepository, + ICurrencyRateRepository currencyRateRepository + ) + { + _accountRepository = accountRepository; + _currencyRateRepository = currencyRateRepository; + } + + public async Task GetDashboardRestDataAsync( + Guid userId, + CancellationToken cancellationToken = default + ) + { + var rests = await _accountRepository.GetRestAtDateAsync(userId, DateTime.UtcNow, cancellationToken); + + return new DashboardData + { + Balance = rests + .Where(x => (x.Type == AccountAccessTypeEnum.balance || x.Type == AccountAccessTypeEnum.credit) && x.CurrencyRate.HasValue) + .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), + + BalanceDebit = rests + .Where(x => x.Type == AccountAccessTypeEnum.balance && x.CurrencyRate.HasValue) + .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), + + BalanceCredit = rests + .Where(x => x.Type == AccountAccessTypeEnum.credit && x.CurrencyRate.HasValue) + .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), + + BalanceRests = rests + .Where(x => x.Type == AccountAccessTypeEnum.balance) + .Where(x => x.CurrencyRate.HasValue && x.Balance.HasValue && x.Balance != 0) + .Select(x => new DashboardRestData + { + Id = x.Id, + Name = x.Name, + Balance = x.Balance, + CurrencyName = x.CurrencyName!, + CurrencyShortName = x.CurrencyShortName!, + CurrencyRate = x.CurrencyRate, + CurrencyQuantity = x.CurrencyQuantity, + }) + .OrderByDescending(x => x.BalanceAtRate) + .ToList(), + }; + } + + public async Task GetDashboardIncomeDataAsync( + Guid userId, + DateTime from, + DateTime to, + Guid? category, + CancellationToken cancellationToken = default + ) + { + var data = category.HasValue + ? await _accountRepository.GetIncomeByCategoryAsync(userId, category.Value, from.ToUtc(), to.ToUtc(), cancellationToken) + : await _accountRepository.GetIncomeByCategoriesAsync(userId, from.ToUtc(), to.ToUtc(), cancellationToken); + + data = data + .Where(x => x.Amount != 0) + .ToList(); + + var currencies = data + .Select(x => x.CurrencyId) + .Distinct() + .ToList(); + + var rates = (await _currencyRateRepository.GetLastRatesAsync(userId, currencies, to.ToUtc(), cancellationToken)) + .ToDictionary(x => x.Key, x => x.Value.Rate * x.Value.Quantity); + + return BuildIncomeData(data, rates); + } + + public async Task GetDashboardOutcomeDataAsync( + Guid userId, + DateTime from, + DateTime to, + Guid? category, + CancellationToken cancellationToken = default + ) + { + var data = category.HasValue + ? await _accountRepository.GetOutcomeByCategoryAsync(userId, category.Value, from.ToUtc(), to.ToUtc(), cancellationToken) + : await _accountRepository.GetOutcomeByCategoriesAsync(userId, from.ToUtc(), to.ToUtc(), cancellationToken); + + data = data + .Where(x => x.Amount != 0) + .ToList(); + + var currencies = data + .Select(x => x.CurrencyId) + .Distinct() + .ToList(); + + var rates = (await _currencyRateRepository.GetLastRatesAsync(userId, currencies, to.ToUtc(), cancellationToken)) + .ToDictionary(x => x.Key, x => x.Value.Rate * x.Value.Quantity); + + return BuildIncomeData(data, rates); + } + + private static DashboardIncomeData BuildIncomeData( + List data, + Dictionary rates + ) + { + return new DashboardIncomeData + { + Data = data.Select(x => new + { + Id = x.Id.ToShort(), + Name = x.Name, + ValueRaw = x.Amount, + Value = x.Amount * rates.FirstOrDefault(r => r.Key == x.CurrencyId).Value, + }) + .GroupBy(x => new { x.Id, x.Name }) + .Select(x => new DashboardIncomeDataItem + { + Id = x.Key.Id, + Name = x.Key.Name, + Value = x.Sum(s => s.Value), + ValueRaw = x.Sum(s => s.ValueRaw), + }) + .OrderBy(x => x.Name) + .ToList(), + + Details = data.Select(x => new + { + Id = x.CurrencyId, + Name = x.CurrencyName, + ValueRaw = x.Amount, + Value = x.Amount * rates.FirstOrDefault(r => r.Key == x.CurrencyId).Value, + }) + .GroupBy(x => new { x.Id, x.Name }) + .Select(x => new DashboardIncomeDataItem + { + Id = x.Key.Id, + Name = x.Key.Name, + Value = x.Sum(s => s.Value), + ValueRaw = x.Sum(s => s.ValueRaw), + }) + .OrderBy(x => x.Name) + .ToList(), + }; + } +} diff --git a/MyOffice.Services/Dashboard/Domain/DashboardData.cs b/MyOffice.Services/Dashboard/Domain/DashboardData.cs new file mode 100644 index 0000000..8f34850 --- /dev/null +++ b/MyOffice.Services/Dashboard/Domain/DashboardData.cs @@ -0,0 +1,43 @@ +namespace MyOffice.Services.Dashboard.Domain; + +using System.Collections.Generic; + +public class DashboardData +{ + public decimal IncomeLast { get; set; } + public decimal IncomePrevious { get; set; } + public decimal OutcomeLast { get; set; } + public decimal OutcomePrevious { get; set; } + public decimal? Balance { get; set; } + public decimal? BalanceDebit { get; set; } + public decimal? BalanceCredit { get; set; } + public List BalanceRests { get; set; } = null!; +} + +public class DashboardRestData +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string CurrencyName { get; set; } = null!; + public string CurrencyShortName { get; set; } = null!; + public decimal? Balance { get; set; } + public decimal? CurrencyRate { get; set; } + public decimal? CurrencyQuantity { get; set; } + + public decimal? BalanceAtRate => Balance * (CurrencyRate * CurrencyQuantity); +} + +public class DashboardIncomeData +{ + public List Data { get; set; } = null!; + public List Details { get; set; } = null!; +} + +public class DashboardIncomeDataItem +{ + public string? Id { get; set; } + public string Currency { get; set; } = null!; + public string Name { get; set; } = null!; + public decimal Value { get; set; } + public decimal ValueRaw { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Services/Identity/IContextProvider.cs b/MyOffice.Services/Identity/IContextProvider.cs new file mode 100644 index 0000000..b57fd11 --- /dev/null +++ b/MyOffice.Services/Identity/IContextProvider.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Identity; + +using Data.Models.Users; + +public interface IContextProvider +{ + User User { get; } + Guid UserId { get; } +} + diff --git a/MyOffice.Services/Item/Domain/ItemCategoryRemoveResult.cs b/MyOffice.Services/Item/Domain/ItemCategoryRemoveResult.cs new file mode 100644 index 0000000..e5d73d6 --- /dev/null +++ b/MyOffice.Services/Item/Domain/ItemCategoryRemoveResult.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Item.Domain +{ + public enum ItemCategoryRemoveResult + { + success, + failure, + not_found, + accounts_exists, + } +} diff --git a/MyOffice.Services/Item/Domain/ItemGetOrAddResult.cs b/MyOffice.Services/Item/Domain/ItemGetOrAddResult.cs new file mode 100644 index 0000000..f3a22b0 --- /dev/null +++ b/MyOffice.Services/Item/Domain/ItemGetOrAddResult.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Services.Item.Domain +{ + public enum ItemGetOrAddResult + { + success, + failure, + not_found, + accounts_exists, + } +} diff --git a/MyOffice.Services/Item/ItemService.cs b/MyOffice.Services/Item/ItemService.cs new file mode 100644 index 0000000..585d180 --- /dev/null +++ b/MyOffice.Services/Item/ItemService.cs @@ -0,0 +1,264 @@ +namespace MyOffice.Services.Item; + +using AutoMapper; +using Domain; +using MyOffice.Core; +using MyOffice.Data.Models.Items; +using MyOffice.Data.Repositories.Item; +using MyOffice.Services.Account.Domain; + +public class ItemService +{ + private readonly IItemCategoryRepository _itemCategoryRepository; + private readonly IItemRepository _itemRepository; + private readonly IItemGlobalRepository _itemGlobalRepository; + private readonly IMapper _mapper; + + public ItemService( + IItemCategoryRepository itemCategoryRepository, + IItemRepository itemRepository, + IItemGlobalRepository itemGlobalRepository, + IMapper mapper + ) + { + _itemCategoryRepository = itemCategoryRepository; + _itemRepository = itemRepository; + _itemGlobalRepository = itemGlobalRepository; + _mapper = mapper; + } + + public async Task> GetAllCategoriesAsync(Guid userId, CancellationToken cancellationToken = default) + { + var result = await _itemCategoryRepository.GetAllAsync(userId, cancellationToken); + if (result.All(x => x.Id != userId)) + { + var category = new ItemCategory + { + Id = userId, + UserId = userId, + Name = "UnCategorized", + }; + await _itemCategoryRepository.AddAsync(category, cancellationToken); + + result.Add((await _itemCategoryRepository.GetAsync(userId, userId, cancellationToken))!); + } + return _mapper.Map>(result); + } + + public async Task> CategoryAddAsync( + Guid userId, + ItemCategoryDto input, + CancellationToken cancellationToken = default) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var result = new Exec(GeneralExecStatus.success); + + var category = new ItemCategory + { + Id = Guid.NewGuid(), + UserId = userId, + Name = input.Name, + IsInternal = input.IsInternal, + }; + + if (!await _itemCategoryRepository.AddAsync(category, cancellationToken)) + { + return result.Set(GeneralExecStatus.failure); + } + + return result.Set(_mapper.Map(category)); + } + + public async Task> CategoryUpdateAsync( + Guid userId, + Guid id, + ItemCategoryDto category, + CancellationToken cancellationToken = default) + { + if (category == null) + throw new ArgumentNullException(nameof(category)); + + var result = new Exec(GeneralExecStatus.success); + + var exists = await _itemCategoryRepository.GetAsync(userId, id, cancellationToken); + if (exists == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + exists.Name = category.Name; + exists.IsInternal = category.IsInternal; + + if (!await _itemCategoryRepository.UpdateAsync(exists, cancellationToken)) + { + return result.Set(GeneralExecStatus.failure); + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task> CategoryRemoveAsync( + Guid userId, + Guid id, + CancellationToken cancellationToken = default) + { + var result = new Exec(ItemCategoryRemoveResult.success); + + var exists = await _itemCategoryRepository.GetAsync(userId, id, cancellationToken); + if (exists == null) + { + return result.Set(ItemCategoryRemoveResult.not_found); + } + + if (exists.Items.Any()) + { + return result.Set(ItemCategoryRemoveResult.accounts_exists); + } + if (!await _itemCategoryRepository.RemoveAsync(exists, cancellationToken)) + { + return result.Set(ItemCategoryRemoveResult.failure); + } + + return result.Set(_mapper.Map(exists)); + } + + public async Task> GetAllAsync(Guid userId, CancellationToken cancellationToken = default) + { + return _mapper.Map>(await _itemRepository.GetAllAsync(userId, cancellationToken)); + } + + public async Task> GetByCategoryAsync( + Guid userId, + Guid categoryId, + CancellationToken cancellationToken = default) + { + return _mapper.Map>(await _itemRepository.GetByCategoryAsync(userId, categoryId, cancellationToken)); + } + + public async Task> GetByUncategorizedAsync(Guid userId, CancellationToken cancellationToken = default) + { + var itemGlobals = await _itemGlobalRepository.GetAvailableToUserAsync(userId, cancellationToken); + foreach (var itemGlobal in itemGlobals) + { + await GetOrCreateAsync(userId, itemGlobal, cancellationToken); + } + + return await GetByCategoryAsync(userId, userId, cancellationToken); + } + + public async Task> UpdateAsync( + Guid userId, + Guid motionId, + Guid categoryId, + CancellationToken cancellationToken = default) + { + var result = new Exec(GeneralExecStatus.success); + + var item = await _itemRepository.GetByGlobalAsync(userId, motionId, cancellationToken); + if (item == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + item.CategoryId = categoryId; + await _itemRepository.UpdateAsync(item, cancellationToken); + + return result.Set(_mapper.Map(item)); + } + + public async Task> GetOrCreateAsync( + Guid userId, + string name, + CancellationToken cancellationToken = default + ) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var result = new Exec(ItemGetOrAddResult.success); + + var itemGlobal = await _itemGlobalRepository.GetByNameAsync(name, cancellationToken); + if (itemGlobal == null) + { + itemGlobal = new ItemGlobal + { + Id = Guid.NewGuid(), + Name = name.Trim(), + }; + + if (!await _itemGlobalRepository.AddAsync(itemGlobal, cancellationToken)) + { + return result.Set(ItemGetOrAddResult.failure); + } + + return await GetOrCreateAsync(userId, itemGlobal, cancellationToken); + } + + return await GetOrCreateAsync(userId, itemGlobal, cancellationToken); + } + + private async Task> GetOrCreateAsync( + Guid userId, + ItemGlobal itemGlobal, + CancellationToken cancellationToken = default + ) + { + if (itemGlobal == null) + throw new ArgumentNullException(nameof(itemGlobal)); + + var result = new Exec(ItemGetOrAddResult.success); + + var item = await _itemRepository.GetByGlobalAsync(userId, itemGlobal.Id, cancellationToken); + if (item == null) + { + item = new Item + { + CategoryId = userId, + ItemGlobalId = itemGlobal.Id, + }; + await _itemRepository.AddAsync(item, cancellationToken); + } + + item.ItemGlobal = itemGlobal; + return result.Set(item); + } + + public async Task> FindItemsAsync( + Guid userId, + string term, + int limit = 15, + CancellationToken cancellationToken = default) + { + if (term == null) + throw new ArgumentNullException(nameof(term)); + + return _mapper.Map>(await _itemRepository.FindAsync(userId, term, limit, cancellationToken)); + } + + public async Task UpdateItemsCategoryAsync( + Guid userId, + Guid categoryId, + List items, + CancellationToken cancellationToken = default) + { + var category = await _itemCategoryRepository.GetAsync(userId, categoryId, cancellationToken); + if (category == null) + { + return GeneralExecStatus.not_found; + } + + foreach (var item in items) + { + var dbItem = await _itemRepository.GetByGlobalAsync(userId, item, cancellationToken); + if (dbItem != null) + { + dbItem.CategoryId = category.Id; + await _itemRepository.UpdateAsync(dbItem, cancellationToken); + } + } + + return GeneralExecStatus.success; + } +} diff --git a/MyOffice.Services/Mapper/AutomapperExtensions.cs b/MyOffice.Services/Mapper/AutomapperExtensions.cs new file mode 100644 index 0000000..9cee4be --- /dev/null +++ b/MyOffice.Services/Mapper/AutomapperExtensions.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using MyOffice.Core; + +namespace MyOffice.Services; + +public static class AutomapperExtensions +{ + public static List ToDto(this IEnumerable list, IMapper mapper) + { + return mapper.Map>(list); + } + + public static TTo ToDto(this IDataModel item, IMapper mapper) + { + return mapper.Map(item); + } +} diff --git a/MyOffice.Services/Mapper/BaseMappingAction.cs b/MyOffice.Services/Mapper/BaseMappingAction.cs new file mode 100644 index 0000000..c518055 --- /dev/null +++ b/MyOffice.Services/Mapper/BaseMappingAction.cs @@ -0,0 +1,12 @@ +using MyOffice.Services.Identity; + +namespace MyOffice.Services.Mapper; + +public class BaseMappingAction +{ + protected readonly IContextProvider ContextProvider; + public BaseMappingAction(IContextProvider contextProvider) + { + ContextProvider = contextProvider; + } +} diff --git a/MyOffice.Services/Mapper/BaseProfile.cs b/MyOffice.Services/Mapper/BaseProfile.cs new file mode 100644 index 0000000..e1932b8 --- /dev/null +++ b/MyOffice.Services/Mapper/BaseProfile.cs @@ -0,0 +1,13 @@ +using AutoMapper; + +namespace MyOffice.Services.Mapper; + +public class BaseProfile : Profile +{ + protected IMappingExpression Mapping; + + public BaseProfile() + { + Mapping = CreateMap(); + } +} diff --git a/MyOffice.Services/Mapper/UserIdResolver.cs b/MyOffice.Services/Mapper/UserIdResolver.cs new file mode 100644 index 0000000..cd87127 --- /dev/null +++ b/MyOffice.Services/Mapper/UserIdResolver.cs @@ -0,0 +1,21 @@ +namespace MyOffice.Services.Mapper; + +using AutoMapper; +using Identity; + +public class UserIdResolver : IValueResolver +{ + private readonly IContextProvider _contextProvider; + + public UserIdResolver( + IContextProvider contextProvider + ) + { + _contextProvider = contextProvider; + } + + public Guid Resolve(object source, object destination, Guid member, ResolutionContext context) + { + return _contextProvider.UserId; + } +} \ No newline at end of file diff --git a/MyOffice.Services/MyOffice.Services.csproj b/MyOffice.Services/MyOffice.Services.csproj new file mode 100644 index 0000000..f4e8c86 --- /dev/null +++ b/MyOffice.Services/MyOffice.Services.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/MyOffice.Services/Notifications/EmailNotificationService.cs b/MyOffice.Services/Notifications/EmailNotificationService.cs new file mode 100644 index 0000000..6374e9b --- /dev/null +++ b/MyOffice.Services/Notifications/EmailNotificationService.cs @@ -0,0 +1,80 @@ +namespace MyOffice.Services.Notifications; + +using MyOffice.Core; +using MyOffice.Data.Models.Notifications; +using MyOffice.Data.Models.Users; +using MyOffice.Data.Models.Verifications; +using MyOffice.Data.Repositories.Item; +using MyOffice.Services.Verifications; + +public class EmailNotificationService +{ + private readonly VerificationService _verificationService; + + public EmailNotificationService( + VerificationService verificationService + ) + { + _verificationService = verificationService; + } + + public void PasswordResetEmail(User user) + { + var code = _verificationService.Add( + user, + user.Email, + VerificationCodeTemplateEnum.password_restore, + VerificationCodeTypeEnum.email + ); + + var subject = ""; + var body = ""; + } +} + +public class TemplateSevice +{ + private readonly IEmailTemplateRepository _emailTemplateRepository; + + public TemplateSevice( + IEmailTemplateRepository emailTemplateRepository + ) + { + _emailTemplateRepository = emailTemplateRepository; + } + + public Exec GetTemplate( + EmailTemplateEnum template, + Dictionary tokens + ) + { + var result = new Exec(GeneralExecStatus.success); + + var emailTemplate = _emailTemplateRepository.Get(template); + if (emailTemplate == null) + { + return result.Set(GeneralExecStatus.not_found); + } + + result.Set(new EmailTemplateFormated + { + Subject = emailTemplate.Subject, + Body = emailTemplate.Template, + }); + + return result; + } +} + +public class EmailTemplateFormated +{ + public string Sender { get; set; } + public string SenderName { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} + +public interface IEmailSender +{ + //Exec Send(string from, string[] to, string subject, string body); +} \ No newline at end of file diff --git a/MyOffice.Services/Users/Domain/AddUserExternalStatusEnum.cs b/MyOffice.Services/Users/Domain/AddUserExternalStatusEnum.cs new file mode 100644 index 0000000..b90155f --- /dev/null +++ b/MyOffice.Services/Users/Domain/AddUserExternalStatusEnum.cs @@ -0,0 +1,8 @@ +namespace MyOffice.Services.Users.Domain; + +public enum AddUserExternalStatusEnum +{ + success, + externalid_used, + user_not_valid +} \ No newline at end of file diff --git a/MyOffice.Services/Users/UserService.cs b/MyOffice.Services/Users/UserService.cs new file mode 100644 index 0000000..115b8af --- /dev/null +++ b/MyOffice.Services/Users/UserService.cs @@ -0,0 +1,96 @@ +namespace MyOffice.Services.Users; + +using Domain; +using Core; +using Data.Models.Users; +using Data.Repositories.Users; + +public class UserService +{ + private readonly IUserRepository _userRepository; + private readonly IUserExternalRepository _userExternalRepository; + private readonly SemaphoreSlim _addExternalLock = new(1, 1); + + public UserService( + IUserRepository userRepository, + IUserExternalRepository userExternalRepository + ) + { + _userRepository = userRepository; + _userExternalRepository = userExternalRepository; + } + + public async Task Get(Guid id) + { + return await _userRepository.GetUserAsync(id); + } + + public async Task> GetUserExternals(Guid userId) + { + return await _userExternalRepository.GetUserExternalsByUserIdAsync(userId); + } + + public async Task GetUserExternal(Guid userId, string provider) + { + return await _userExternalRepository.GetByUserIdAsync(userId, provider); + } + + public async Task> RemoveUserExternal( + Guid userId, + string provider, + CancellationToken cancellationToken = default) + { + var result = new Exec(); + + var userExternal = await _userExternalRepository.GetByUserIdAsync(userId, provider); + if (userExternal == null) return result.Set(GeneralExecStatus.not_found); + + await _userExternalRepository.RemoveUserExternalAsync(userExternal, cancellationToken); + + return result.Set(userExternal); + } + + public async Task> AddUserExternalAsync( + Guid userId, + string provider, + string externalId, + string email, + CancellationToken cancellationToken = default + ) + { + var result = new Exec(AddUserExternalStatusEnum.success); + + var externalLogin = await _userExternalRepository.GetByUserIdAsync(userId, provider); + + if (externalLogin != null) return result.Set(externalLogin); + + await _addExternalLock.WaitAsync(cancellationToken); + try + { + externalLogin = await _userExternalRepository.GetByUserIdAsync(userId, provider); + if (externalLogin != null) return result.Set(externalLogin); + + externalLogin = await _userExternalRepository.GetByExternalIdAsync(externalId, provider); + if (externalLogin != null) return result.Set(AddUserExternalStatusEnum.externalid_used); + + var user = await _userRepository.GetByUserUserNameAsync(email); + if (user?.Id != userId) return result.Set(AddUserExternalStatusEnum.user_not_valid); + + externalLogin = new UserExternal + { + UserId = userId, + CreatedOn = DateTime.UtcNow, + Provider = provider.ToLower(), + ExternalId = externalId, + Email = email + }; + await _userExternalRepository.AddUserExternalAsync(externalLogin, cancellationToken); + } + finally + { + _addExternalLock.Release(); + } + + return result; + } +} diff --git a/MyOffice.Services/Validators/ContextValidator.cs b/MyOffice.Services/Validators/ContextValidator.cs new file mode 100644 index 0000000..f844d8a --- /dev/null +++ b/MyOffice.Services/Validators/ContextValidator.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Services.Validators +{ + public static class ContextValidator + { + } +} diff --git a/MyOffice.Services/Verifications/VerificationService.cs b/MyOffice.Services/Verifications/VerificationService.cs new file mode 100644 index 0000000..988b11b --- /dev/null +++ b/MyOffice.Services/Verifications/VerificationService.cs @@ -0,0 +1,51 @@ +namespace MyOffice.Services.Verifications; + +using MyOffice.Core.Helpers; +using MyOffice.Data.Models.Users; +using MyOffice.Data.Models.Verifications; +using MyOffice.Data.Repositories.Item; + +public class VerificationService +{ + private readonly TimeSpan _defaultExpires = TimeSpan.FromDays(1); + + private readonly IVerificationCodeRepository _verificationCodeRepository; + + public VerificationService( + IVerificationCodeRepository verificationCodeRepository + ) + { + _verificationCodeRepository = verificationCodeRepository; + } + + public VerificationCode Add( + User user, + string destination, + VerificationCodeTemplateEnum template, + VerificationCodeTypeEnum type, + DateTime? expiresOn = null, + string? metadata = null + ) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + + var verificationCode = new VerificationCode + { + UserId = user.Id, + Code = RandomizationHelper.Generate(40), + CreatedOn = DateTime.UtcNow, + ExpiresOn = expiresOn ?? DateTime.UtcNow.Add(_defaultExpires), + Template = template.ToString(), + DestinationType = type.ToString(), + Destination = destination, + Metadata = metadata, + }; + + _verificationCodeRepository.Add(verificationCode); + + return verificationCode; + } +} \ No newline at end of file diff --git a/MyOffice.Shared/Configuration.cs b/MyOffice.Shared/Configuration.cs new file mode 100644 index 0000000..5d75f75 --- /dev/null +++ b/MyOffice.Shared/Configuration.cs @@ -0,0 +1,48 @@ +namespace MyOffice.Shared; + +using Microsoft.Extensions.Configuration; + +public static class SharedConfiguration +{ + public const string BaseFileName = "appsettings.shared.json"; + + /// + /// Adds shared JSON sources to an existing configuration builder (base + environment overlay). + /// + public static IConfigurationBuilder AddSharedAppSettings( + this IConfigurationBuilder builder, + string environmentName, + string? basePath = null, + bool optional = false, + bool reloadOnChange = true + ) + { + basePath ??= AppContext.BaseDirectory; + + builder + .AddJsonFile(Path.Combine(basePath, BaseFileName), optional: optional, reloadOnChange: reloadOnChange) + .AddJsonFile( + Path.Combine(basePath, $"appsettings.shared.{environmentName}.json"), + optional: true, + reloadOnChange: reloadOnChange); + + return builder; + } + + /// + /// Builds a standalone configuration for design-time tools (EF migrations, etc.). + /// Layering: base shared → environment shared → environment variables. + /// + public static IConfiguration Build(string? environmentName = null) + { + environmentName ??= Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") + ?? "Production"; + + return new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddSharedAppSettings(environmentName, optional: false, reloadOnChange: false) + .AddEnvironmentVariables() + .Build(); + } +} diff --git a/MyOffice.Shared/MyOffice.Shared.csproj b/MyOffice.Shared/MyOffice.Shared.csproj new file mode 100644 index 0000000..cc156eb --- /dev/null +++ b/MyOffice.Shared/MyOffice.Shared.csproj @@ -0,0 +1,40 @@ + + + + net10.0 + enable + enable + + + + + + + + + Always + true + + + Always + true + PreserveNewest + + + Always + true + + + Always + true + PreserveNewest + + + + + + + + + + diff --git a/MyOffice.Shared/appsettings.shared.Docker.json b/MyOffice.Shared/appsettings.shared.Docker.json new file mode 100644 index 0000000..86dc9f0 --- /dev/null +++ b/MyOffice.Shared/appsettings.shared.Docker.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "npgsql": "Host=postgres;Port=5432;Database=myoffice;Username=myoffice;Password=myoffice" + } +} diff --git a/MyOffice.Shared/appsettings.shared.json b/MyOffice.Shared/appsettings.shared.json new file mode 100644 index 0000000..4c8cd57 --- /dev/null +++ b/MyOffice.Shared/appsettings.shared.json @@ -0,0 +1,7 @@ +{ + "ConnectionStrings": { + "sqlite": "Filename=AppDbContext.Dev.db", + "mssql": "", + "npgsql": "" + } +} \ No newline at end of file diff --git a/MyOffice.Tests/Account/AccountAclTests.cs b/MyOffice.Tests/Account/AccountAclTests.cs new file mode 100644 index 0000000..f931d5c --- /dev/null +++ b/MyOffice.Tests/Account/AccountAclTests.cs @@ -0,0 +1,65 @@ +using MyOffice.Data.Models.Accounts; +using MyOffice.Services.Account; +using AccountEntity = MyOffice.Data.Models.Accounts.Account; + +namespace MyOffice.Tests.Accounts; + +public class AccountAclTests +{ + private static AccountEntity CreateAccount(Guid userId, bool write, bool manage) + { + return new AccountEntity + { + Id = Guid.NewGuid(), + Name = "Cash", + AccessRights = + [ + new AccountAccess + { + UserId = userId, + IsAllowRead = true, + IsAllowWrite = write, + IsAllowManage = manage, + } + ] + }; + } + + [Fact] + public void CanWrite_true_when_flag_set() + { + var userId = Guid.NewGuid(); + var account = CreateAccount(userId, write: true, manage: false); + + Assert.True(AccountAcl.CanWrite(account, userId)); + Assert.False(AccountAcl.CanManage(account, userId)); + } + + [Fact] + public void CanManage_true_when_flag_set() + { + var userId = Guid.NewGuid(); + var account = CreateAccount(userId, write: false, manage: true); + + Assert.False(AccountAcl.CanWrite(account, userId)); + Assert.True(AccountAcl.CanManage(account, userId)); + } + + [Fact] + public void CanWrite_false_for_other_user() + { + var account = CreateAccount(Guid.NewGuid(), write: true, manage: true); + + Assert.False(AccountAcl.CanWrite(account, Guid.NewGuid())); + Assert.False(AccountAcl.CanManage(account, Guid.NewGuid())); + } + + [Fact] + public void CanWrite_false_when_access_rights_null() + { + var account = new AccountEntity { Id = Guid.NewGuid(), Name = "X", AccessRights = null }; + + Assert.False(AccountAcl.CanWrite(account, Guid.NewGuid())); + Assert.False(AccountAcl.CanManage(account, Guid.NewGuid())); + } +} diff --git a/MyOffice.Tests/Account/AccountMotionBalancingTests.cs b/MyOffice.Tests/Account/AccountMotionBalancingTests.cs new file mode 100644 index 0000000..4a3163a --- /dev/null +++ b/MyOffice.Tests/Account/AccountMotionBalancingTests.cs @@ -0,0 +1,30 @@ +using MyOffice.Services.Account; + +namespace MyOffice.Tests.Accounts; + +public class AccountMotionBalancingTests +{ + [Fact] + public void ResolveAmounts_income_primary_creates_expense_on_balancing() + { + var (plus, minus) = AccountMotionBalancing.ResolveAmounts(primaryPlus: 100m, amountBalancing: 100m); + + Assert.Equal(0m, plus); + Assert.Equal(100m, minus); + } + + [Fact] + public void ResolveAmounts_expense_primary_creates_income_on_balancing() + { + var (plus, minus) = AccountMotionBalancing.ResolveAmounts(primaryPlus: 0m, amountBalancing: 50m); + + Assert.Equal(50m, plus); + Assert.Equal(0m, minus); + } + + [Fact] + public void BalancingItemName_prefixes_account_name() + { + Assert.Equal("+Wallet", AccountMotionBalancing.BalancingItemName("Wallet")); + } +} diff --git a/MyOffice.Tests/Account/MotionAddAclTests.cs b/MyOffice.Tests/Account/MotionAddAclTests.cs new file mode 100644 index 0000000..7088c74 --- /dev/null +++ b/MyOffice.Tests/Account/MotionAddAclTests.cs @@ -0,0 +1,173 @@ +using AutoMapper; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using MyOffice.Data.Models.Accounts; +using MyOffice.Data.Models.Items; +using MyOffice.Data.Repositories.Account; +using MyOffice.Data.Repositories.Currency; +using MyOffice.Data.Repositories.Item; +using MyOffice.Services.Account; +using MyOffice.Services.Account.Domain; +using MyOffice.Services.Identity; +using MyOffice.Services.Item; + +using AccountEntity = MyOffice.Data.Models.Accounts.Account; + +namespace MyOffice.Tests.Accounts; + +public class MotionAddAclTests +{ + [Fact] + public async Task MotionAddAsync_returns_forbidden_without_write_access() + { + var userId = Guid.NewGuid(); + var accountId = Guid.NewGuid(); + var account = new AccountEntity + { + Id = accountId, + Name = "Cash", + AccessRights = + [ + new AccountAccess + { + UserId = userId, + IsAllowRead = true, + IsAllowWrite = false, + IsAllowManage = false, + } + ] + }; + + var accountRepo = new Mock(); + accountRepo + .Setup(x => x.GetAsync(userId, accountId, It.IsAny())) + .ReturnsAsync(account); + + var service = CreateService(accountRepo.Object); + + var exec = await service.MotionAddAsync( + userId, + accountId, + new MotionAddUpdate + { + Date = DateTime.UtcNow, + Item = "Coffee", + Plus = 0, + Minus = 10, + }); + + Assert.Equal(MotionAddStatus.forbidden, exec.Status); + } + + [Fact] + public async Task MotionAddAsync_skips_balancing_when_counterparty_not_writable() + { + var userId = Guid.NewGuid(); + var primaryId = Guid.NewGuid(); + var balancingId = Guid.NewGuid(); + + var primary = new AccountEntity + { + Id = primaryId, + Name = "Cash", + AccessRights = + [ + new AccountAccess { UserId = userId, IsAllowRead = true, IsAllowWrite = true } + ] + }; + var balancing = new AccountEntity + { + Id = balancingId, + Name = "Bank", + AccessRights = + [ + new AccountAccess { UserId = userId, IsAllowRead = true, IsAllowWrite = false } + ] + }; + + var accountRepo = new Mock(); + accountRepo + .Setup(x => x.GetAsync(userId, primaryId, It.IsAny())) + .ReturnsAsync(primary); + accountRepo + .Setup(x => x.GetAsync(userId, balancingId, It.IsAny())) + .ReturnsAsync(balancing); + + var motionRepo = new Mock(); + motionRepo + .Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + var service = CreateService(accountRepo.Object, motionRepo.Object); + + var exec = await service.MotionAddAsync( + userId, + primaryId, + new MotionAddUpdate + { + Date = DateTime.UtcNow, + Item = "Transfer", + Plus = 0, + Minus = 25, + AccountId = balancingId.ToString("N"), + AmountBalancing = 25, + }); + + Assert.Equal(MotionAddStatus.success, exec.Status); + Assert.NotNull(exec.Result); + Assert.Single(exec.Result!); + motionRepo.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + private static ItemService CreateItemServiceStub() + { + var nextId = 1; + var itemRepo = new Mock(); + var itemGlobalRepo = new Mock(); + + itemGlobalRepo + .Setup(x => x.GetByNameAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ItemGlobal?)null); + itemGlobalRepo + .Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + itemRepo + .Setup(x => x.GetByGlobalAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Item?)null); + itemRepo + .Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true) + .Callback((i, _) => i.Id = nextId++); + + return new ItemService( + Mock.Of(), + itemRepo.Object, + itemGlobalRepo.Object, + new MapperConfiguration(_ => { }, NullLoggerFactory.Instance).CreateMapper()); + } + + private static AccountService CreateService( + IAccountRepository accountRepository, + IMotionRepository? motionRepository = null) + { + var mapper = new MapperConfiguration( + cfg => cfg.AddProfile(), + NullLoggerFactory.Instance).CreateMapper(); + + return new AccountService( + Mock.Of>(), + mapper, + Mock.Of(), + Mock.Of(), + Mock.Of(), + accountRepository, + Mock.Of(), + Mock.Of(), + motionRepository ?? Mock.Of(), + Mock.Of(), + Mock.Of(), + CreateItemServiceStub(), + Mock.Of()); + } +} diff --git a/MyOffice.Tests/Auth/OpenIddictRedirectUriTests.cs b/MyOffice.Tests/Auth/OpenIddictRedirectUriTests.cs new file mode 100644 index 0000000..ac21b6c --- /dev/null +++ b/MyOffice.Tests/Auth/OpenIddictRedirectUriTests.cs @@ -0,0 +1,22 @@ +using MyOffice.Web.Auth; + +namespace MyOffice.Tests.Auth; + +public class OpenIddictRedirectUriTests +{ + [Fact] + public void BuildRedirectUri_uses_localhost_when_host_empty() + { + var uri = OpenIddictSeeder.BuildRedirectUri(null); + + Assert.Equal("http://localhost:4300/silent-refresh.html", uri.ToString()); + } + + [Fact] + public void BuildRedirectUri_appends_silent_refresh_and_trims_slash() + { + var uri = OpenIddictSeeder.BuildRedirectUri("https://app.example.com/"); + + Assert.Equal("https://app.example.com/silent-refresh.html", uri.ToString()); + } +} diff --git a/MyOffice.Tests/Auth/PasswordHasherTests.cs b/MyOffice.Tests/Auth/PasswordHasherTests.cs new file mode 100644 index 0000000..6182366 --- /dev/null +++ b/MyOffice.Tests/Auth/PasswordHasherTests.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Identity; +using MyOffice.Web.Identity.Domain; +using MyOffice.Web.Identity.Repositories; + +namespace MyOffice.Tests.Auth; + +public class PasswordHasherTests +{ + private readonly PasswordHasher _hasher = new(); + private readonly ApplicationUser _user = new() { Id = Guid.NewGuid(), UserName = "test@example.com" }; + + [Fact] + public void HashPassword_then_verify_succeeds_with_identity_format() + { + var hash = _hasher.HashPassword(_user, "P@ssw0rd!"); + + Assert.Equal(PasswordVerificationResult.Success, _hasher.VerifyHashedPassword(_user, hash, "P@ssw0rd!")); + } + + [Fact] + public void VerifyHashedPassword_fails_for_wrong_password() + { + var hash = _hasher.HashPassword(_user, "P@ssw0rd!"); + + Assert.Equal(PasswordVerificationResult.Failed, _hasher.VerifyHashedPassword(_user, hash, "wrong")); + } + + [Fact] + public void HashPassword_produces_distinct_hashes_due_to_salt() + { + var a = _hasher.HashPassword(_user, "same"); + var b = _hasher.HashPassword(_user, "same"); + + Assert.NotEqual(a, b); + } + + [Fact] + public void VerifyHashedPassword_legacy_hash_returns_rehash_needed() + { + var legacy = PasswordHasher.HashLegacyForTests("legacy-secret"); + + Assert.Equal( + PasswordVerificationResult.SuccessRehashNeeded, + _hasher.VerifyHashedPassword(_user, legacy, "legacy-secret")); + } + + [Fact] + public void VerifyHashedPassword_legacy_wrong_password_fails() + { + var legacy = PasswordHasher.HashLegacyForTests("legacy-secret"); + + Assert.Equal( + PasswordVerificationResult.Failed, + _hasher.VerifyHashedPassword(_user, legacy, "nope")); + } +} diff --git a/MyOffice.Tests/Data/DemoDataSeederTests.cs b/MyOffice.Tests/Data/DemoDataSeederTests.cs new file mode 100644 index 0000000..d03991d --- /dev/null +++ b/MyOffice.Tests/Data/DemoDataSeederTests.cs @@ -0,0 +1,54 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using MyOffice.DbContext; + +namespace MyOffice.Tests.Data; + +public class DemoDataSeederTests +{ + [Fact] + public async Task SeedIfEmpty_creates_demo_users_catalog_and_motions() + { + var path = Path.Combine(Path.GetTempPath(), $"myoffice-seed-{Guid.NewGuid():N}.db"); + var connection = new ConnectionConfiguration("sqlite", $"Data Source={path}"); + var options = new DbContextOptionsBuilder(); + DbContextServiceCollectionExtensions.ConfigureDbContextOptions(options, connection); + + await using (var db = new AppDbContext(AppDbContextProvidersEnum.sqlite, options.Options)) + { + await DatabaseBootstrapper.InitializeAsync(db); + + Assert.Equal(3, await db.Users.CountAsync()); + Assert.Equal(9, await db.Currencies.CountAsync()); + Assert.Equal(9, await db.CurrencyRates.CountAsync()); + Assert.Equal(9, await db.AccountCategories.CountAsync()); + Assert.Equal(27, await db.Accounts.CountAsync()); + Assert.Equal(300, await db.Motions.CountAsync()); + + var uahUser = await db.Users.SingleAsync(x => x.Email == "user_UAH@user_UAH.userUAH"); + Assert.Equal("UAH", uahUser.CurrencyId); + + var uahRates = await db.CurrencyRates + .Include(x => x.Currency) + .Where(x => x.Currency!.UserId == uahUser.Id) + .ToListAsync(); + Assert.Equal(1m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "UAH").Rate); + Assert.Equal(42m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "USD").Rate); + Assert.Equal(50m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "EUR").Rate); + + Assert.Contains(await db.AccountCategories.Where(x => x.UserId == uahUser.Id).Select(x => x.Name).ToListAsync(), + x => x == "Готівка"); + Assert.Contains(await db.ItemCategories.Where(x => x.UserId == uahUser.Id).Select(x => x.Name).ToListAsync(), + x => x == "Доходи"); + + await DemoDataSeeder.SeedIfEmptyAsync(db); + Assert.Equal(3, await db.Users.CountAsync()); + } + + SqliteConnection.ClearAllPools(); + if (File.Exists(path)) + { + File.Delete(path); + } + } +} diff --git a/MyOffice.Tests/MyOffice.Tests.csproj b/MyOffice.Tests/MyOffice.Tests.csproj new file mode 100644 index 0000000..f1d43c2 --- /dev/null +++ b/MyOffice.Tests/MyOffice.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyOffice.Web/App.Config b/MyOffice.Web/App.Config new file mode 100644 index 0000000..cae4452 --- /dev/null +++ b/MyOffice.Web/App.Config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MyOffice.Web/Controllers/AccountController.cs b/MyOffice.Web/Controllers/AccountController.cs new file mode 100644 index 0000000..c313bea --- /dev/null +++ b/MyOffice.Web/Controllers/AccountController.cs @@ -0,0 +1,187 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Core; +using Core.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Models.Account; +using Models.Item; +using Models.Motion; +using MyOffice.Web.Infrastructure.Attributes; +using Services.Account; +using Services.Account.Domain; +using Services.Item; + +[Authorize] +[ApiController] +[Route("api/accounts")] +[DefaultFromBody] +public class AccountController : BaseApiController +{ + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly AccountService _accountService; + private readonly ItemService _itemService; + + public AccountController( + ILogger logger, + IMapper mapper, + AccountService accountService, + ItemService itemService + ) + { + _logger = logger; + _mapper = mapper; + _accountService = accountService; + _itemService = itemService; + } + + [HttpGet] + public async Task AccountsGet([FromQuery] string category) + { + var list = await _accountService.GetByCategoryDetailedAsync(UserId, category!.AsGuid()); + + return OkResponse(_mapper.Map>(list.OrderBy(x => x.Account.Name).ToList())); + } + + [HttpGet("{id}")] + public async Task AccountGet(string id) + { + var exec = await _accountService.GetByIdDetailedAsync(UserId, id!.AsGuid()); + + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map(result)), + notFoundDetail: "Account not found.", + failureDetail: "Failed to load account."); + } + + [HttpGet("{id}/motions")] + public async Task MotionsGet(string id, [FromQuery] MotionsGetRequest request) + { + var exec = await _accountService.GetMotionsAsync( + UserId, + id!.AsGuid(), + request.From.StartOfDay(), + request.To.EndOfDay()); + + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map>(result)), + notFoundDetail: "Account not found.", + failureDetail: "Failed to load motions."); + } + + [HttpPost("{id}/motions")] + public async Task MotionsPost(string id, MotionRequest motion) + { + var exec = await _accountService.MotionAddAsync( + UserId, + id.AsGuid(), + _mapper.Map(motion)); + + switch (exec.Status) + { + case MotionAddStatus.account_not_found: + return ProblemNotFoundResponse("Account not found."); + case MotionAddStatus.forbidden: + return ProblemForbiddenResponse("Write access required."); + case MotionAddStatus.failure: + return ProblemBadResponse("Adding motion failed."); + case MotionAddStatus.success: + return Ok(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPut("{id}/motions/{motionId}")] + public async Task MotionsPut(string id, string motionId, MotionRequest motion) + { + var exec = await _accountService.MotionUpdateAsync( + UserId, + id.AsGuid(), + motionId.AsGuid(), + _mapper.Map(motion)); + + switch (exec.Status) + { + case MotionUpdateStatus.not_found: + return ProblemNotFoundResponse("Motion not found."); + case MotionUpdateStatus.forbidden: + return ProblemForbiddenResponse("Write access required."); + case MotionUpdateStatus.failure: + return ProblemBadResponse("Updating motion failed."); + case MotionUpdateStatus.success: + return OkResponse(_mapper.Map>(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("{id}/motions/{motionId}")] + public async Task MotionsDelete(string id, string motionId) + { + var exec = await _accountService.MotionRemoveAsync( + UserId, + id.AsGuid(), + motionId.AsGuid()); + + switch (exec.Status) + { + case MotionDeleteStatus.not_found: + return ProblemNotFoundResponse("Motion not found."); + case MotionDeleteStatus.forbidden: + return ProblemForbiddenResponse("Write access required."); + case MotionDeleteStatus.failure: + return ProblemBadResponse("Deliting motion failed."); + case MotionDeleteStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpGet("~/api/items")] + public async Task FindItems([FromQuery] string term, CancellationToken cancellationToken) + { + var itemsDto = await _itemService.FindItemsAsync(UserId, term, cancellationToken: cancellationToken); + + var items = _mapper.Map>(itemsDto); + + if (term.StartsWith("+") && term.Length > 1) + { + var foundAccounts = await _accountService.FindAccountsAsync(UserId, term.Substring(1), cancellationToken); + var accounts = foundAccounts.OrderByDescending(x => x.Name); + + foreach (var account in accounts) + { + var accountName = $"+{account.Name}"; + var item = items.FirstOrDefault(x => x.Name.IsPresent() && x.Name!.Length > 1 && x.Name.Substring(1) == accountName); + + if (item == null) + { + items.Add(new ItemViewModel + { + Name = accountName, + AccountId = account.Id.ToShort(), + }); + } + else + { + item.AccountId = account.Id.ToShort(); + } + } + } + + return OkResponse(_mapper + .Map>(items) + .OrderBy(x => x.AccountId) + .ThenBy(x => x.Name) + ); + } +} diff --git a/MyOffice.Web/Controllers/AuthorizationController.cs b/MyOffice.Web/Controllers/AuthorizationController.cs new file mode 100644 index 0000000..968c32d --- /dev/null +++ b/MyOffice.Web/Controllers/AuthorizationController.cs @@ -0,0 +1,71 @@ +namespace MyOffice.Web.Controllers; + +using System.Security.Claims; +using Data.Repositories.Users; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Server.AspNetCore; +using OpenIddict.Validation.AspNetCore; +using static OpenIddict.Abstractions.OpenIddictConstants; + +[ApiController] +public sealed class AuthorizationController : ControllerBase +{ + private readonly IUserRepository _userRepository; + + public AuthorizationController(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] + [HttpGet("~/connect/authorize")] + [HttpPost("~/connect/authorize")] + public async Task Authorize() + { + var result = await HttpContext.AuthenticateAsync( + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + if (!result.Succeeded) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The user is not authenticated." + })); + } + + return SignIn(result.Principal!, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] + [HttpGet("~/connect/userinfo")] + [HttpPost("~/connect/userinfo")] + [Produces("application/json")] + public async Task Userinfo() + { + var userIdValue = User.FindFirstValue(Claims.Subject); + if (!Guid.TryParse(userIdValue, out var userId)) + return Challenge(authenticationSchemes: OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + + var user = await _userRepository.GetUserAsync(userId); + if (user is null) + return Challenge(authenticationSchemes: OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + + return Ok(new + { + sub = userIdValue, + id = userIdValue, + email = user.Email, + name = user.FullName ?? user.UserName, + firstName = user.FirstName, + lastName = user.LastName, + fullName = user.FullName, + phone = user.Phone + }); + } +} diff --git a/MyOffice.Web/Controllers/BaseApiController.cs b/MyOffice.Web/Controllers/BaseApiController.cs new file mode 100644 index 0000000..d6131ed --- /dev/null +++ b/MyOffice.Web/Controllers/BaseApiController.cs @@ -0,0 +1,80 @@ +namespace MyOffice.Web.Controllers; + +using System.Security.Authentication; +using Microsoft.AspNetCore.Mvc; +using Core; +using Core.Extensions; +using IdentityModel; +using MyOffice.Web.Infrastructure.Attributes; +using MyOffice.Web.Models; + +[DefaultFromBody] +public class BaseApiController : ControllerBase +{ + public Guid UserId + { + get + { + var subject = User.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject)?.Value; + if (subject.IsPresent() && Guid.TryParse(subject, out var guid)) return guid; + + throw new AuthenticationException("Get UserId failed"); + } + } + + [NonAction] + public ObjectResult ProblemBadResponse(string? detail = null) + { + return Problem(detail, statusCode: StatusCodes.Status400BadRequest); + } + + [NonAction] + public ObjectResult ProblemNotFoundResponse(string? detail = null) + { + return Problem(detail ?? "Not found.", statusCode: StatusCodes.Status404NotFound); + } + + [NonAction] + public ObjectResult ProblemForbiddenResponse(string? detail = null) + { + return Problem(detail ?? "Forbidden.", statusCode: StatusCodes.Status403Forbidden); + } + + [NonAction] + public ObjectResult OkResponse(IResponseModel response) + { + return Ok(response); + } + + [NonAction] + public ObjectResult OkResponse(IEnumerable response) where T : IResponseModel + { + return Ok(response); + } + + /// + /// Maps to ProblemDetails status codes: + /// not_found → 404, forbidden → 403, failure → 400. + /// + protected ObjectResult MapGeneralExec( + Exec exec, + Func onSuccess, + string notFoundDetail, + string? failureDetail = null, + string? forbiddenDetail = null) where T : class + { + switch (exec.Status) + { + case GeneralExecStatus.success: + return onSuccess(exec.Result!); + case GeneralExecStatus.not_found: + return ProblemNotFoundResponse(notFoundDetail); + case GeneralExecStatus.forbidden: + return ProblemForbiddenResponse(forbiddenDetail); + case GeneralExecStatus.failure: + return ProblemBadResponse(failureDetail ?? "Operation failed."); + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } +} \ No newline at end of file diff --git a/MyOffice.Web/Controllers/DashboardController.cs b/MyOffice.Web/Controllers/DashboardController.cs new file mode 100644 index 0000000..bd6c188 --- /dev/null +++ b/MyOffice.Web/Controllers/DashboardController.cs @@ -0,0 +1,75 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Core.Extensions; +using Infrastructure.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MyOffice.Web.Models.Dashboard; +using Services.Dashboard; + +[Authorize] +[ApiController] +[Route("api/dashboard")] +[DefaultFromBody] +public class DashboardController : BaseApiController +{ + private readonly ILogger _logger; + private readonly DashboardService _dashboardService; + private readonly IMapper _mapper; + + public DashboardController( + ILogger logger, + DashboardService dashboardService, + IMapper mapper + ) + { + _logger = logger; + _dashboardService = dashboardService; + _mapper = mapper; + } + + [HttpGet] + public async Task Index(CancellationToken cancellationToken) + { + var data = await _dashboardService.GetDashboardRestDataAsync(UserId, cancellationToken); + + return OkResponse(_mapper.Map(data)); + } + + [HttpGet("income")] + public async Task Income( + DateTime from, + DateTime to, + [AsGuid(true)] string? category, + CancellationToken cancellationToken + ) + { + var data = await _dashboardService.GetDashboardIncomeDataAsync( + UserId, + from.StartOfDay(), + to.EndOfDay(), + category?.AsGuidNull(), + cancellationToken); + + return OkResponse(_mapper.Map(data)); + } + + [HttpGet("outcome")] + public async Task Outcome( + DateTime from, + DateTime to, + [AsGuid(true)] string? category, + CancellationToken cancellationToken + ) + { + var data = await _dashboardService.GetDashboardOutcomeDataAsync( + UserId, + from.StartOfDay(), + to.EndOfDay(), + category?.AsGuidNull(), + cancellationToken); + + return OkResponse(_mapper.Map(data)); + } +} diff --git a/MyOffice.Web/Controllers/GeneralController.cs b/MyOffice.Web/Controllers/GeneralController.cs new file mode 100644 index 0000000..e9e8780 --- /dev/null +++ b/MyOffice.Web/Controllers/GeneralController.cs @@ -0,0 +1,35 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Models.Currency; +using Services.Currency; +using MyOffice.Web.Infrastructure.Attributes; + +[Authorize] +[ApiController] +[Route("api/general")] +[DefaultFromBody] +public class GeneralController : BaseApiController +{ + private readonly CurrencyService _currencyService; + private readonly IMapper _mapper; + + public GeneralController( + CurrencyService currencyService, + IMapper mapper + ) + { + _currencyService = currencyService; + _mapper = mapper; + } + + [HttpGet("currencies")] + public async Task GetGlobalCurrencies(CancellationToken cancellationToken) + { + var list = await _currencyService.GetGlobalAllAsync(cancellationToken); + + return Ok(_mapper.Map>(list)); + } +} diff --git a/MyOffice.Web/Controllers/SettingsAccountCategoryController.cs b/MyOffice.Web/Controllers/SettingsAccountCategoryController.cs new file mode 100644 index 0000000..bc85edb --- /dev/null +++ b/MyOffice.Web/Controllers/SettingsAccountCategoryController.cs @@ -0,0 +1,122 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Microsoft.AspNetCore.Authorization; + +using Core; +using Core.Extensions; +using Models.Account; +using Services.Account; +using Services.Account.Domain; +using Infrastructure.Attributes; +using Services.Identity; +using Microsoft.AspNetCore.Mvc; + +[Authorize] +[ApiController] +[Route("api/settings/account-categories")] +[DefaultFromBody] +public class SettingsAccountCategoryController : BaseApiController +{ + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly AccountService _accountService; + private readonly IContextProvider _contextProvider; + + public SettingsAccountCategoryController( + ILogger logger, + IMapper mapper, + AccountService accountService, + IContextProvider contextProvider + ) + { + _logger = logger; + _mapper = mapper; + _accountService = accountService; + _contextProvider = contextProvider; + } + + [HttpGet] + public async Task AccountCategories(CancellationToken cancellationToken) + { + var list = await _accountService.GetAllCategoriesAsync(UserId, cancellationToken); + + return OkResponse(_mapper.Map>(list).OrderBy(x => x.Name)); + } + + [HttpGet("{id}")] + public async Task AccountCategory([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _accountService.GetCategoryAsync(UserId, id.AsGuid(), cancellationToken); + + switch (exec.Status) + { + case GeneralExecStatus.not_found: + case GeneralExecStatus.failure: + return ProblemNotFoundResponse("Account category not found."); + + case GeneralExecStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPost] + public async Task AccountCategoriesAdd(AccountCategoryViewModel request, CancellationToken cancellationToken) + { + var exec = await _accountService.CategoryAddAsync(UserId, new AccountCategoryDto { Name = request.Name! }, cancellationToken); + switch (exec.Status) + { + case GeneralExecStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + case GeneralExecStatus.failure: + case GeneralExecStatus.not_found: + return ProblemBadResponse("Adding account category failed."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPut("{id}")] + public async Task AccountCategoriesUpdate([AsGuid] string id, AccountCategoryViewModel request, CancellationToken cancellationToken) + { + var exec = await _accountService.CategoryUpdateAsync(UserId, id.AsGuid(), new AccountCategoryDto { Name = request.Name! }, cancellationToken); + switch (exec.Status) + { + case GeneralExecStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + case GeneralExecStatus.failure: + case GeneralExecStatus.not_found: + return ProblemBadResponse("Update account category failed."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("{id}")] + public async Task AccountCategoriesDelete([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _accountService.CategoryRemoveAsync(UserId, id.AsGuid(), cancellationToken); + switch (exec.Status) + { + case AccountCategoryRemoveResult.success: + return OkResponse(_mapper.Map(exec.Result!)); + + case AccountCategoryRemoveResult.failure: + return ProblemBadResponse("Remove account category failed."); + case AccountCategoryRemoveResult.not_found: + return ProblemNotFoundResponse("Account category not found."); + case AccountCategoryRemoveResult.accounts_exists: + return ProblemBadResponse("Account category have accounts."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } +} diff --git a/MyOffice.Web/Controllers/SettingsAccountController.cs b/MyOffice.Web/Controllers/SettingsAccountController.cs new file mode 100644 index 0000000..20a44f5 --- /dev/null +++ b/MyOffice.Web/Controllers/SettingsAccountController.cs @@ -0,0 +1,226 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +using Core; +using Core.Extensions; +using Models.Account; +using Services.Account; +using Services.Account.Domain; +using Infrastructure.Attributes; +using Services.Identity; + +[Authorize] +[ApiController] +[Route("api/settings/accounts")] +[DefaultFromBody] +public class SettingsAccountController : BaseApiController +{ + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly AccountService _accountService; + private readonly IContextProvider _contextProvider; + + public SettingsAccountController( + ILogger logger, + IMapper mapper, + AccountService accountService, + IContextProvider contextProvider + ) + { + _logger = logger; + _mapper = mapper; + _accountService = accountService; + _contextProvider = contextProvider; + } + + [HttpGet("invites")] + public async Task AccountInvites(CancellationToken cancellationToken) + { + var invites = await _accountService.InvitesGetAsync(_contextProvider.User.Email, cancellationToken); + + return OkResponse(_mapper.Map>(invites)); + } + + [HttpPost("invites/{id}/accept")] + public async Task AccountInviteAccept([AsGuid] string id, AccountInviteAcceptRequest request, CancellationToken cancellationToken) + { + var exec = await _accountService.InviteAcceptAsync(UserId, id.AsGuid(), request.Name, cancellationToken); + + switch (exec.Status) + { + case InviteAcceptStatus.invite_not_found: + return ProblemNotFoundResponse("Invite not found."); + case InviteAcceptStatus.account_not_found: + return ProblemNotFoundResponse("Account not found."); + + case InviteAcceptStatus.already_accepted: + case InviteAcceptStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPost("invites/{id}/reject")] + public async Task AccountInviteReject([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _accountService.InviteRejectAsync(id.AsGuid(), cancellationToken); + + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map(result)), + notFoundDetail: "Invite not found.", + failureDetail: "Invite reject failed."); + } + + [HttpGet] + public async Task AccountsGet([FromQuery][AsGuid(true)] string? category, CancellationToken cancellationToken) + { + var list = category.IsPresent() + ? await _accountService.GetByCategoryAsync(UserId, category!.AsGuid(), cancellationToken) + : await _accountService.GetAllAccountsAsync(UserId, cancellationToken); + + return OkResponse(_mapper.Map>(list.OrderBy(x => x.Name))); + } + + [HttpPost] + public async Task AccountsAdd(AccountViewModel request, CancellationToken cancellationToken) + { + var exec = await _accountService.AccountAddAsync(UserId, new AccountAdd + { + Name = request.Name, + CurrencyId = request.CurrencyId, + CategoryId = request.CategoryId.AsGuid(), + Type = request.Type, + }, cancellationToken); + + switch (exec.Status) + { + case AccountAddStatus.category_not_found: + return ProblemNotFoundResponse("Category not found."); + case AccountAddStatus.currency_not_found: + return ProblemNotFoundResponse("Currency not found."); + case AccountAddStatus.failure: + return ProblemBadResponse("Adding account failed."); + case AccountAddStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPut("{id}")] + public async Task AccountsUpdate([AsGuid] string id, AccountEditRequestModel request, CancellationToken cancellationToken) + { + var exec = await _accountService.AccountUpdateAsync(UserId, id.AsGuid(), new AccountEdit + { + Name = request.Name, + CurrencyId = request.CurrencyId, + CategoryId = request.CategoryId?.AsGuidNull(), + UserId = request.UserId?.AsGuidNull(), + Type = request.Type, + }, cancellationToken); + + switch (exec.Status) + { + case AccountEditStatus.not_found: + return ProblemNotFoundResponse("Account not found."); + case AccountEditStatus.forbidden: + return ProblemForbiddenResponse("Manage access required."); + case AccountEditStatus.category_not_found: + return ProblemNotFoundResponse("Category not found."); + case AccountEditStatus.currency_not_found: + return ProblemNotFoundResponse("Currency not found."); + case AccountEditStatus.failure: + return ProblemBadResponse("Adding account failed."); + case AccountEditStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("{id}")] + public async Task AccountsDelete([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _accountService.AccountDeleteAsync(UserId, id, cancellationToken); + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map(result)), + notFoundDetail: "Account not found.", + forbiddenDetail: "Manage access required."); + } + + [HttpDelete("{id}/category/{categoryId}")] + public async Task AccountsCategoryDelete([AsGuid] string id, [AsGuid] string categoryId, CancellationToken cancellationToken) + { + var exec = await _accountService.AccountCategoryRemoveAsync(UserId, id.AsGuid(), categoryId.AsGuid(), cancellationToken); + + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map(result)), + notFoundDetail: "Category not found.", + failureDetail: "Category remove failed.", + forbiddenDetail: "Manage access required."); + } + + [HttpPost("{id}/access")] + public async Task AccountsAccessAdd([AsGuid] string id, AccountAccessViewModel request, CancellationToken cancellationToken) + { + var model = _mapper.Map>(request.Accesses); + + var exec = await _accountService.AccessUpdateAsync(UserId, id.AsGuid(), model, cancellationToken); + + if (exec.Status != GeneralExecStatus.success) + { + return MapGeneralExec( + exec, + _ => OkResponse(_mapper.Map(exec.Result!)), + notFoundDetail: "Account not found.", + failureDetail: "Access update failed.", + forbiddenDetail: "Manage access required."); + } + + if (!request.Email.IsPresent()) + { + return OkResponse(_mapper.Map(exec.Result!)); + } + + var inviteExec = await _accountService.AccessInviteAsync(UserId, id.AsGuid(), request.Email!, request.AllowWrite, cancellationToken); + + switch (inviteExec.Status) + { + case AccessInviteStatus.account_not_found: + return ProblemNotFoundResponse("Account not found."); + case AccessInviteStatus.forbidden: + return ProblemForbiddenResponse("Manage access required."); + + case AccessInviteStatus.access_exists: + case AccessInviteStatus.invite_exists: + case AccessInviteStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("{id}/access/{userId}")] + public async Task AccountsAccessDelete([AsGuid] string id, [AsGuid] string userId, CancellationToken cancellationToken) + { + var exec = await _accountService.AccessDeleteAsync(UserId, id.AsGuid(), userId.AsGuid(), cancellationToken); + + return MapGeneralExec( + exec, + result => OkResponse(_mapper.Map(result)), + notFoundDetail: "Account not found.", + failureDetail: "Access delete failed.", + forbiddenDetail: "Manage access required."); + } +} diff --git a/MyOffice.Web/Controllers/SettingsCurrencyController.cs b/MyOffice.Web/Controllers/SettingsCurrencyController.cs new file mode 100644 index 0000000..9d45e6d --- /dev/null +++ b/MyOffice.Web/Controllers/SettingsCurrencyController.cs @@ -0,0 +1,142 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Core; +using Core.Extensions; +using Infrastructure.Attributes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Models.Currency; +using Services.Currency; +using Services.Currency.Domain; + +[Authorize] +[ApiController] +[Route("api/settings/currencies")] +[DefaultFromBody] +public class SettingCurrencyController : BaseApiController +{ + private readonly CurrencyService _currencyService; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public SettingCurrencyController( + CurrencyService currencyService, + ILogger logger, + IMapper mapper + ) + { + _currencyService = currencyService; + _logger = logger; + _mapper = mapper; + } + + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + var list = await _currencyService.GetAllWithRatesAsync(UserId, cancellationToken); + + return OkResponse(_mapper.Map>(list).OrderBy(x => x.Id)); + } + + [HttpPost] + public async Task Add(CurrencyAddModel currency, CancellationToken cancellationToken) + { + var exec = await _currencyService.CurrencyAddAsync(UserId, new CurrencyDto + { + UserId = UserId, + CurrencyGlobalId = currency.Id, + Name = currency.Name, + ShortName = currency.ShortName, + }, cancellationToken); + + switch (exec.Status) + { + case CurrencyAddStatus.success: + case CurrencyAddStatus.exists: + await _currencyService.CurrencyRateAddAsync(UserId, exec.Result!.Id, new CurrencyRateDto + { + CurrencyId = exec.Result!.Id, + Rate = currency.Rate, + Quantity = currency.Quantity, + DateTime = currency.RateDate.Date, + }, cancellationToken); + return OkResponse(_mapper.Map(exec.Result!)); + + case CurrencyAddStatus.failed: + return ProblemBadResponse("Adding currency failed."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPut("{id}")] + public async Task Update(string id, CurrencyEditModel currency, CancellationToken cancellationToken) + { + var exec = await _currencyService.CurrencyUpdateAsync( + UserId, + id.AsGuid(), + new CurrencyEdit { Name = currency.Name, ShortName = currency.ShortName, IsPrimary = currency.IsPrimary }, + cancellationToken + ); + + switch (exec.Status) + { + case CurrencyEditStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + case CurrencyEditStatus.not_found: + case CurrencyEditStatus.failed: + return ProblemNotFoundResponse("Currency not found."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("{id}")] + public async Task Delete([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _currencyService.RemoveAsync(UserId, id.AsGuid(), cancellationToken); + + switch (exec.Status) + { + case GeneralExecStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + case GeneralExecStatus.not_found: + case GeneralExecStatus.failure: + return ProblemNotFoundResponse("Currency not found."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPost("{id}/rate")] + public async Task AddRate(string id, CurrencyRateModel currencyRate, CancellationToken cancellationToken) + { + var exec = await _currencyService.CurrencyRateAddAsync(UserId, id.AsGuid(), new CurrencyRateDto + { + CurrencyId = id.AsGuid(), + Quantity = currencyRate.Quantity, + Rate = currencyRate.Rate, + DateTime = currencyRate.RateDate.Date, + }, cancellationToken); + + switch (exec.Status) + { + case CurrencyAddRateStatus.failed: + return ProblemBadResponse("Adding currency rate failed."); + case CurrencyAddRateStatus.not_found: + return ProblemNotFoundResponse("Currency not found."); + + case CurrencyAddRateStatus.success: + return OkResponse(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } +} diff --git a/MyOffice.Web/Controllers/SettingsItemController.cs b/MyOffice.Web/Controllers/SettingsItemController.cs new file mode 100644 index 0000000..a03ba98 --- /dev/null +++ b/MyOffice.Web/Controllers/SettingsItemController.cs @@ -0,0 +1,163 @@ +namespace MyOffice.Web.Controllers; + +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +using Models.Item; +using Core.Extensions; +using Core; +using Infrastructure.Attributes; +using Services.Item; +using Services.Item.Domain; + +[Authorize] +[ApiController] +[Route("api/settings")] +[DefaultFromBody] +public class SettingsItemController : BaseApiController +{ + private ILogger _logger; + private IMapper _mapper; + private ItemService _itemService; + + public SettingsItemController( + ILogger logger, + ItemService itemService, + IMapper mapper + ) + { + _itemService = itemService; + _logger = logger; + _mapper = mapper; + } + + [HttpGet("item-categories")] + public async Task ItemsCategories(CancellationToken cancellationToken) + { + var list = await _itemService.GetAllCategoriesAsync(UserId, cancellationToken); + + return Ok(_mapper.Map>(list) + .OrderByDescending(x => x.SortOrder) + .ThenBy(x => x.Name)); + } + + [HttpPost("item-categories")] + public async Task ItemCategoriesAdd(ItemCategoryViewModel request, CancellationToken cancellationToken) + { + var exec = await _itemService.CategoryAddAsync(UserId, request.FromModel(), cancellationToken); + + switch (exec.Status) + { + case GeneralExecStatus.success: + return Ok(_mapper.Map(exec.Result!)); + + case GeneralExecStatus.failure: + case GeneralExecStatus.not_found: + return ProblemBadResponse("Adding item category failed."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPut("item-categories/{id}")] + public async Task ItemCategoriesUpdate( + [AsGuid] string id, + ItemCategoryViewModel request, + CancellationToken cancellationToken) + { + var exec = await _itemService.CategoryUpdateAsync(UserId, id.AsGuid(), request.FromModel(), cancellationToken); + + switch (exec.Status) + { + case GeneralExecStatus.success: + return Ok(_mapper.Map(exec.Result!)); + + case GeneralExecStatus.failure: + case GeneralExecStatus.not_found: + return ProblemBadResponse("Update item category failed."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpDelete("item-categories/{id}")] + public async Task ItemCategoriesDelete([AsGuid] string id, CancellationToken cancellationToken) + { + var exec = await _itemService.CategoryRemoveAsync(UserId, id.AsGuid(), cancellationToken); + switch (exec.Status) + { + case ItemCategoryRemoveResult.success: + return Ok(_mapper.Map(exec.Result!)); + + case ItemCategoryRemoveResult.failure: + return ProblemBadResponse("Remove item category failed."); + case ItemCategoryRemoveResult.not_found: + return ProblemNotFoundResponse("Account item not found."); + case ItemCategoryRemoveResult.accounts_exists: + return ProblemBadResponse("Account motion have accounts."); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpGet("items")] + public async Task Items([FromQuery][AsGuid] string category, CancellationToken cancellationToken) + { + var categoryId = category.AsGuid(); + + var list = categoryId != UserId + ? await _itemService.GetByCategoryAsync(UserId, categoryId, cancellationToken) + : await _itemService.GetByUncategorizedAsync(UserId, cancellationToken); + + return Ok(_mapper.Map>(list.OrderBy(x => x.Name))); + } + + [HttpPut("items/{id}")] + public async Task ItemsUpdate( + [AsGuid] string id, + ItemEditModel request, + CancellationToken cancellationToken) + { + var exec = await _itemService.UpdateAsync(UserId, id.AsGuid(), request.Category.AsGuid(), cancellationToken); + + switch (exec.Status) + { + case GeneralExecStatus.not_found: + return ProblemNotFoundResponse("Item not found."); + case GeneralExecStatus.failure: + return ProblemBadResponse("Item update failed."); + case GeneralExecStatus.success: + return Ok(_mapper.Map(exec.Result!)); + + default: + throw new NotSupportedException(exec.Status.ToString()); + } + } + + [HttpPost("items")] + public async Task ItemsPost(ItemChangeCategoryModel request, CancellationToken cancellationToken) + { + var exec = await _itemService.UpdateItemsCategoryAsync( + UserId, + request.category.AsGuid(), + request.Items.Select(x => x.AsGuid()).ToList(), + cancellationToken); + + switch (exec) + { + case GeneralExecStatus.not_found: + return ProblemNotFoundResponse("Category not found."); + case GeneralExecStatus.failure: + return ProblemBadResponse("Update failed."); + case GeneralExecStatus.success: + return Ok(new {}); + + default: + throw new NotSupportedException(exec.ToString()); + } + } +} diff --git a/MyOffice.Web/Controllers/UserController.cs b/MyOffice.Web/Controllers/UserController.cs new file mode 100644 index 0000000..d4044c7 --- /dev/null +++ b/MyOffice.Web/Controllers/UserController.cs @@ -0,0 +1,189 @@ +namespace MyOffice.Web.Controllers; + +using Models.Auth; +using Identity.Domain; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authorization; +using MyOffice.Data.Models.Users; +using Core.Extensions; +using MyOffice.Core.Identity; +using Core; +using Services.Users; +using Services.Users.Domain; +using MyOffice.Data.Models.Currencies; +using MyOffice.Web.Infrastructure.Attributes; + +[ApiController] +[Route("api/user")] +[DefaultFromBody] +public class UserController : BaseApiController +{ + private readonly UserManager> _userManager; + private readonly UserService _userService; + private readonly IEnumerable _externalProviderValidators; + + public UserController( + UserManager> userManager, + UserService userService, + IEnumerable externalProviderValidators + ) + { + _userManager = userManager; + _userService = userService; + _externalProviderValidators = externalProviderValidators; + } + + [AllowAnonymous] + [HttpPost("register")] + public async Task Register([FromBody] RegisterModel request) + { + if (!ModelState.IsValid) + return BadRequest(new + { + Succeeded = false, + Errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => new { Code = "Validation", Description = e.ErrorMessage }) + }); + + var email = request.UserName.Trim(); + var user = new ApplicationUser + { + Id = Guid.NewGuid(), + UserName = email, + Email = email, + IsEmailConfirmed = true, + CurrencyId = CurrencyGlobalIdEnum.USD.ToString(), + }; + + var result = await _userManager.CreateAsync(user, request.Password); + + return Ok(new + { + result.Succeeded, + Errors = result.Errors + .Where(x => !x.Code.Equals("DuplicateUserName")) + .Select(x => new { x.Code, x.Description }) + }); + } + + [Authorize] + [HttpGet("profile")] + public async Task ProfileGet() + { + var user = await _userManager.FindByIdAsync(UserId.ToString()); + + return user.ToModel(await _userService.GetUserExternals(user.Id)); + } + + [Authorize] + [HttpPost("profile")] + public async Task ProfileUpdate([FromBody] ProfileModel model) + { + var user = await _userManager.FindByIdAsync(UserId.ToString()); ; + + user.FirstName = model.FirstName; + user.LastName = model.LastName; + user.FullName = model.FullName.NullIfEmpty() ?? $"{model.FirstName} {model.LastName}"; + user.CurrencyId = model.Currency; + + await _userManager.UpdateAsync(user); + + return user.ToModel(await _userService.GetUserExternals(user.Id)); + } + + [Authorize] + [HttpPost("attach")] + public async Task AttachProvider([FromBody] AttachModel model) + { + var validator = _externalProviderValidators.FirstOrDefault(x => x.Provider.EqualsIgnoreCase(model.Provider)); + if (validator == null) return ProblemBadResponse("Provider not supported"); + + var result = await validator.ValidateAsync(model.Token); + if (!result.IsSuccessed) return ProblemBadResponse("Token not valid"); + + var execResult = await _userService.AddUserExternalAsync(UserId, model.Provider, result.ExternalId!, result.Email!); + switch (execResult.Status) + { + case AddUserExternalStatusEnum.success: + return Ok(new + { + success = true + }); + + case AddUserExternalStatusEnum.externalid_used: + case AddUserExternalStatusEnum.user_not_valid: + return ProblemBadResponse("Provider already connected"); + + default: + throw new NotSupportedException(execResult.Status.ToString()); + } + } + + [Authorize] + [HttpPost("deattach")] + public async Task DeattachProvider([FromBody] DeattachModel model) + { + var exec = await _userService.RemoveUserExternal(UserId, model.Provider); + + if (exec.Status == GeneralExecStatus.not_found) return ProblemBadResponse("Provider not connected"); + + return Ok(new + { + success = true + }); + } +} + +public class DeattachModel +{ + [Required] public string Provider { get; set; } = null!; +} + +public class AttachModel +{ + [Required] public string Provider { get; set; } = null!; + [Required] public string Token { get; set; } = null!; +} + +public class ProfileModel +{ + public class ProviderModel + { + public string Provider { get; set; } = null!; + + public DateTime CreatedOn { get; set; } + } + + public string? Email { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? FullName { get; set; } + public bool? IsEmailConfirmed { get; set; } + public string? Currency { get; set; } + public List? Providers { get; set; } +} + +public static class ProfileModelExtensions +{ + public static ProfileModel ToModel(this ApplicationUser user, List userClaims) + { + return new ProfileModel + { + Email = user.Email, + FirstName = user.FirstName, + LastName = user.LastName, + FullName = user.FullName, + IsEmailConfirmed = user.IsEmailConfirmed, + Currency = user.CurrencyId, + Providers = userClaims?.Select(x => new ProfileModel.ProviderModel + { + Provider = x.Provider, + CreatedOn = x.CreatedOn + }).ToList() + }; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/ContextProvider.cs b/MyOffice.Web/Identity/ContextProvider.cs new file mode 100644 index 0000000..8ad8e1e --- /dev/null +++ b/MyOffice.Web/Identity/ContextProvider.cs @@ -0,0 +1,39 @@ +namespace MyOffice.Web.Identity; + +using Data.Repositories.Users; +using MyOffice.Data.Models.Users; +using MyOffice.Services.Identity; + +public class ContextProvider : IContextProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserRepository _userRepository; + + public ContextProvider( + IHttpContextAccessor httpContextAccessor, + IUserRepository userRepository + ) + { + _httpContextAccessor = httpContextAccessor; + _userRepository = userRepository; + } + + private User? _user = null; + public User User + { + get + { + _user ??= _userRepository.GetUser(UserId); + + return _user!; + } + } + + public Guid UserId + { + get + { + return Guid.Parse(_httpContextAccessor!.HttpContext!.User!.Claims!.FirstOrDefault(x => x.Type == "sub")!.Value); + } + } +} diff --git a/MyOffice.Web/Identity/Domain/ApplicationRole.cs b/MyOffice.Web/Identity/Domain/ApplicationRole.cs new file mode 100644 index 0000000..ddd8f96 --- /dev/null +++ b/MyOffice.Web/Identity/Domain/ApplicationRole.cs @@ -0,0 +1,5 @@ +namespace MyOffice.Web.Identity.Domain; + +public class ApplicationRole +{ +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/Domain/ApplicationUser.cs b/MyOffice.Web/Identity/Domain/ApplicationUser.cs new file mode 100644 index 0000000..c4f63d4 --- /dev/null +++ b/MyOffice.Web/Identity/Domain/ApplicationUser.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Web.Identity.Domain; + +public class ApplicationUser +{ +#pragma warning disable CS8618 // Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. + public TKey Id { get; set; } +#pragma warning restore CS8618 // Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. + public string UserName { get; set; } = null!; + public string Email { get; set; } = null!; + public string PasswordHash { get; set; } = null!; + + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? FullName { get; set; } + public bool IsEmailConfirmed { get; set; } + public string? CurrencyId { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/Domain/ExternalProvidersConfig.cs b/MyOffice.Web/Identity/Domain/ExternalProvidersConfig.cs new file mode 100644 index 0000000..af1b964 --- /dev/null +++ b/MyOffice.Web/Identity/Domain/ExternalProvidersConfig.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Web.Identity.Domain; + +public class ExternalProvidersConfig +{ + public class Auth0Config + { + public string? ClientId { get; set; } + public string? Domain { get; set; } + public string? SecretKey { get; set; } + } + + public class GoogleConfig + { + public string? ClientId { get; set; } + } + + public Auth0Config? Auth0 { get; set; } + public GoogleConfig? Google { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorAuth0.cs b/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorAuth0.cs new file mode 100644 index 0000000..efff106 --- /dev/null +++ b/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorAuth0.cs @@ -0,0 +1,69 @@ +namespace MyOffice.Web.Identity.ExternalProviders; + +using Domain; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Core.Extensions; +using Core.Identity; + +public class ExternalProviderValidatorAuth0 : IExternalProviderValidator +{ + private readonly ExternalProvidersConfig _externalProvidersConfig; + private readonly ILogger _logger; + + public ExternalProviderValidatorAuth0( + ILogger logger, + IOptions externalProvidersConfig + ) + { + _externalProvidersConfig = externalProvidersConfig.Value; + _logger = logger; + + Provider = ExternalProvidersConst.PROVIDER_AUTH0; + IsConfigured = _externalProvidersConfig.IsAuth0Configured(); + } + + public string Provider { get; } + public bool IsConfigured { get; } + + public async Task ValidateAsync(string token) + { + var auth0Domain = _externalProvidersConfig.Auth0!.Domain!; + var secretKey = _externalProvidersConfig.Auth0!.SecretKey!; + var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); + var configurationManager = new ConfigurationManager( + $"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); + var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None); + + var validations = new TokenValidationParameters + { + ValidIssuer = auth0Domain, + ValidAudiences = new[] { _externalProvidersConfig.Auth0!.ClientId }, + IssuerSigningKeys = openIdConfig.SigningKeys, + TokenDecryptionKey = securityKey + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var user = tokenHandler.ValidateToken(token, validations, out var validatedToken); + if (user.Identity?.IsAuthenticated != true) + { + return ExternalProviderValidatorResult.Failed(); + } + + var email = user.Claims.GetEmail(); + var emailVerified = user.Claims.GetValue("email_verified").AsBool(); + var fullName = user.Claims.GetValue("name")?.ToString(); + var externalId = user.Claims.GetSID(); + + if (email == null || externalId == null) + { + return ExternalProviderValidatorResult.Failed(); + } + + return ExternalProviderValidatorResult.Success(email, emailVerified, externalId, fullName); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorGoogle.cs b/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorGoogle.cs new file mode 100644 index 0000000..d5e6c4d --- /dev/null +++ b/MyOffice.Web/Identity/ExternalProviders/ExternalProviderValidatorGoogle.cs @@ -0,0 +1,36 @@ +namespace MyOffice.Web.Identity.ExternalProviders; + +using MyOffice.Core.Identity; +using Domain; +using Google.Apis.Auth; +using Microsoft.Extensions.Options; + +public class ExternalProviderValidatorGoogle : IExternalProviderValidator +{ + private readonly ExternalProvidersConfig _externalProvidersConfig; + + public ExternalProviderValidatorGoogle( + IOptions externalProvidersConfig + ) + { + _externalProvidersConfig = externalProvidersConfig.Value; + + Provider = ExternalProvidersConst.PROVIDER_GOOGLE; + IsConfigured = _externalProvidersConfig.IsGoogleConfigured(); + } + + public string Provider { get; } + public bool IsConfigured { get; } + + public async Task ValidateAsync(string token) + { + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List() { _externalProvidersConfig.Google!.ClientId! } + }; + var result = await GoogleJsonWebSignature.ValidateAsync(token, settings); + if (!result.EmailVerified) return ExternalProviderValidatorResult.Failed(); + + return ExternalProviderValidatorResult.Success(result.Email, result.EmailVerified, result.Subject, result.Name); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/ExternalProviders/ExternalProvidersConst.cs b/MyOffice.Web/Identity/ExternalProviders/ExternalProvidersConst.cs new file mode 100644 index 0000000..261bc0a --- /dev/null +++ b/MyOffice.Web/Identity/ExternalProviders/ExternalProvidersConst.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Web.Identity.ExternalProviders; + +public class ExternalProvidersConst +{ + public const string PROVIDER_GOOGLE = "google"; + public const string PROVIDER_AUTH0 = "auth0"; +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/ExternalProvidersConfigExtensions.cs b/MyOffice.Web/Identity/ExternalProvidersConfigExtensions.cs new file mode 100644 index 0000000..49c8a0d --- /dev/null +++ b/MyOffice.Web/Identity/ExternalProvidersConfigExtensions.cs @@ -0,0 +1,24 @@ +using MyOffice.Web.Identity.Domain; + +namespace MyOffice.Web.Identity; + +using Core.Extensions; + +public static class ExternalProvidersConfigExtensions +{ + public static bool IsGoogleConfigured(this ExternalProvidersConfig? config) + { + return config != null + && config.Google != null + && config.Google.ClientId.IsPresent(); + } + + public static bool IsAuth0Configured(this ExternalProvidersConfig? config) + { + return config != null + && config.Auth0 != null + && config.Auth0.ClientId.IsPresent() + && config.Auth0.Domain.IsPresent() + && config.Auth0.SecretKey.IsPresent(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/OpenIddict/ExternalGrantHandler.cs b/MyOffice.Web/Identity/OpenIddict/ExternalGrantHandler.cs new file mode 100644 index 0000000..7cdf7cc --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/ExternalGrantHandler.cs @@ -0,0 +1,137 @@ +namespace MyOffice.Web.Auth; + +using Core.Extensions; +using Core.Identity; +using Data.Models.Users; +using Data.Repositories.Users; +using Identity.Domain; +using Identity.ExternalProviders; +using Identity.Repositories; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +public sealed class ExternalGrantHandler : IOpenIddictServerHandler +{ + private readonly AppUserManager _userManager; + private readonly IUserExternalRepository _userExternalRepository; + private readonly IEnumerable _externalProviderValidators; + private readonly IHttpContextAccessor _httpContextAccessor; + + public ExternalGrantHandler( + AppUserManager userManager, + IUserExternalRepository userExternalRepository, + IEnumerable externalProviderValidators, + IHttpContextAccessor httpContextAccessor + ) + { + _userManager = userManager; + _userExternalRepository = userExternalRepository; + _externalProviderValidators = externalProviderValidators; + _httpContextAccessor = httpContextAccessor; + } + + public async ValueTask HandleAsync(HandleTokenRequestContext context) + { + if (!string.Equals(context.Request.GrantType, OpenIddictAuthConstants.ExternalGrantType, StringComparison.Ordinal)) + return; + + var provider = context.Request.GetParameter("provider")?.ToString(); + if (provider.IsMissing()) + { + context.Reject(Errors.InvalidRequest, "The provider parameter is required."); + return; + } + + var validator = _externalProviderValidators.FirstOrDefault(x => x.Provider.EqualsIgnoreCase(provider)); + if (validator is null || !validator.IsConfigured) + { + context.Reject(Errors.InvalidRequest, $"Provider not supported: {provider}"); + return; + } + + var token = context.Request.GetParameter("token")?.ToString(); + if (token.IsMissing()) + { + context.Reject(Errors.InvalidRequest, "The token parameter is required."); + return; + } + + var validationResult = await validator.ValidateAsync(token); + if (!validationResult.IsSuccessed) + { + context.Reject(Errors.InvalidGrant, "Token not valid."); + return; + } + + if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true) + { + context.Reject(Errors.InvalidGrant, "Authentication failed."); + return; + } + + var user = await _userManager.FindByEmailAsync(validationResult.Email!); + if (user is null) + { + if (!validationResult.EmailVerified) + { + context.Reject(Errors.InvalidGrant, "Email not confirmed."); + return; + } + + user = await CreateUserAsync(validationResult.Email!, validationResult.EmailVerified, validationResult.FullName); + if (user is null) + { + context.Reject(Errors.InvalidGrant, "Unable to create user."); + return; + } + } + + var externalLogin = await _userExternalRepository.GetByUserIdAsync(user.Id, provider) + ?? AddExternalLogin(user, validationResult.Email!, validationResult.ExternalId!, provider); + + context.SignIn(OpenIddictClaimsHelper.CreatePrincipal(user, context.Request.GetScopes())); + } + + private async Task?> CreateUserAsync(string email, bool isEmailConfirmed, string? fullName) + { + var user = new ApplicationUser + { + Id = Guid.NewGuid(), + UserName = email, + Email = email, + IsEmailConfirmed = isEmailConfirmed, + FullName = fullName + }; + + var password = "Qq1!_" + Guid.NewGuid(); + var result = await _userManager.CreateAsync(user, password); + return result.Succeeded ? user : null; + } + + private UserExternal AddExternalLogin( + ApplicationUser user, + string email, + string externalId, + string provider + ) + { + var existing = _userExternalRepository.GetByUserId(user.Id, provider); + if (existing is not null) + return existing; + + var claim = new UserExternal + { + UserId = user.Id, + CreatedOn = DateTime.UtcNow, + Provider = provider.ToLower(), + ExternalId = externalId, + Email = email + }; + _userExternalRepository.AddUserExternal(claim); + + return claim; + } +} diff --git a/MyOffice.Web/Identity/OpenIddict/OpenIddictClaimsHelper.cs b/MyOffice.Web/Identity/OpenIddict/OpenIddictClaimsHelper.cs new file mode 100644 index 0000000..3cdbdb6 --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/OpenIddictClaimsHelper.cs @@ -0,0 +1,41 @@ +namespace MyOffice.Web.Auth; + +using System.Security.Claims; +using Identity.Domain; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; + +public static class OpenIddictClaimsHelper +{ + public static ClaimsPrincipal CreatePrincipal( + ApplicationUser user, + IEnumerable scopes + ) + { + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + var userId = user.Id.ToString(); + identity.SetClaim(Claims.Subject, userId); + identity.SetClaim(Claims.Name, user.UserName); + identity.SetClaim(Claims.PreferredUsername, user.UserName); + identity.SetClaim(Claims.Email, user.Email); + identity.SetClaim(Claims.EmailVerified, user.IsEmailConfirmed); + + if (!string.IsNullOrWhiteSpace(user.FullName)) + identity.SetClaim(Claims.GivenName, user.FullName); + + identity.SetScopes(scopes); + identity.SetDestinations(static claim => claim.Type switch + { + Claims.Name or Claims.PreferredUsername or Claims.Email or Claims.EmailVerified or Claims.GivenName + => [Destinations.AccessToken, Destinations.IdentityToken], + _ => [Destinations.AccessToken] + }); + + return new ClaimsPrincipal(identity); + } +} diff --git a/MyOffice.Web/Identity/OpenIddict/OpenIddictConstants.cs b/MyOffice.Web/Identity/OpenIddict/OpenIddictConstants.cs new file mode 100644 index 0000000..62689d0 --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/OpenIddictConstants.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Web.Auth; + +public static class OpenIddictAuthConstants +{ + public const string ApiScope = "api"; + public const string ApiFriendlyName = "MyOffice.Web API"; + public const string SpaClientId = "angulartemplate_spa"; + public const string ExternalGrantType = "external"; + public const string RolesScope = "roles"; +} diff --git a/MyOffice.Web/Identity/OpenIddict/OpenIddictSeeder.cs b/MyOffice.Web/Identity/OpenIddict/OpenIddictSeeder.cs new file mode 100644 index 0000000..a095cd3 --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/OpenIddictSeeder.cs @@ -0,0 +1,130 @@ +namespace MyOffice.Web.Auth; + +using DbContext; +using Infrastructure; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; + +public sealed class OpenIddictSeeder : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public OpenIddictSeeder( + IServiceProvider serviceProvider, + IConfiguration configuration, + ILogger logger + ) + { + _serviceProvider = serviceProvider; + _configuration = configuration; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + + await RegisterScopesAsync(scope.ServiceProvider, cancellationToken); + await RegisterClientAsync(scope.ServiceProvider, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private async Task RegisterScopesAsync(IServiceProvider provider, CancellationToken cancellationToken) + { + var manager = provider.GetRequiredService(); + + if (await manager.FindByNameAsync(OpenIddictAuthConstants.ApiScope, cancellationToken) is null) + { + await manager.CreateAsync(new OpenIddictScopeDescriptor + { + Name = OpenIddictAuthConstants.ApiScope, + DisplayName = OpenIddictAuthConstants.ApiFriendlyName, + Resources = { OpenIddictAuthConstants.ApiScope } + }, cancellationToken); + + _logger.LogInformation("Created OpenIddict scope {Scope}.", OpenIddictAuthConstants.ApiScope); + } + + if (await manager.FindByNameAsync(OpenIddictAuthConstants.RolesScope, cancellationToken) is null) + { + await manager.CreateAsync(new OpenIddictScopeDescriptor + { + Name = OpenIddictAuthConstants.RolesScope, + DisplayName = "User roles" + }, cancellationToken); + + _logger.LogInformation("Created OpenIddict scope {Scope}.", OpenIddictAuthConstants.RolesScope); + } + } + + private async Task RegisterClientAsync(IServiceProvider provider, CancellationToken cancellationToken) + { + var manager = provider.GetRequiredService(); + var globalSettings = provider.GetRequiredService(); + + var redirectUri = BuildRedirectUri(globalSettings.Host); + var existing = await manager.FindByClientIdAsync(OpenIddictAuthConstants.SpaClientId, cancellationToken); + var descriptor = CreateSpaClientDescriptor(redirectUri); + + if (existing is null) + { + await manager.CreateAsync(descriptor, cancellationToken); + _logger.LogInformation("Created OpenIddict client {ClientId}.", OpenIddictAuthConstants.SpaClientId); + return; + } + + var currentRedirectUris = await manager.GetRedirectUrisAsync(existing, cancellationToken); + foreach (var uri in currentRedirectUris) + { + if (!string.Equals(uri, redirectUri.ToString(), StringComparison.Ordinal)) + descriptor.RedirectUris.Add(new Uri(uri, UriKind.Absolute)); + } + + await manager.UpdateAsync(existing, descriptor, cancellationToken); + _logger.LogInformation("Updated OpenIddict client {ClientId}.", OpenIddictAuthConstants.SpaClientId); + } + + internal static OpenIddictApplicationDescriptor CreateSpaClientDescriptor(Uri redirectUri) + { + var descriptor = new OpenIddictApplicationDescriptor + { + ClientId = OpenIddictAuthConstants.SpaClientId, + DisplayName = "MyOffice SPA", + ClientType = ClientTypes.Public, + ConsentType = ConsentTypes.Implicit, + Permissions = + { + Permissions.Endpoints.Authorization, + Permissions.Endpoints.Token, + Permissions.Endpoints.EndSession, + Permissions.GrantTypes.AuthorizationCode, + Permissions.GrantTypes.Password, + Permissions.GrantTypes.RefreshToken, + Permissions.Prefixes.GrantType + OpenIddictAuthConstants.ExternalGrantType, + Permissions.ResponseTypes.Code, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + Scopes.OpenId, + Permissions.Prefixes.Scope + Scopes.OfflineAccess, + Permissions.Prefixes.Scope + OpenIddictAuthConstants.ApiScope, + Permissions.Prefixes.Scope + OpenIddictAuthConstants.RolesScope + } + }; + + descriptor.RedirectUris.Add(redirectUri); + return descriptor; + } + + internal static Uri BuildRedirectUri(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return new Uri("http://localhost:4300/silent-refresh.html"); + + return new Uri($"{host.TrimEnd('/')}/silent-refresh.html"); + } +} diff --git a/MyOffice.Web/Identity/OpenIddict/OpenIddictServiceCollectionExtensions.cs b/MyOffice.Web/Identity/OpenIddict/OpenIddictServiceCollectionExtensions.cs new file mode 100644 index 0000000..44942fd --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/OpenIddictServiceCollectionExtensions.cs @@ -0,0 +1,192 @@ +namespace MyOffice.Web.Auth; + +using System.Security.Cryptography; +using DbContext; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Server; +using OpenIddict.Validation.AspNetCore; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +public static class OpenIddictServiceCollectionExtensions +{ + public static IServiceCollection AddMyOfficeOpenIddict( + this IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment + ) + { + services.AddOpenIddict() + .AddCore(options => + { + options.UseEntityFrameworkCore() + .UseDbContext(); + }) + .AddServer(options => + { + options.SetAuthorizationEndpointUris("/connect/authorize") + .SetTokenEndpointUris("/connect/token") + .SetUserInfoEndpointUris("/connect/userinfo") + .SetEndSessionEndpointUris("/connect/logout"); + + options.AllowAuthorizationCodeFlow() + .AllowPasswordFlow() + .AllowRefreshTokenFlow() + .AllowCustomFlow(OpenIddictAuthConstants.ExternalGrantType); + + options.RegisterScopes( + Scopes.OpenId, + Scopes.Email, + Scopes.Profile, + Scopes.Roles, + Scopes.OfflineAccess, + OpenIddictAuthConstants.ApiScope, + OpenIddictAuthConstants.RolesScope); + + ConfigureCryptography(options, configuration, environment); + + var aspNetCoreBuilder = options.UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableUserInfoEndpointPassthrough() + .EnableEndSessionEndpointPassthrough(); + + // Local HTTP only. In production nginx terminates TLS and + // ForwardedHeaders restores https:// for OpenIddict. + // Docker demo also serves plain HTTP (OpenIddict:AllowHttp / Environment=Docker). + if (environment.IsDevelopment() + || IsDockerEnvironment(environment) + || configuration.GetValue("OpenIddict:AllowHttp", false)) + { + aspNetCoreBuilder.DisableTransportSecurityRequirement(); + } + + // Password and external grants need custom handlers (separate registrations — + // a second UseScopedHandler() on the same builder replaces the first). + options.AddEventHandler(builder => + builder.UseScopedHandler()); + + options.AddEventHandler(builder => + builder.UseScopedHandler()); + }) + .AddValidation(options => + { + options.UseLocalServer(); + options.UseAspNetCore(); + }); + + services.AddAuthentication(options => + { + options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + }); + + services.AddHostedService(); + + return services; + } + + private static void ConfigureCryptography( + OpenIddictServerBuilder options, + IConfiguration configuration, + IHostEnvironment environment + ) + { + // Dev: certs in the user profile. Docker/demo: ephemeral keys (no cert store in containers). + if (string.IsNullOrWhiteSpace(configuration["OpenIddict:SigningKeyPath"]) + && (environment.IsDevelopment() + || IsDockerEnvironment(environment) + || configuration.GetValue("OpenIddict:UseEphemeralKeys", false))) + { + if (IsDockerEnvironment(environment) + || configuration.GetValue("OpenIddict:UseEphemeralKeys", false)) + { + options.AddEphemeralEncryptionKey() + .AddEphemeralSigningKey(); + } + else + { + options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); + } + + return; + } + + var contentRoot = configuration.GetValue(WebHostDefaults.ContentRootKey) + ?? throw new InvalidOperationException("Content root path is not configured."); + + var signingPath = ResolveKeyPath( + configuration["OpenIddict:SigningKeyPath"], + contentRoot, + "keys/signing.pem"); + var encryptionPath = ResolveKeyPath( + configuration["OpenIddict:EncryptionKeyPath"], + contentRoot, + "keys/encryption.pem"); + + if (!File.Exists(signingPath)) + { + throw new InvalidOperationException( + $"OpenIddict signing key not found at '{signingPath}'. " + + "Set OpenIddict:SigningKeyPath or place keys/signing.pem under the content root."); + } + + options.AddSigningKey(LoadRsaSecurityKey(signingPath)); + + if (File.Exists(encryptionPath) && + !string.Equals(encryptionPath, signingPath, StringComparison.OrdinalIgnoreCase)) + { + options.AddEncryptionKey(LoadRsaSecurityKey(encryptionPath)); + } + else + { + // Prefer a dedicated encryption key. Fallback keeps older single-key setups working. + options.AddEncryptionKey(LoadRsaSecurityKey(signingPath)); + } + } + + private static bool IsDockerEnvironment(IHostEnvironment environment) => + string.Equals(environment.EnvironmentName, "Docker", StringComparison.OrdinalIgnoreCase); + + private static string ResolveKeyPath(string? configuredPath, string contentRoot, string defaultRelative) + { + if (!string.IsNullOrWhiteSpace(configuredPath)) + { + return Path.IsPathRooted(configuredPath) + ? configuredPath + : Path.GetFullPath(Path.Combine(contentRoot, configuredPath)); + } + + return Path.GetFullPath(Path.Combine(contentRoot, defaultRelative)); + } + + private static RsaSecurityKey LoadRsaSecurityKey(string path) + { + var privateKey = File.ReadAllText(path) + .Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) + .Replace("-----END RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) + .Replace("-----BEGIN PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) + .Replace("-----END PRIVATE KEY-----", string.Empty, StringComparison.Ordinal) + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal); + + var rsa = RSA.Create(); + var keyBytes = Convert.FromBase64String(privateKey); + + try + { + rsa.ImportRSAPrivateKey(keyBytes, out _); + } + catch (CryptographicException) + { + rsa.ImportPkcs8PrivateKey(keyBytes, out _); + } + + return new RsaSecurityKey(rsa); + } +} diff --git a/MyOffice.Web/Identity/OpenIddict/PasswordGrantHandler.cs b/MyOffice.Web/Identity/OpenIddict/PasswordGrantHandler.cs new file mode 100644 index 0000000..ce48be5 --- /dev/null +++ b/MyOffice.Web/Identity/OpenIddict/PasswordGrantHandler.cs @@ -0,0 +1,70 @@ +namespace MyOffice.Web.Auth; + +using Identity.Domain; +using Identity.Repositories; +using Microsoft.AspNetCore.Identity; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +public sealed class PasswordGrantHandler : IOpenIddictServerHandler +{ + private readonly AppUserManager _userManager; + private readonly SignInManager> _signInManager; + + public PasswordGrantHandler( + AppUserManager userManager, + SignInManager> signInManager + ) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public async ValueTask HandleAsync(HandleTokenRequestContext context) + { + if (!string.Equals(context.Request.GrantType, GrantTypes.Password, StringComparison.Ordinal)) + return; + + var username = context.Request.Username?.Trim(); + if (string.IsNullOrWhiteSpace(username)) + { + context.Reject(Errors.InvalidGrant, "Invalid username or password."); + return; + } + + var user = await _userManager.FindByNameAsync(username) + ?? await _userManager.FindByEmailAsync(username); + if (user is null) + { + context.Reject(Errors.InvalidGrant, "Invalid username or password."); + return; + } + + if (!await _signInManager.CanSignInAsync(user)) + { + context.Reject(Errors.InvalidGrant, "The specified user cannot sign in."); + return; + } + + if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) + { + context.Reject(Errors.InvalidGrant, "The specified user is locked out."); + return; + } + + var result = await _signInManager.CheckPasswordSignInAsync(user, context.Request.Password!, lockoutOnFailure: true); + if (!result.Succeeded) + { + context.Reject(Errors.InvalidGrant, "Invalid username or password."); + return; + } + + if (_userManager.SupportsUserLockout) + await _userManager.ResetAccessFailedCountAsync(user); + + context.SignIn(OpenIddictClaimsHelper.CreatePrincipal(user, context.Request.GetScopes())); + } +} diff --git a/MyOffice.Web/Identity/Repositories/AppUserManager.cs b/MyOffice.Web/Identity/Repositories/AppUserManager.cs new file mode 100644 index 0000000..bbdf67e --- /dev/null +++ b/MyOffice.Web/Identity/Repositories/AppUserManager.cs @@ -0,0 +1,133 @@ +namespace MyOffice.Web.Identity.Repositories; + +using System.Security.Cryptography; +using Domain; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +/// +/// Identity V3 hasher for new passwords; still verifies the legacy +/// (16-byte salt + 20-byte PBKDF2-SHA1 @ 100k) format and signals rehash. +/// +public class PasswordHasher : IPasswordHasher> +{ + private const int LegacySaltSize = 16; + private const int LegacyHashSize = 20; + private const int LegacyIterations = 100_000; + private const int LegacyPayloadSize = LegacySaltSize + LegacyHashSize; + + private readonly PasswordHasher> _identityHasher = new(); + + public string HashPassword(ApplicationUser user, string password) => + _identityHasher.HashPassword(user, password); + + public PasswordVerificationResult VerifyHashedPassword( + ApplicationUser user, + string hashedPassword, + string providedPassword + ) + { + if (string.IsNullOrEmpty(hashedPassword) || providedPassword == null) + { + return PasswordVerificationResult.Failed; + } + + // Prefer modern Identity format when payload is not the legacy 36-byte blob. + if (!IsLegacyPayload(hashedPassword)) + { + return _identityHasher.VerifyHashedPassword(user, hashedPassword, providedPassword); + } + + if (VerifyLegacyHash(providedPassword, hashedPassword)) + { + return PasswordVerificationResult.SuccessRehashNeeded; + } + + return PasswordVerificationResult.Failed; + } + + private static bool IsLegacyPayload(string hashedPassword) + { + try + { + var bytes = Convert.FromBase64String(hashedPassword); + return bytes.Length == LegacyPayloadSize; + } + catch (FormatException) + { + return false; + } + } + + internal static string HashLegacyForTests(string password) + { + var salt = RandomNumberGenerator.GetBytes(LegacySaltSize); + var hash = Rfc2898DeriveBytes.Pbkdf2( + password, + salt, + LegacyIterations, + HashAlgorithmName.SHA1, + LegacyHashSize); + + var payload = new byte[LegacyPayloadSize]; + Buffer.BlockCopy(salt, 0, payload, 0, LegacySaltSize); + Buffer.BlockCopy(hash, 0, payload, LegacySaltSize, LegacyHashSize); + return Convert.ToBase64String(payload); + } + + private static bool VerifyLegacyHash(string password, string passwordHash) + { + byte[] hashBytes; + try + { + hashBytes = Convert.FromBase64String(passwordHash); + } + catch (FormatException) + { + return false; + } + + if (hashBytes.Length != LegacyPayloadSize) + { + return false; + } + + var salt = hashBytes.AsSpan(0, LegacySaltSize); + var expected = hashBytes.AsSpan(LegacySaltSize, LegacyHashSize); + var actual = Rfc2898DeriveBytes.Pbkdf2( + password, + salt, + LegacyIterations, + HashAlgorithmName.SHA1, + LegacyHashSize); + + return CryptographicOperations.FixedTimeEquals(expected, actual); + } +} + +public class AppUserManager : UserManager> +{ + public AppUserManager( + IUserStore> store, + IOptions optionsAccessor, + IPasswordHasher> passwordHasher, + IEnumerable>> userValidators, + IEnumerable>> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger>> logger) : + base( + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger + ) + { + } +} diff --git a/MyOffice.Web/Identity/Repositories/RoleStore.cs b/MyOffice.Web/Identity/Repositories/RoleStore.cs new file mode 100644 index 0000000..285f433 --- /dev/null +++ b/MyOffice.Web/Identity/Repositories/RoleStore.cs @@ -0,0 +1,62 @@ +namespace MyOffice.Web.Identity.Repositories; + +using Domain; +using Microsoft.AspNetCore.Identity; + +public class RoleStore : IRoleStore +{ + public void Dispose() + { + } + + public Task CreateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Identity/Repositories/UserStore.cs b/MyOffice.Web/Identity/Repositories/UserStore.cs new file mode 100644 index 0000000..881d42a --- /dev/null +++ b/MyOffice.Web/Identity/Repositories/UserStore.cs @@ -0,0 +1,198 @@ +namespace MyOffice.Web.Identity.Repositories; + +using Data.Models.Users; +using Data.Repositories.Users; +using Domain; +using Microsoft.AspNetCore.Identity; + +public class UserStore : + IUserStore>, + IUserPasswordStore>, + IUserEmailStore> +{ + private readonly IUserRepository _userRepository; + + public UserStore( + IUserRepository userRepository + ) + { + _userRepository = userRepository; + } + + public void Dispose() + { + } + + public Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.Id.ToString()); + } + + public Task GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.UserName); + } + + public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, + CancellationToken cancellationToken) + { + user.UserName = normalizedName; + return Task.FromResult(0); + } + + public Task CreateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + var result = _userRepository.AddUser(new User + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + PasswordHash = user.PasswordHash, + FirstName = user.FirstName, + LastName = user.LastName, + FullName = user.FullName, + CurrencyId = user.CurrencyId!, + IsEmailConfirmed = user.IsEmailConfirmed + }); + + return Task.FromResult(result == 1 ? IdentityResult.Success : IdentityResult.Failed()); + } + + public Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + var userDb = new User + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + PasswordHash = user.PasswordHash, + FirstName = user.FirstName, + LastName = user.LastName, + FullName = user.FullName, + CurrencyId = user.CurrencyId!, + IsEmailConfirmed = user.IsEmailConfirmed + }; + + var result = _userRepository.UpdateUser(userDb); + return Task.FromResult(result == 1 ? IdentityResult.Success : IdentityResult.Failed()); + } + + public Task DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public async Task> FindByIdAsync(string userId, CancellationToken cancellationToken) + { + var user = await _userRepository.GetUserAsync(Guid.Parse(userId)); + if (user == null) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + + return new ApplicationUser + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + PasswordHash = user.PasswordHash, + FirstName = user.FirstName, + LastName = user.LastName, + FullName = user.FullName, + CurrencyId = user.CurrencyId, + IsEmailConfirmed = user.IsEmailConfirmed + }; + } + + public async Task> FindByNameAsync(string normalizedUserName, + CancellationToken cancellationToken) + { + var user = await _userRepository.GetByUserUserNameAsync(normalizedUserName); + if (user == null) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + + return new ApplicationUser + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + PasswordHash = user.PasswordHash, + FirstName = user.FirstName, + LastName = user.LastName, + FullName = user.FullName, + CurrencyId = user.CurrencyId, + IsEmailConfirmed = user.IsEmailConfirmed + }; + } + + public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, + CancellationToken cancellationToken) + { + user.PasswordHash = passwordHash; + return Task.FromResult(0); + } + + public Task GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.PasswordHash); + } + + public Task HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash)); + } + + public Task SetEmailAsync(ApplicationUser user, string email, CancellationToken cancellationToken) + { + user.Email = email; + return Task.CompletedTask; + } + + public Task GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.Email); + } + + public Task GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.IsEmailConfirmed); + } + + public Task SetEmailConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken) + { + user.IsEmailConfirmed = confirmed; + return Task.CompletedTask; + } + + public Task> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + return FindByNameAsync(normalizedEmail, cancellationToken); + } + + public Task GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user?.Email); + } + + public Task SetNormalizedEmailAsync(ApplicationUser user, string normalizedEmail, + CancellationToken cancellationToken) + { + return Task.FromResult(0); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Infrastructure/Attributes/AsGuidAttribute.cs b/MyOffice.Web/Infrastructure/Attributes/AsGuidAttribute.cs new file mode 100644 index 0000000..1439835 --- /dev/null +++ b/MyOffice.Web/Infrastructure/Attributes/AsGuidAttribute.cs @@ -0,0 +1,35 @@ +namespace MyOffice.Web.Infrastructure.Attributes +{ + using System.ComponentModel.DataAnnotations; + using Core.Extensions; + + public class AsGuidAttribute: ValidationAttribute + { + private readonly bool _nullable; + + public AsGuidAttribute(bool nullable = false) + { + _nullable = nullable; + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value == null && _nullable) + { + return ValidationResult.Success; + } + + if (value == null || value.ToString().IsMissing()) + { + return new ValidationResult("Id parameter is not valid"); + } + + if (!Guid.TryParse(value.ToString(), out _)) + { + return new ValidationResult("Id parameter is not valid"); + } + + return ValidationResult.Success; + } + } +} diff --git a/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs b/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs new file mode 100644 index 0000000..4ef604e --- /dev/null +++ b/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs @@ -0,0 +1,71 @@ +namespace MyOffice.Web.Infrastructure.Attributes; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class DefaultFromBodyAttribute : Attribute +{ +} + +public class DefaultFromBodyBindingConvention : IActionModelConvention +{ + public void Apply(ActionModel action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (action.Controller.Attributes.Any(x => x is DefaultFromBodyAttribute)) + { + foreach (var parameter in action.Parameters) + { + if (parameter.Attributes.Any(x => + x is FromQueryAttribute + or FromRouteAttribute + or FromHeaderAttribute + or FromFormAttribute + or FromServicesAttribute)) + { + continue; + } + + // Already bound (e.g. CancellationToken → Special) — do not force Body. + if (parameter.BindingInfo?.BindingSource is { } existing + && existing != BindingSource.ModelBinding + && existing != BindingSource.Custom) + { + continue; + } + + var paramType = parameter.ParameterInfo.ParameterType; + if (paramType == typeof(CancellationToken) + || Nullable.GetUnderlyingType(paramType) == typeof(CancellationToken)) + { + continue; + } + + var isSimpleType = paramType.IsPrimitive + || paramType.IsEnum + || paramType == typeof(string) + || paramType == typeof(decimal) + || paramType == typeof(Guid) + || paramType == typeof(Guid?) + || paramType == typeof(DateTime) + || paramType == typeof(DateTime?) + || paramType == typeof(DateOnly) + || paramType == typeof(DateOnly?) + || paramType == typeof(TimeOnly) + || paramType == typeof(TimeOnly?); + + if (!isSimpleType) + { + parameter.BindingInfo ??= new BindingInfo(); + parameter.BindingInfo.BindingSource = BindingSource.Body; + } + } + } + } +} diff --git a/MyOffice.Web/Infrastructure/Attributes/GlobalModelStateValidatorAttribute.cs b/MyOffice.Web/Infrastructure/Attributes/GlobalModelStateValidatorAttribute.cs new file mode 100644 index 0000000..fa8d645 --- /dev/null +++ b/MyOffice.Web/Infrastructure/Attributes/GlobalModelStateValidatorAttribute.cs @@ -0,0 +1,28 @@ +namespace MyOffice.Web.Infrastructure.Attributes; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +public class GlobalModelStateValidatorAttribute : ActionFilterAttribute +{ + private readonly ProblemDetailsFactory _problemDetailsFactory; + + public GlobalModelStateValidatorAttribute(ProblemDetailsFactory problemDetailsFactory) + { + _problemDetailsFactory = problemDetailsFactory; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new ObjectResult(_problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState)) + { + StatusCode = StatusCodes.Status400BadRequest + }; + } + + base.OnActionExecuting(context); + } +} diff --git a/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs b/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs new file mode 100644 index 0000000..9b37b12 --- /dev/null +++ b/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs @@ -0,0 +1,95 @@ +namespace MyOffice.Web.Infrastructure; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +public class CustomProblemDetailsFactory : ProblemDetailsFactory +{ + private readonly ApiBehaviorOptions options; + private readonly JsonOptions jsonOptions; + + public CustomProblemDetailsFactory(IOptions options, IOptions jsonOptions) + { + this.options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + this.jsonOptions = jsonOptions?.Value ?? throw new ArgumentNullException(nameof(jsonOptions)); + } + + public override ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + statusCode ??= 500; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + public override ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + if (modelStateDictionary == null) + { + throw new ArgumentNullException(nameof(modelStateDictionary)); + } + + statusCode ??= 400; + + var errors = modelStateDictionary + .Where(x => x.Value?.Errors.Any() == true) + .ToDictionary( + kvp => jsonOptions?.JsonSerializerOptions?.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key, + kvp => kvp.Value!.Errors.Select(x => x.ErrorMessage).ToArray() + ); + + var problemDetails = new ValidationProblemDetails(errors) + { + Status = statusCode, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (title != null) + { + // For validation problem details, don't overwrite the default title with null. + problemDetails.Title = title; + } + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + } +} diff --git a/MyOffice.Web/Infrastructure/DatabaseInitializerHostedService.cs b/MyOffice.Web/Infrastructure/DatabaseInitializerHostedService.cs new file mode 100644 index 0000000..b18e96a --- /dev/null +++ b/MyOffice.Web/Infrastructure/DatabaseInitializerHostedService.cs @@ -0,0 +1,33 @@ +namespace MyOffice.Web.Infrastructure; + +using DbContext; + +/// +/// Runs EF migrate + seed on startup with a scoped DbContext. +/// +public sealed class DatabaseInitializerHostedService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public DatabaseInitializerHostedService( + IServiceProvider serviceProvider, + ILogger logger + ) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + _logger.LogInformation("Applying database migrations and seed data…"); + await DatabaseBootstrapper.InitializeAsync(db, cancellationToken); + _logger.LogInformation("Database ready."); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/MyOffice.Web/Infrastructure/Filters/ResponseFilter.cs b/MyOffice.Web/Infrastructure/Filters/ResponseFilter.cs new file mode 100644 index 0000000..dda3f3d --- /dev/null +++ b/MyOffice.Web/Infrastructure/Filters/ResponseFilter.cs @@ -0,0 +1,47 @@ +namespace MyOffice.Web.Infrastructure.Filters; + +using System.Collections; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using MyOffice.Web.Models; + +public class ResponseFilter : IActionFilter +{ + private readonly ILogger _logger; + + public ResponseFilter(ILogger logger) + { + _logger = logger; + } + + public void OnActionExecuting(ActionExecutingContext context) + { + } + + public void OnActionExecuted(ActionExecutedContext context) + { + // TODO: restore + /*var route = $"{context.Controller.GetType().Name}.{context.ActionDescriptor.DisplayName}"; + if (context.Result == null) + throw new NotSupportedException($"[{route}] Response required"); + if (!(context.Result is ObjectResult result)) + throw new NotSupportedException($"[{route}] Response must be an ObjectResult - {context.Result?.GetType().Name}"); + if (result.Value == null) + throw new NotSupportedException($"[{route}] Response must be an ObjectResult with value"); + + if (result.Value is IResponseModel) + { + return; + } + + var type = result.Value.GetType(); + if (type.IsGenericType + && result.Value is IEnumerable + && type.GenericTypeArguments.Any(x => x.GetInterfaces().Any(y => y == typeof(IResponseModel)))) + { + return; + } + + throw new NotSupportedException($"[{route}] Response must be an ObjectResult with an IResponseModel - {result.Value?.GetType().Name}");*/ + } +} \ No newline at end of file diff --git a/MyOffice.Web/Infrastructure/GlobalSettings.cs b/MyOffice.Web/Infrastructure/GlobalSettings.cs new file mode 100644 index 0000000..afa20c2 --- /dev/null +++ b/MyOffice.Web/Infrastructure/GlobalSettings.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Web.Infrastructure; + +public class GlobalSettings +{ + public string? Host { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Infrastructure/RouteHelper.cs b/MyOffice.Web/Infrastructure/RouteHelper.cs new file mode 100644 index 0000000..d021e0d --- /dev/null +++ b/MyOffice.Web/Infrastructure/RouteHelper.cs @@ -0,0 +1,50 @@ +namespace MyOffice.Web.Infrastructure; + +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +public static class RouteHelperExtensions +{ + public static void Bind(this IRouteBuilder routeBuilder, Expression> expression) + { + System.Console.WriteLine("Bind"); + System.Console.WriteLine(expression.Name); + System.Console.WriteLine(expression.Body.ToString()); + routeBuilder.MapRoute("SettingsAccountAccountsGet", "api/settings/accounts", new { controller = "SettingsAccount", action = "AccountsGet" }); + } +} + +public class CustomRouter : IRouter +{ + private readonly IRouter _defaultRouter; + private readonly string _controller; + private readonly string _action; + + public CustomRouter(IRouter defaultRouter, string controller, string action) + { + _defaultRouter = defaultRouter; + _controller = controller; + _action = action; + } + + public VirtualPathData? GetVirtualPath(VirtualPathContext context) + { + Console.WriteLine($"1:{context.RouteName}"); + return null; + } + + public async Task RouteAsync(RouteContext context) + { + var headers = context.HttpContext.Request.Headers; + var path = context.HttpContext.Request.Path.Value!.Split('/'); + + Console.WriteLine($"CustomRouter:{context.HttpContext.Request.Path.Value}"); + + context.RouteData.Values["controller"] = _controller; + context.RouteData.Values["action"] = _action; + + await _defaultRouter.RouteAsync(context); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Infrastructure/UnhandledExceptionHandler.cs b/MyOffice.Web/Infrastructure/UnhandledExceptionHandler.cs new file mode 100644 index 0000000..24cf6b8 --- /dev/null +++ b/MyOffice.Web/Infrastructure/UnhandledExceptionHandler.cs @@ -0,0 +1,48 @@ +namespace MyOffice.Web.Infrastructure; + +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +/// +/// Returns ProblemDetails for unhandled exceptions (replaces missing /Error page). +/// +public sealed class UnhandledExceptionHandler : IExceptionHandler +{ + private readonly IHostEnvironment _environment; + private readonly ILogger _logger; + private readonly ProblemDetailsFactory _problemDetailsFactory; + + public UnhandledExceptionHandler( + IHostEnvironment environment, + ILogger logger, + ProblemDetailsFactory problemDetailsFactory + ) + { + _environment = environment; + _logger = logger; + _problemDetailsFactory = problemDetailsFactory; + } + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken + ) + { + _logger.LogError(exception, "Unhandled exception for {Method} {Path}", + httpContext.Request.Method, + httpContext.Request.Path); + + var problem = _problemDetailsFactory.CreateProblemDetails( + httpContext, + statusCode: StatusCodes.Status500InternalServerError, + title: "An unexpected error occurred.", + detail: _environment.IsDevelopment() ? exception.Message : null); + + httpContext.Response.StatusCode = problem.Status ?? StatusCodes.Status500InternalServerError; + httpContext.Response.ContentType = "application/problem+json"; + await httpContext.Response.WriteAsJsonAsync(problem, cancellationToken); + return true; + } +} diff --git a/MyOffice.Web/Models/Account/AccessRightsViewModel.cs b/MyOffice.Web/Models/Account/AccessRightsViewModel.cs new file mode 100644 index 0000000..108f2f8 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccessRightsViewModel.cs @@ -0,0 +1,41 @@ +namespace MyOffice.Web.Models.Account; + +using AutoMapper; +using MyOffice.Services.Account.Domain; +using MyOffice.Services.Identity; +using User; + +public class AccessRightsViewModel +{ + public UserViewModel? User { get; set; } = null!; + public bool AllowRead { get; set; } + public bool AllowWrite { get; set; } + public bool AllowManage { get; set; } + public bool AllowDelete { get; set; } + public bool IsOwner { get; set; } +} + +public class AccessRightsViewModelProfile : Profile +{ + public AccessRightsViewModelProfile() + { + CreateMap() + .AfterMap() + ; + + } +} + +public class AccessRightsViewModelMappingAction : IMappingAction +{ + private readonly IContextProvider _contextProvider; + + public AccessRightsViewModelMappingAction(IContextProvider contextProvider) + { + _contextProvider = contextProvider; + } + + public void Process(AccountAccessDto source, AccessRightsViewModel destination, ResolutionContext context) + { + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/AccountAccessInviteViewModel.cs b/MyOffice.Web/Models/Account/AccountAccessInviteViewModel.cs new file mode 100644 index 0000000..b691570 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountAccessInviteViewModel.cs @@ -0,0 +1,21 @@ +namespace MyOffice.Web.Models.Account; + +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class AccountAccessInviteViewModel: IResponseModel +{ + public string? Id { get; set; } + public string? Account { get; set; } + public bool? AllowWrite { get; set; } +} + +public class AccountAccessInviteViewModelProfile: Profile +{ + public AccountAccessInviteViewModelProfile() + { + CreateMap() + .ForMember(x => x.AllowWrite, o => o.MapFrom(x => x.IsAllowWrite)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/AccountAccessViewModel.cs b/MyOffice.Web/Models/Account/AccountAccessViewModel.cs new file mode 100644 index 0000000..90046b7 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountAccessViewModel.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using MyOffice.Services.Account.Domain; + +namespace MyOffice.Web.Models.Account; + +public class AccountAccessViewModel +{ + public class AccountAccessItemViewModel + { + public string UserId { get; set; } = null!; + public bool AllowWrite { get; set; } + } + + public string? Email { get; set; } + public bool AllowWrite { get; set; } + public List Accesses { get; set; } = null!; +} + +public class AccountAccessItemViewModelProfile : Profile +{ + public AccountAccessItemViewModelProfile() + { + } +} diff --git a/MyOffice.Web/Models/Account/AccountCategoryViewModel.cs b/MyOffice.Web/Models/Account/AccountCategoryViewModel.cs new file mode 100644 index 0000000..b36df26 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountCategoryViewModel.cs @@ -0,0 +1,30 @@ +namespace MyOffice.Web.Models.Account; + +using System.ComponentModel.DataAnnotations; +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class AccountCategoryViewModel: IResponseModel +{ + public string? Id { get; set; } + + [Required] + public string? Name { get; set; } + + public bool? AllowDelete { get; set; } +} + +public class AccountCategoryViewModelProfile: Profile +{ + public AccountCategoryViewModelProfile() + { + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(x => x.CategoryId)) + .ForMember(x => x.Name, o => o.MapFrom(x => x.Category!.Name)) + ; + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(x => x.Id)) + .ForMember(x => x.Name, o => o.MapFrom(x => x.Name)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/AccountDetailedViewModel.cs b/MyOffice.Web/Models/Account/AccountDetailedViewModel.cs new file mode 100644 index 0000000..63b1448 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountDetailedViewModel.cs @@ -0,0 +1,20 @@ +namespace MyOffice.Web.Models.Account; + +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class AccountDetailedViewModel: BaseViewModel +{ + public AccountViewModel Account { get; set; } = null!; + public decimal Rest { get; set; } +} + +public class AccountDetailedViewModelProfile: Profile +{ + public AccountDetailedViewModelProfile() + { + CreateMap() + .ForMember(x => x.Account, o => o.MapFrom(x => x.Account)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/AccountEditRequestModel.cs b/MyOffice.Web/Models/Account/AccountEditRequestModel.cs new file mode 100644 index 0000000..07ee7c9 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountEditRequestModel.cs @@ -0,0 +1,14 @@ +namespace MyOffice.Web.Models.Account; + +using System.ComponentModel.DataAnnotations; + +public class AccountEditRequestModel +{ + [Required] + public string Name { get; set; } = null!; + [Required] + public string CurrencyId { get; set; } = null!; + public string? CategoryId { get; set; } + public string? UserId { get; set; } + public string? Type { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/AccountInviteAcceptRequest.cs b/MyOffice.Web/Models/Account/AccountInviteAcceptRequest.cs new file mode 100644 index 0000000..246160f --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountInviteAcceptRequest.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Web.Models.Account; + +public class AccountInviteAcceptRequest +{ + public string Name { get; set; } = null!; +} diff --git a/MyOffice.Web/Models/Account/AccountViewModel.cs b/MyOffice.Web/Models/Account/AccountViewModel.cs new file mode 100644 index 0000000..5ee5a51 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountViewModel.cs @@ -0,0 +1,61 @@ +namespace MyOffice.Web.Models.Account; + +using AutoMapper; +using MyOffice.Services.Account.Domain; +using MyOffice.Services.Identity; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; + +public class AccountViewModel: IResponseModel +{ + public string? Id { get; set; } + + [Required] + public string Name { get; set; } = null!; + public string Type { get; set; } = null!; + + [Required] + public string CurrencyId { get; set; } = null!; + public string? CurrencyName { get; set; } + + public List? Categories { get; set; } + + [Required] + public string CategoryId { get; set; } = null!; + + #region Permissions + + public List? AccessRights { get; set; } + public bool AllowRead { get; set; } + public bool AllowWrite { get; set; } + public bool AllowDelete { get; set; } + public bool AllowManage { get; set; } + + #endregion Permissions +} + +public class AccountViewModelProfile : Profile +{ + public AccountViewModelProfile() + { + CreateMap() + .ForMember(x => x.CurrencyId, o => o.MapFrom(x => x.CurrencyGlobalId)) + .ForMember(x => x.CurrencyName, o => o.MapFrom(x => x.Currency!.Name)) + .AfterMap() + ; + } +} + +public class AccountViewModelMappingAction : IMappingAction +{ + private readonly IContextProvider _contextProvider; + + public AccountViewModelMappingAction(IContextProvider contextProvider) + { + _contextProvider = contextProvider; + } + + public void Process(AccountDto source, AccountViewModel destination, ResolutionContext context) + { + } +} diff --git a/MyOffice.Web/Models/Account/AccountViewModelProfile.cs b/MyOffice.Web/Models/Account/AccountViewModelProfile.cs new file mode 100644 index 0000000..5a8b582 --- /dev/null +++ b/MyOffice.Web/Models/Account/AccountViewModelProfile.cs @@ -0,0 +1,48 @@ +namespace MyOffice.Web.Models.Account; + +using AutoMapper; + +using MyOffice.Services.Currency.Domain; +using Currency; +using Item; +using Motion; +using Services.Account.Domain; +using User; +using MyOffice.Services.Identity; + +/*public class CustomResolver : IValueResolver +{ + private readonly ILogger _logger; + private readonly IContextProvider _contextProvider; + + public CustomResolver( + ILogger logger, + IContextProvider contextProvider + ) + { + _logger = logger; + _contextProvider = contextProvider; + } + + public bool Resolve(AccountAccessDto source, AccessRightsViewModel destination, bool member, ResolutionContext context) + { + return source.OwnerId == _contextProvider.UserId; + } +}*/ + +/*public class PublicationSystemResolver : IMemberValueResolver +{ + private readonly IContextProvider _contextProvider; + + public PublicationSystemResolver( + IContextProvider contextProvider + ) + { + this._contextProvider = contextProvider; + } + + public bool Resolve(object source, object destination, string sourceMember, bool destMember, ResolutionContext context) + { + return true; + } +}*/ \ No newline at end of file diff --git a/MyOffice.Web/Models/Account/MotionsGetRequest.cs b/MyOffice.Web/Models/Account/MotionsGetRequest.cs new file mode 100644 index 0000000..4c7bcd3 --- /dev/null +++ b/MyOffice.Web/Models/Account/MotionsGetRequest.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Web.Models.Account +{ + using System.ComponentModel.DataAnnotations; + + public class MotionsGetRequest + { + [Required] + public DateTime From { get; set; } + [Required] + public DateTime To { get; set; } + } +} diff --git a/MyOffice.Web/Models/Auth/LoginModel.cs b/MyOffice.Web/Models/Auth/LoginModel.cs new file mode 100644 index 0000000..7127c9c --- /dev/null +++ b/MyOffice.Web/Models/Auth/LoginModel.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Web.Models.Auth; + +public class LoginModel +{ + public string? UserName { get; set; } + public string? Password { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Auth/RegisterModel.cs b/MyOffice.Web/Models/Auth/RegisterModel.cs new file mode 100644 index 0000000..086c364 --- /dev/null +++ b/MyOffice.Web/Models/Auth/RegisterModel.cs @@ -0,0 +1,18 @@ +namespace MyOffice.Web.Models.Auth; + +using System.ComponentModel.DataAnnotations; + +public class RegisterModel +{ + [Required] + [EmailAddress] + public string UserName { get; set; } = null!; + + [Required] + [MinLength(8)] + public string Password { get; set; } = null!; + + [Required] + [Compare(nameof(Password))] + public string ConfirmPassword { get; set; } = null!; +} diff --git a/MyOffice.Web/Models/BaseViewModel.cs b/MyOffice.Web/Models/BaseViewModel.cs new file mode 100644 index 0000000..8a65a94 --- /dev/null +++ b/MyOffice.Web/Models/BaseViewModel.cs @@ -0,0 +1,6 @@ +namespace MyOffice.Web.Models; + +public class BaseViewModel : IResponseModel +{ + +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Currency/CurrencyAddModel.cs b/MyOffice.Web/Models/Currency/CurrencyAddModel.cs new file mode 100644 index 0000000..cdb00e5 --- /dev/null +++ b/MyOffice.Web/Models/Currency/CurrencyAddModel.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Web.Models.Currency +{ + public class CurrencyAddModel + { + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public int Quantity { get; set; } + public decimal Rate { get; set; } + public DateTime RateDate { get; set; } + } + + public class CurrencyRateModel + { + public int Quantity { get; set; } + public decimal Rate { get; set; } + public DateTime RateDate { get; set; } + } +} diff --git a/MyOffice.Web/Models/Currency/CurrencyEditModel.cs b/MyOffice.Web/Models/Currency/CurrencyEditModel.cs new file mode 100644 index 0000000..412607a --- /dev/null +++ b/MyOffice.Web/Models/Currency/CurrencyEditModel.cs @@ -0,0 +1,11 @@ +namespace MyOffice.Web.Models.Currency; + +public class CurrencyEditModel +{ + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public int Quantity { get; set; } + public decimal Rate { get; set; } + public DateTime RateDate { get; set; } + public bool IsPrimary { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Currency/CurrencyGlobalViewModel.cs b/MyOffice.Web/Models/Currency/CurrencyGlobalViewModel.cs new file mode 100644 index 0000000..6b71f9a --- /dev/null +++ b/MyOffice.Web/Models/Currency/CurrencyGlobalViewModel.cs @@ -0,0 +1,22 @@ +/// +/// +namespace MyOffice.Web.Models.Currency; + +using AutoMapper; +using MyOffice.Services.Currency.Domain; + +public class CurrencyGlobalViewModel: IResponseModel +{ + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + public int Quantity { get; set; } + public string Symbol { get; set; } = null!; +} + +public class CurrencyGlobalViewModelProfile : Profile +{ + public CurrencyGlobalViewModelProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Currency/CurrencyRateViewModel.cs b/MyOffice.Web/Models/Currency/CurrencyRateViewModel.cs new file mode 100644 index 0000000..0b6f26c --- /dev/null +++ b/MyOffice.Web/Models/Currency/CurrencyRateViewModel.cs @@ -0,0 +1,20 @@ +namespace MyOffice.Web.Models.Currency; + +using AutoMapper; +using MyOffice.Services.Currency.Domain; + +public class CurrencyRateViewModel: BaseViewModel +{ + public string Currency { get; set; } = null!; + public DateTime DateTime { get; set; } + public int Quantity { get; set; } + public decimal Rate { get; set; } +} + +public class CurrencyRateViewModelProfile: Profile +{ + public CurrencyRateViewModelProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Currency/CurrencyViewModel.cs b/MyOffice.Web/Models/Currency/CurrencyViewModel.cs new file mode 100644 index 0000000..efade2f --- /dev/null +++ b/MyOffice.Web/Models/Currency/CurrencyViewModel.cs @@ -0,0 +1,36 @@ +namespace MyOffice.Web.Models.Currency; + +using AutoMapper; +using MyOffice.Services.Currency.Domain; + +public class CurrencyViewModel: BaseViewModel +{ + public string Id { get; set; } = null!; + public string Code { get; set; } = null!; + public string Symbol { get; set; } = null!; + public string Name { get; set; } = null!; + public string ShortName { get; set; } = null!; + public decimal? Rate { get; set; } + public int? Quantity { get; set; } + public DateTime? RateDate { get; set; } + public bool IsPrimary { get; set; } +} + +public class CurrencyViewModelProfile: Profile +{ + public CurrencyViewModelProfile() + { + CreateMap(); + + CreateMap() + .ForMember(x => x.Id, o => o.MapFrom(x => x.Currency.Id)) + .ForMember(x => x.Code, o => o.MapFrom(x => x.Currency.CurrencyGlobalId)) + .ForMember(x => x.Name, o => o.MapFrom(x => x.Currency.Name)) + .ForMember(x => x.ShortName, o => o.MapFrom(x => x.Currency.ShortName)) + .ForMember(x => x.Rate, o => o.MapFrom(x => x.Rate == null ? (decimal?)null : x.Rate.Rate)) + .ForMember(x => x.IsPrimary, o => o.MapFrom(x => x.Currency.IsPrimary)) + .ForMember(x => x.Quantity, o => o.MapFrom(x => x.Rate == null ? (int?)null : x.Rate.Quantity)) + .ForMember(x => x.RateDate, o => o.MapFrom(x => x.Rate == null ? (DateTime?)null : x.Rate.DateTime)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Dashboard/DashboardIncomeDataViewModel.cs b/MyOffice.Web/Models/Dashboard/DashboardIncomeDataViewModel.cs new file mode 100644 index 0000000..8af7fb0 --- /dev/null +++ b/MyOffice.Web/Models/Dashboard/DashboardIncomeDataViewModel.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Web.Models.Dashboard; + +using AutoMapper; +using MyOffice.Services.Dashboard.Domain; + +public class DashboardIncomeDataViewModel : DashboardIncomeData, IResponseModel +{ + +} + +public class DashboardIncomeDataViewModelProfile : Profile +{ + public DashboardIncomeDataViewModelProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Dashboard/DashboardViewModel.cs b/MyOffice.Web/Models/Dashboard/DashboardViewModel.cs new file mode 100644 index 0000000..42d4b46 --- /dev/null +++ b/MyOffice.Web/Models/Dashboard/DashboardViewModel.cs @@ -0,0 +1,17 @@ +namespace MyOffice.Web.Models.Dashboard; + +using AutoMapper; +using MyOffice.Services.Dashboard.Domain; + +public class DashboardViewModel : DashboardData, IResponseModel +{ + +} + +public class DashboardViewModelProfile : Profile +{ + public DashboardViewModelProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/IRequestModel.cs b/MyOffice.Web/Models/IRequestModel.cs new file mode 100644 index 0000000..95ac4db --- /dev/null +++ b/MyOffice.Web/Models/IRequestModel.cs @@ -0,0 +1,5 @@ +namespace MyOffice.Web.Models; + +public interface IRequestModel +{ +} diff --git a/MyOffice.Web/Models/IResponseModel.cs b/MyOffice.Web/Models/IResponseModel.cs new file mode 100644 index 0000000..c1948e0 --- /dev/null +++ b/MyOffice.Web/Models/IResponseModel.cs @@ -0,0 +1,5 @@ +namespace MyOffice.Web.Models; + +public interface IResponseModel +{ +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemCategoryViewModel.cs b/MyOffice.Web/Models/Item/ItemCategoryViewModel.cs new file mode 100644 index 0000000..d6596d5 --- /dev/null +++ b/MyOffice.Web/Models/Item/ItemCategoryViewModel.cs @@ -0,0 +1,45 @@ +namespace MyOffice.Web.Models.Item; + +using System.ComponentModel.DataAnnotations; +using AutoMapper; +using Services.Account.Domain; + +public class ItemCategoryViewModel: IResponseModel +{ + public string? Id { get; set; } + + [Required] + public string Name { get; set; } = null!; + + public bool AllowDelete { get; set; } + public int SortOrder { get; set; } + public bool Internal { get; set; } +} + +//TODO: REMOVE +public static class ItemCategoryViewModelExtensions +{ + public static ItemCategoryDto FromModel(this ItemCategoryViewModel input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + return new ItemCategoryDto + { + Name = input.Name, + IsInternal = input.Internal, + }; + } +} + +public class ItemCategoryViewModelProfile: Profile +{ + public ItemCategoryViewModelProfile() + { + CreateMap() + .ForMember(x => x.Internal, o => o.MapFrom(x => x.IsInternal)) + .ForMember(x => x.AllowDelete, o => o.MapFrom(x => !x.Items.Any() && x.Id != x.UserId)) + .ForMember(x => x.SortOrder, o => o.MapFrom(x => x.Id == x.UserId ? 1 : 0)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs b/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs new file mode 100644 index 0000000..104544d --- /dev/null +++ b/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Web.Models.Item; + +public class ItemChangeCategoryModel +{ + public string category { get; set; } = null!; + public List Items { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemEditModel.cs b/MyOffice.Web/Models/Item/ItemEditModel.cs new file mode 100644 index 0000000..43f161a --- /dev/null +++ b/MyOffice.Web/Models/Item/ItemEditModel.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Web.Models.Item; + +public class ItemEditModel +{ + public string Id { get; set; } = null!; + public string Category { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemViewModel.cs b/MyOffice.Web/Models/Item/ItemViewModel.cs new file mode 100644 index 0000000..33a0a12 --- /dev/null +++ b/MyOffice.Web/Models/Item/ItemViewModel.cs @@ -0,0 +1,29 @@ +namespace MyOffice.Web.Models.Item; + +using System.ComponentModel.DataAnnotations; +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class ItemViewModel : BaseViewModel +{ + public string? Id { get; set; } + [Required] + public string? Name { get; set; } + + public bool AllowDelete { get; set; } + + public string CategoryId { get; set; } = null!; + public string? Category { get; set; } = null!; + + public string? AccountId { get; set; } +} + +public class ItemViewModelProfile: Profile +{ + public ItemViewModelProfile() + { + CreateMap() + .ForMember(x => x.Category, o => o.MapFrom(x => x.Category!.Name)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Motion/AccountControllerProfile.cs b/MyOffice.Web/Models/Motion/AccountControllerProfile.cs new file mode 100644 index 0000000..c976d20 --- /dev/null +++ b/MyOffice.Web/Models/Motion/AccountControllerProfile.cs @@ -0,0 +1,18 @@ +namespace MyOffice.Web.Models.Motion; + +using AutoMapper; +using Data.Models.Accounts; +using Services.Account.Domain; + +public class AccountControllerProfile : Profile +{ + public AccountControllerProfile() + { + CreateMap() + .ForMember(x => x.Date, o => o.MapFrom(x => x.DateTime)) + .ForMember(x => x.Item, o => o.MapFrom(x => x.Item.ItemGlobal.Name)) + .ForMember(x => x.Plus, o => o.MapFrom(x => x.AmountPlus)) + .ForMember(x => x.Minus, o => o.MapFrom(x => x.AmountMinus)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Motion/MotionRequest.cs b/MyOffice.Web/Models/Motion/MotionRequest.cs new file mode 100644 index 0000000..a4f2bc8 --- /dev/null +++ b/MyOffice.Web/Models/Motion/MotionRequest.cs @@ -0,0 +1,24 @@ +namespace MyOffice.Web.Models.Motion; + +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class MotionRequest +{ + public DateTime Date { get; set; } + public string Item { get; set; } = null!; + public string? ItemId { get; set; } = null!; + public string? AccountId { get; set; } = null!; + public string? Description { get; set; } + public decimal? Plus { get; set; } + public decimal? Minus { get; set; } + public decimal? AmountBalancing { get; set; } +} + +public class MotionRequestProfile : Profile +{ + public MotionRequestProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Motion/MotionViewModel.cs b/MyOffice.Web/Models/Motion/MotionViewModel.cs new file mode 100644 index 0000000..c5a0f11 --- /dev/null +++ b/MyOffice.Web/Models/Motion/MotionViewModel.cs @@ -0,0 +1,27 @@ +namespace MyOffice.Web.Models.Motion; + +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class MotionViewModel : BaseViewModel +{ + public string Id { get; set; } = null!; + public DateTime Date { get; set; } + public string AccountId { get; set; } = null!; + public string Item { get; set; } = null!; + public string? Description { get; set; } + public decimal Plus { get; set; } + public decimal Minus { get; set; } +} + +public class MotionViewModelProfile : Profile +{ + public MotionViewModelProfile() + { + CreateMap() + .ForMember(x => x.Item, x => x.MapFrom(m => m.Item.Name)) + .ForMember(x => x.Minus, x => x.MapFrom(m => m.AmountMinus)) + .ForMember(x => x.Plus, x => x.MapFrom(m => m.AmountPlus)) + ; + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/User/UserViewModel.cs b/MyOffice.Web/Models/User/UserViewModel.cs new file mode 100644 index 0000000..03d8b8b --- /dev/null +++ b/MyOffice.Web/Models/User/UserViewModel.cs @@ -0,0 +1,19 @@ +namespace MyOffice.Web.Models.User; + +using AutoMapper; +using MyOffice.Services.Account.Domain; + +public class UserViewModel +{ + public string Id { get; set; } = null!; + public string UserName { get; set; } = null!; + public string Email { get; set; } = null!; +} + +public class UserViewModelProfile: Profile +{ + public UserViewModelProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Models/ViewModelProfile.cs b/MyOffice.Web/Models/ViewModelProfile.cs new file mode 100644 index 0000000..a5e4db6 --- /dev/null +++ b/MyOffice.Web/Models/ViewModelProfile.cs @@ -0,0 +1,12 @@ +namespace MyOffice.Web.Models; + +using AutoMapper; +using Core.Extensions; + +public class ViewModelProfile : Profile +{ + public ViewModelProfile() + { + CreateMap().ConvertUsing(x => x.ToShort()); + } +} \ No newline at end of file diff --git a/MyOffice.Web/MyOffice.Web.csproj b/MyOffice.Web/MyOffice.Web.csproj new file mode 100644 index 0000000..4456927 --- /dev/null +++ b/MyOffice.Web/MyOffice.Web.csproj @@ -0,0 +1,54 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + Never + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyOffice.Web/Program.Routes.cs b/MyOffice.Web/Program.Routes.cs new file mode 100644 index 0000000..9ec95fe --- /dev/null +++ b/MyOffice.Web/Program.Routes.cs @@ -0,0 +1,56 @@ +namespace MyOffice.Web; + +public partial class Program +{ + /// + /// Legacy imperative MapRoute table removed — API controllers use attribute routing. + /// Kept as a no-op so call sites stay stable while Routes constants remain for SPA/docs. + /// + private static void MapRoutes(WebApplication app) + { + } +} + +/// +/// Canonical API path constants (also used by SPA clients / docs). +/// +public static class Routes +{ + public static readonly string GlobalCurrencies = "api/general/currencies"; + public static readonly string SettingsCurrencies = "api/settings/currencies"; + public static readonly string SettingsCurrency = "api/settings/currencies/{id}"; + public static readonly string SettingsCurrencyRate = "api/settings/currencies/{id}/rate"; + + public static readonly string SettingsAccountCategories = "api/settings/account-categories"; + public static readonly string SettingsAccountCategory = "api/settings/account-categories/{id}"; + + public static readonly string SettingsAccounts = "api/settings/accounts"; + public static readonly string SettingsAccount = "api/settings/accounts/{id}"; + public static readonly string SettingsAccountAccountCategory = "api/settings/accounts/{id}/category/{categoryId}"; + + public static readonly string SettingsAccountAccesses = "api/settings/accounts/{id}/access"; + public static readonly string SettingsAccountAccess = "api/settings/accounts/{id}/access/{userId}"; + public static readonly string SettingsAccountInvites = "api/settings/accounts/invites"; + public static readonly string SettingsAccountInviteAccept = "api/settings/accounts/invites/{id}/accept"; + public static readonly string SettingsAccountInviteReject = "api/settings/accounts/invites/{id}/reject"; + + public static readonly string SettingsItemCategories = "api/settings/item-categories"; + public static readonly string SettingsItemCategory = "api/settings/item-categories/{id}"; + public static readonly string SettingsItems = "api/settings/items"; + public static readonly string SettingsItem = "api/settings/items/{id}"; + + public static readonly string Accounts = "api/accounts"; + public static readonly string Account = "api/accounts/{id}"; + public static readonly string AccountMotions = "api/accounts/{id}/motions"; + public static readonly string AccountMotion = "api/accounts/{id}/motions/{motionId}"; + public static readonly string Items = "api/items"; + + public static readonly string Dashboard = "api/dashboard"; + public static readonly string DashboardIncome = "api/dashboard/income"; + public static readonly string DashboardOutcome = "api/dashboard/outcome"; + + public static readonly string UserRegister = "api/user/register"; + public static readonly string UserProfile = "api/user/profile"; + public static readonly string UserAttach = "api/user/attach"; + public static readonly string UserDeattach = "api/user/deattach"; +} diff --git a/MyOffice.Web/Program.cs b/MyOffice.Web/Program.cs new file mode 100644 index 0000000..4aabbda --- /dev/null +++ b/MyOffice.Web/Program.cs @@ -0,0 +1,449 @@ +namespace MyOffice.Web; + +using System.IO; +using System.Linq; +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Logging; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Console; +using System.Text.Json.Serialization; + +using Identity; +using Identity.Domain; +using Identity.ExternalProviders; +using Identity.Repositories; +using Auth; +using Core.Identity; +using Core.Extensions; +using Data.Repositories.Account; +using Data.Repositories.Currency; +using Data.Repositories.Item; +using Data.Repositories.Users; +using DbContext; +using Services.Users; +using Shared; +using Infrastructure; +using Models.Account; +using MyOffice.Services.Currency; +using Services.Account; +using Services.Item; +using MyOffice.Services.Dashboard; +using MyOffice.Services.Identity; +using MyOffice.Services.Account.Domain; +using MyOffice.Web.Infrastructure.Attributes; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using MyOffice.Web.Infrastructure.Filters; + +//TODO: Data.Model only Repository and Service, response <-> mapper <-> web <-> mapper <-> service <-> repository +//TODO: Project management +//TODO: Model names. Controller = xxxRequest/xxxResponse. Service xxxInput/xxxOutput. +//TODO: Validate string as Guid when id +//TODO: Test back +//TODO: Test front +//TODO: Test DB ??? +//TODO: Email confirmation +//TODO: Password recovery +//TODO: Account sharing +//TODO: /dashboard/dashboard -> /dashboard/main or /dashboard +//TODO: SPA load categories twice menu + setting (cache) +//TODO: SPA update account category -> update menu +//TODO: Response model from base type +//TODO: XXXResult -> XXXStatus +//TODO: SPA isAllowDelete -> allowDelete (remove is) +//TODO: Repository auto registration +//TODO: Services auto registration +//TODO: openid-configuration failed - lock login +//TODO: BUG some times after login redirect to dashboard but exists return url +//TODO: automapper -> Extension ToModel() ToDbo() FromModel() FromDbo() +//TODO: SPA all http requests -> services +//TODO: Items, select category -> save url to allow refresh +//TODO: Accounts, select category -> save url to allow refresh + +public partial class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + AddServices(builder, args); + + var app = builder.Build(); + Configure(app); + + app.Run(); + } + + private static void AddServices(WebApplicationBuilder builder, string[] args) + { + #region Loging + + builder.Services.AddLogging(logging => + logging.AddSimpleConsole(options => + { + //options.SingleLine = true; + options.TimestampFormat = "[HH:mm:ss] "; + options.ColorBehavior = LoggerColorBehavior.Enabled; + }) + ); + //File Logger + builder.Logging.AddFile(builder.Configuration.GetSection("Logging")); + + #endregion Loging + + builder.Services.Configure(options => + { + options.AddFilter("OpenIddict", LogLevel.Warning); + }); + + // Shared JSON after appsettings.*, then restore higher-priority sources + builder.Configuration.AddSharedAppSettings(builder.Environment.EnvironmentName); + builder.Configuration.AddEnvironmentVariables(); + builder.Configuration.AddCommandLine(args); + + #region Database + + var databaseProvider = builder.Configuration["DatabaseProvider"]; + if (databaseProvider == null) + throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}"); + + var connectionString = builder.Configuration.GetConnectionString(databaseProvider); + if (connectionString == null) + throw new NullReferenceException($"Configuration ConnectionString {databaseProvider}"); + + var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString); + RepositoryInitializer.Initialize(builder.Services, connectionConfiguration); + // Before OpenIddictSeeder so the schema exists when clients/scopes are registered. + builder.Services.AddHostedService(); + + #endregion Database + + // Configurations + builder.Services.Configure( + builder.Configuration.GetSection("ExternalProviders") + ); + + InitializeGlobalSettings(builder); + + AddIdentityServices(builder); + + builder.Services.AddScoped(); + + AddAutoMapper(builder); + + AddRepositories(builder); + + AddBusinessServices(builder); + + builder.Services + .AddControllers() + .AddJsonOptions(options => + { + options.AllowInputFormatterExceptionMessages = builder.Environment.IsDevelopment(); + options.JsonSerializerOptions.MaxDepth = 0; + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + }); + + builder.Services.AddCors(); + + builder.Services.AddControllersWithViews(options => + { + options.Conventions.Add(new DefaultFromBodyBindingConvention()); + options.Filters.Add(typeof(GlobalModelStateValidatorAttribute)); + options.Filters.Add(typeof(ResponseFilter)); + }); + + builder.Services.Configure(options => + { + }); + + builder.Services.Configure(x => { + }); + + builder.Services.AddProblemDetails(); + builder.Services.AddExceptionHandler(); + builder.Services.AddSingleton(); + } + + private static void InitializeGlobalSettings(WebApplicationBuilder builder) + { + var frontEndHost = builder.Configuration.GetValue("FrontEnd:Host"); + if (frontEndHost.IsMissing()) + { + frontEndHost = "http://localhost:4300"; + if (!builder.Environment.IsDevelopment()) + { + throw new InvalidOperationException( + "FrontEnd:Host must be set outside Development (do not derive it from Referer)."); + } + } + + frontEndHost = frontEndHost!.TrimEnd('/'); + builder.Services.Configure(options => options.Host = frontEndHost); + builder.Services.AddSingleton(resolver => resolver.GetRequiredService>().Value); + } + + private static void AddIdentityServices(WebApplicationBuilder builder) + { + builder.Services.AddHttpContextAccessor(); + + // add identity + builder.Services + .AddIdentity, ApplicationRole>() + .AddUserStore() + .AddRoleStore() + .AddUserManager() + .AddSignInManager>>() + .AddDefaultTokenProviders(); + + builder.Services.AddScoped>, PasswordHasher>(); + + // Identity Services + builder.Services.AddScoped>, UserStore>(); + builder.Services.AddScoped, RoleStore>(); + + // External providers + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Configure Identity options and password complexity here + builder.Services.Configure(options => + { + // User settings + options.User.RequireUniqueEmail = true; + + // Email+password accounts can sign in immediately after register + options.SignIn.RequireConfirmedAccount = false; + options.SignIn.RequireConfirmedEmail = false; + + // Password settings + options.Password.RequireDigit = true; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + }); + + builder.Services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor + | ForwardedHeaders.XForwardedProto + | ForwardedHeaders.XForwardedHost; + // Trust nginx in Docker; headers are only present behind the proxy. + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + builder.Services.AddMyOfficeOpenIddict(builder.Configuration, builder.Environment); + } + + private static void AddAutoMapper(WebApplicationBuilder builder) + { + // AutoMapper 15+ dual license: runs without a key (warning logs only). + // Set AutoMapper:LicenseKey (or AUTOMAPPER_LICENSE_KEY) from https://automapper.io — free Community tier if under $5M revenue. + var licenseKey = builder.Configuration["AutoMapper:LicenseKey"]; + + builder.Services.AddAutoMapper( + c => + { + if (!string.IsNullOrWhiteSpace(licenseKey)) + { + c.LicenseKey = licenseKey; + } + + c.AllowNullCollections = true; + c.AllowNullDestinationValues = true; + }, + typeof(AccountDto).Assembly, + typeof(AccountViewModel).Assembly + ); + } + + private static void AddRepositories(WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + } + + private static void AddBusinessServices(WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + } + + private static readonly ConcurrentDictionary _staticFilesCache = new(); + + private static void Configure(WebApplication app) + { + // nginx (prod) terminates TLS and forwards X-Forwarded-Proto/Host + app.UseForwardedHeaders(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + IdentityModelEventSource.ShowPII = true; + } + else + { + app.UseExceptionHandler(); + app.UseStatusCodePages(); + } + + // Explicit routing so CORS runs before endpoint matching (preflight / OPTIONS). + app.UseRouting(); + ConfigureCors(app); + + app.UseDefaultFiles(); + app.UseStaticFiles(); + + var logger = app.Logger; + var globalSettings = app.Services.GetRequiredService(); + logger.LogInformation("FrontEnd host: {Host}", globalSettings.Host); + + // send index.html / static files for non-API routes + var coreRoutes = new[] + { + // API routes + new PathString("/api"), + // Identity server routes + new PathString("/.well-known"), + new PathString("/connect"), + new PathString("/silent-refresh.html") + }; + + var webRootPath = app.Configuration.GetValue(WebHostDefaults.ContentRootKey); + var wwwrootPath = Path.Combine(webRootPath!, "wwwroot"); + app.Use(async (context, next) => + { + var path = context.Request.Path; + + if (path.Value == null) + { + await next(); + return; + } + + if (coreRoutes.Any(x => path.StartsWithSegments(x, StringComparison.OrdinalIgnoreCase))) + { + await next(); + return; + } + + if (!_staticFilesCache.TryGetValue(path.Value, out var physicalFileInfo)) + { + var segments = path.Value.Split('/', '\\'); + var fileName = segments.LastOrDefault(); + var fileInfo = string.IsNullOrEmpty(fileName) + ? null + : new FileInfo(Path.Combine(wwwrootPath, fileName)); + + if (fileInfo is null || !fileInfo.Exists) + { + // SPA deep link fallback + var indexInfo = new FileInfo(Path.Combine(wwwrootPath, "index.html")); + if (!indexInfo.Exists) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + physicalFileInfo = new PhysicalFileInfo(indexInfo); + } + else + { + physicalFileInfo = new PhysicalFileInfo(fileInfo); + _staticFilesCache.TryAdd(path.Value, physicalFileInfo); + } + } + + await context.Response.SendFileAsync(physicalFileInfo); + }); + + app.UseAuthentication(); + app.UseAuthorization(); + + MapRoutes(app); + + app.MapControllers(); + } + + private static void ConfigureCors(WebApplication app) + { + var allowedOrigins = (app.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim().TrimEnd('/')) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + var frontEndHost = app.Configuration.GetValue("FrontEnd:Host")?.Trim().TrimEnd('/'); + if (!string.IsNullOrWhiteSpace(frontEndHost) + && !allowedOrigins.Contains(frontEndHost, StringComparer.OrdinalIgnoreCase)) + { + allowedOrigins.Add(frontEndHost); + } + + var isDocker = string.Equals( + app.Environment.EnvironmentName, + "Docker", + StringComparison.OrdinalIgnoreCase); + + app.Logger.LogInformation( + "CORS origins: {Origins}", + allowedOrigins.Count > 0 ? string.Join(", ", allowedOrigins) : "(none)"); + + app.UseCors(policy => + { + if (allowedOrigins.Count > 0) + { + policy.WithOrigins(allowedOrigins.ToArray()) + .AllowAnyHeader() + .AllowAnyMethod(); + } + else if (app.Environment.IsDevelopment() || isDocker) + { + // Demo / local: SPA and API are on different host ports. + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + } + }); + } +} \ No newline at end of file diff --git a/MyOffice.Web/Properties/launchSettings.json b/MyOffice.Web/Properties/launchSettings.json new file mode 100644 index 0000000..833dff2 --- /dev/null +++ b/MyOffice.Web/Properties/launchSettings.json @@ -0,0 +1,56 @@ +{ + "profiles": { + "MyOffice.Web": { + "commandName": "Project", + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:9100", + "dotnetRunMessages": true + }, + "MyOffice.SPA": { + "commandName": "Project" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS": { + "commandName": "IIS", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "watch": { + "commandName": "Executable", + "executablePath": "dotnet", + "workingDirectory": "$(ProjectDir)", + "hotReloadEnabled": true, + "hotReloadProfile": "aspnetcore", + "commandLineArgs": "watch run", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iis": { + "applicationUrl": "http://localhost:9300" + }, + "iisExpress": { + "applicationUrl": "http://localhost:9300", + "sslPort": 0 + }, + "watch": { + "applicationUrl": "http://localhost:9300" + } + } +} diff --git a/MyOffice.Web/appsettings.Docker.json b/MyOffice.Web/appsettings.Docker.json new file mode 100644 index 0000000..88a0d2c --- /dev/null +++ b/MyOffice.Web/appsettings.Docker.json @@ -0,0 +1,31 @@ +{ + "DatabaseProvider": "npgsql", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:8080" + } + } + }, + "FrontEnd": { + "Host": "http://localhost:32080" + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:32080", + "http://127.0.0.1:32080" + ] + }, + "OpenIddict": { + "SigningKeyPath": "", + "EncryptionKeyPath": "", + "UseEphemeralKeys": true, + "AllowHttp": true + } +} diff --git a/MyOffice.Web/appsettings.json b/MyOffice.Web/appsettings.json new file mode 100644 index 0000000..df31feb --- /dev/null +++ b/MyOffice.Web/appsettings.json @@ -0,0 +1,47 @@ +{ + "DatabaseProvider": "npgsql", + /*"DatabaseProvider": "sqlite",*/ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:9100" + } + /*, + "Https": { + "Url": "https://*:9101" + }*/ + } + }, + "AllowedHosts": "*", + "FrontEnd": { + "Host": "http://localhost:4300" + }, + "AutoMapper": { + "LicenseKey": "" + }, + "OpenIddict": { + "SigningKeyPath": "", + "EncryptionKeyPath": "" + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:4300" + ] + }, + "ExternalProviders": { + "Auth0": { + "ClientId": "", + "Domain": "", + "SecretKey": "" + }, + "Google": { + "ClientId": "" + } + } +} \ No newline at end of file diff --git a/MyOffice.Web/web.Release.config b/MyOffice.Web/web.Release.config new file mode 100644 index 0000000..a7c2127 --- /dev/null +++ b/MyOffice.Web/web.Release.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyOffice.Web/web.config b/MyOffice.Web/web.config new file mode 100644 index 0000000..add3e64 --- /dev/null +++ b/MyOffice.Web/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyOffice.Web/wwwroot/index.html b/MyOffice.Web/wwwroot/index.html new file mode 100644 index 0000000..80b20c7 --- /dev/null +++ b/MyOffice.Web/wwwroot/index.html @@ -0,0 +1,23 @@ + + + + + MyOffice + + + + + + +

DEVELOPER INDEX.HTML

+ + \ No newline at end of file