commit 77a536ee797c34020cd3a1f7944cb41881157d39 Author: honzapatCZ Date: Mon Sep 2 17:30:42 2024 +0200 init diff --git a/AutoScan.cs b/AutoScan.cs new file mode 100644 index 0000000..952783c --- /dev/null +++ b/AutoScan.cs @@ -0,0 +1,59 @@ +namespace NejCommon; + +public interface IScopedService +{ + +} +public interface ISingletonService +{ + +} +public interface ITransientService +{ + +} +public interface IBackgroundService +{ + +} +public interface ISettings +{ + string Path { get; } +} + +public static class AutoScan +{ + public static void RegisterServices(this IServiceCollection collection) + { + collection.Scan(scan => scan + // We start out with all types in the assembly of ITransientService + .FromAssemblyOf() + // AddClasses starts out with all public, non-abstract types in this assembly. + // These types are then filtered by the delegate passed to the method. + // In this case, we filter out only the classes that are assignable to ITransientService. + .AddClasses(classes => classes.AssignableTo()) + // We then specify what type we want to register these classes as. + // In this case, we want to register the types as all of its implemented interfaces. + // So if a type implements 3 interfaces; A, B, C, we'd end up with three separate registrations. + .AsSelf() + // And lastly, we specify the lifetime of these registrations. + .WithTransientLifetime() + // Here we start again, with a new full set of classes from the assembly above. + // This time, filtering out only the classes assignable to IScopedService. + .AddClasses(classes => classes.AssignableTo()) + // Now, we just want to register these types as a single interface, IScopedService. + .AsSelf() + // And again, just specify the lifetime. + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo()) + .AsSelf() + .AsImplementedInterfaces() + .WithSingletonLifetime() + .AddClasses(classes => classes.AssignableTo()) + .AsSelf() + .AsImplementedInterfaces() + .WithSingletonLifetime() + + ); + } +} \ No newline at end of file diff --git a/Controllers/AutoController.cs b/Controllers/AutoController.cs new file mode 100644 index 0000000..c4f9e7a --- /dev/null +++ b/Controllers/AutoController.cs @@ -0,0 +1,172 @@ +using System.Linq.Expressions; +using System.Reflection; +using ApiSourceGeneratorHelper; +using AutoMapPropertyHelper; +using EntityFrameworkCore.Projectables; +using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NejAccountingAPI.Controllers; +using NejAccountingAPI.Models; +using NejCommon.Utils; +using Swashbuckle.AspNetCore.Annotations; + +namespace NejCommon.Controllers +{ + public abstract class AutoController : AutoController where TType : class, new() where TRequest : IAutomappedAttribute, new() where TResponse : IAutomappedAttribute, new() + { + public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers) + { + } + } + + public abstract class AutoController : AutoController + where TType : class, new() + where TRequest : IAutomappedAttribute, new() + where TResponse : IAutomappedAttribute, new() + { + public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers) + { + } + } + + /// + /// + /// + /// The underyling type + /// The request type + /// The response type + [ApiController] + public abstract class AutoController : ControllerBase + where TType : class, new() + where TGetAllResponse : IAutomappedAttribute, new() + where TCreateRequest : IAutomappedAttribute, new() + where TCreateResponse : IAutomappedAttribute, new() + where TGetResponse : IAutomappedAttribute, new() + where TUpdateRequest : IAutomappedAttribute, new() + where TUpdateResponse : IAutomappedAttribute, new() + { + protected readonly AppDbContext db; + protected readonly IServiceProvider providers; + + public AutoController(AppDbContext appDb, IServiceProvider providers) : base() + { + db = appDb; + this.providers = providers; + } + + protected abstract IQueryable GetQuery(TOwner comp); + protected virtual TType AssociateWithParent(TType entity, TOwner comp) + { + if (typeof(TOwner) != typeof(Company)) + { + throw new NotImplementedException("Special parent association not implemented"); + } + + var props = typeof(TType).GetProperty("Company"); + if (props == null) + { + //not implemented + throw new NotImplementedException("Special parent association not implemented"); + } + props.SetValue(entity, comp); + return entity; + } + protected virtual IQueryable ApplyDefaultOrdering(IQueryable query) + { + return query; + } + + + /// + /// Get all the + /// + /// Success + /// There was an error + [HttpGet] + public virtual async Task, Ok>>> GetAll(TOwner company, [FromQuery] Pagination pag) + { + var data = await ApplyDefaultOrdering(GetQuery(company)).AsNoTrackingWithIdentityResolution().ApplyPaginationRes(providers, pag); + + //Console.Writeline(data.Data); + + return TypedResults.Ok(data); + } + + /// + /// Creates the + /// + /// + /// Success + /// There was an error + [HttpPost] + public virtual async Task, CreatedAtRoute>> Create(TOwner company, [FromBody] TCreateRequest body) + { + var entity = db.Create(); + + entity = AssociateWithParent(entity, company); + + await db.AddAsync(entity); + + body.ApplyTo(providers, entity); + + return await db.ApiSaveChangesAsyncCreate(providers, entity); + } + + /// + /// Gets the + /// + /// + /// Success + /// There was an error + [HttpGet] + [Route("{id}/")] + public virtual async Task>> Get([FromServices][ModelBinder(Name = "id")] TType entity) + { + var dat = new TGetResponse().ApplyFrom(providers, entity); + return TypedResults.Ok(dat); + } + + /// + /// Delete company. + /// + /// + /// Success + /// There was an error + [HttpDelete] + [Route("{id}/")] + public virtual async Task, Ok>> Delete([FromServices][ModelBinder(Name = "id")] TType entity) + { + db.Remove(entity!); + + return await db.ApiSaveChangesAsync(TypedResults.Ok()); + } + + /// + /// Update company. + /// + /// + /// + /// Success + /// There was an error + [HttpPut] + [Route("{id}/")] + public virtual async Task, Ok>> Update([FromServices][ModelBinder(Name = "id")] TType entity, [FromBody] TUpdateRequest body) + {/* + if(entity is InvoiceBase inv){ + //Console.Writeline(inv.InvoiceItems.Count); + }*/ + body.ApplyTo(providers, entity); + + var dat = new TUpdateResponse().ApplyFrom(providers, entity); + + var res = await db.ApiSaveChangesAsyncOk(providers, entity); + + //use the private constructor thru reflection + var ctor = typeof(Results, Ok>).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0]; + return (Results, Ok>)ctor.Invoke(new object[] { res.Result }); + } + } +} diff --git a/Controllers/IApiDescriptionVisibilitySchemaFilter.cs b/Controllers/IApiDescriptionVisibilitySchemaFilter.cs new file mode 100644 index 0000000..be05efe --- /dev/null +++ b/Controllers/IApiDescriptionVisibilitySchemaFilter.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace NejCommon.Controllers +{ + public class IApiDescriptionVisibilitySchemaFilter : ISchemaFilter + { + List schemas = new List(); + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + RemoveSchemas(context); + //Get the .net type of the schema + var type = context.Type; + + //Get attributes that imlement IApiDescriptionVisibilityProvider interface + var attributes = type.GetCustomAttributes(typeof(IApiDescriptionVisibilityProvider), true).Select(a => a as IApiDescriptionVisibilityProvider); + + //If there are no attributes, then return + if (!attributes.Any()) + return; + + var ignore = attributes.Any(a => a.IgnoreApi); + + //If the schema is ignored, then remove it from the schema repository + if (!ignore) + return; + + schemas.Add(schema); + RemoveSchemas(context); + } + + public void RemoveSchemas(SchemaFilterContext context) + { + foreach (var schema in schemas) + { + var key = context.SchemaRepository.Schemas.FirstOrDefault(k => k.Value == schema).Key; + if (string.IsNullOrWhiteSpace(key)) + continue; + //Console.WriteLine($"Removing schema {key}"); + context.SchemaRepository.Schemas.Remove(key); + } + } + } + + public class OrphanedFilter : IDocumentFilter + { + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + // Add all paths parameters, bodies, responses and query parameters to a list + var ops = swaggerDoc.Paths.SelectMany(p => p.Value.Operations).ToList(); + + var Parameters = ops + .SelectMany(o => o.Value.Parameters) + .Select(x => x.Schema).ToList(); + var requst = ops + .Select(o => o.Value.RequestBody) + .Where(c => c != null) + .SelectMany(x => x.Content) + .Select(c => c.Value.Schema).ToList(); + var res = ops + .SelectMany(o => o.Value.Responses) + .SelectMany(r => r.Value.Content) + .Select(c => c.Value.Schema).ToList(); + + var usedSchemas = Parameters.Union(requst).Union(res).Distinct(); + + /* + Console.WriteLine($"Used schemas: {string.Join(", ", swaggerDoc.Components.Parameters)}"); + + var usedSchemas = swaggerDoc.Components.Parameters.Select(x => x.Value.Schema). + Union(swaggerDoc.Components.RequestBodies.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)). + Union(swaggerDoc.Components.Responses.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)). + Distinct(); + */ + var schemas = usedSchemas.ToList(); + + var currentSchemas = new Dictionary(); + foreach(var sch in schemas){ + ExpandSchemas(context.SchemaRepository, currentSchemas, sch); + } + + //Console.WriteLine($"Used schemas: {string.Join(", ", currentSchemas.Keys)}"); + + var orphanedSchemas = swaggerDoc.Components.Schemas.Where(x => !currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value); + + //Console.WriteLine($"Unused schemas: {string.Join(", ", orphanedSchemas.Keys)}"); + + swaggerDoc.Components.Schemas = swaggerDoc.Components.Schemas.Where(x => currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value); + } + + public static void ExpandSchemas(SchemaRepository repo, Dictionary currentSchemas, OpenApiSchema schemaToExpand) + { + if(schemaToExpand == null) + return; + + if(schemaToExpand.Type == "array") + schemaToExpand = schemaToExpand.Items; + + var selfRef = schemaToExpand.Reference?.Id; + var properties = schemaToExpand.Properties.Values.ToList(); + + if(selfRef != null && !currentSchemas.ContainsKey(selfRef) && repo.Schemas.ContainsKey(selfRef)) + { + var sch = repo.Schemas[selfRef]; + currentSchemas.Add(selfRef, sch); + + ExpandSchemas(repo, currentSchemas, sch); + } + foreach(var sch in properties) + { + ExpandSchemas(repo, currentSchemas, sch); + } + } + } +} \ No newline at end of file diff --git a/Controllers/ResponseEnricher.cs b/Controllers/ResponseEnricher.cs new file mode 100644 index 0000000..1e6147c --- /dev/null +++ b/Controllers/ResponseEnricher.cs @@ -0,0 +1,62 @@ +using System.Text; +using System.Xml; +using System.Xml.Serialization; +using ClosedXML.Excel; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace NejCommon.Controllers; + +public static class Responses +{ + public static FileStreamHttpResult RespondXlsx(this ControllerBase controller, XLWorkbook file, string fileName) + { + var stream = new MemoryStream(); + file.SaveAs(stream); + + stream.Seek(0, SeekOrigin.Begin); + + controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName); + return TypedResults.File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); + } + public static FileStreamHttpResult RespondXlsxTable(this ControllerBase controller, IEnumerable data, string name = "Data", string? sheetName = null) + { + if (sheetName is null) + { + //limit the name to 31 characters + sheetName = name.Length > 31 ? name.Substring(0, 31) : name; + } + + var notebook = new XLWorkbook(); + var sheet = notebook.Worksheets.Add(sheetName); + sheet.FirstCell().InsertTable(data); + + sheet.Columns().AdjustToContents(); + + return controller.RespondXlsx(notebook, name + ".xlsx"); + } + + public static FileStreamHttpResult RespondXml(this ControllerBase controller, T obj, string fileName = "Data.xml") + { + var stream = new MemoryStream(); + //use utf8 encoding + var serializer = new XmlSerializer(typeof(T)); + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + }; + + //Console.Writeline(stream.Length); + + using (var writer = XmlWriter.Create(stream, settings)) + { + serializer.Serialize(writer, obj); + //Console.Writeline(stream.Length); + stream.Seek(0, SeekOrigin.Begin); + + controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName); + return TypedResults.File(stream, "text/xml", fileName); + } + } +} \ No newline at end of file diff --git a/Controllers/TypedResultsPolyfill.cs b/Controllers/TypedResultsPolyfill.cs new file mode 100644 index 0000000..c1091a0 --- /dev/null +++ b/Controllers/TypedResultsPolyfill.cs @@ -0,0 +1,108 @@ + +using System.Diagnostics.Eventing.Reader; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace NejCommon.Controllers +{ + public class TypedResultsMetadataProvider : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var responseType = context.MethodInfo.ReturnType; + //Console.WriteLine(context.MethodInfo.DeclaringType.Name); + //Console.WriteLine(context.MethodInfo.Name); + //Console.WriteLine(responseType); + var t = IsSubclassOfRawGeneric(typeof(Microsoft.AspNetCore.Http.HttpResults.Results<,>), responseType); + if (t == null) + { + return; + } + + var parArg = t.GetGenericArguments(); + if (operation.Responses.ContainsKey("200")) + operation.Responses.Remove("200"); + + foreach (var arg in parArg) + { + if (arg == typeof(NotFound)) + { + operation.Responses.Add("404", new OpenApiResponse { Description = "Not found" }); + } + else if (arg == typeof(Ok)) + { + operation.Responses.Add("200", new OpenApiResponse { Description = "Success" }); + } + else if (IsSubclassOfRawGeneric(typeof(Ok<>), arg) != null) + { + + var okArg = IsSubclassOfRawGeneric(typeof(Ok<>), arg).GetGenericArguments()[0]; + Console.WriteLine("Adding: " + okArg); + + //get or generate the schema + var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository); + operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } }); + + } + else if (arg == typeof(CreatedAtRoute)) + { + operation.Responses.Add("201", new OpenApiResponse { Description = "Success" }); + } + else if (IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg) != null) + { + if (operation.Responses.ContainsKey("201")) + operation.Responses.Remove("201"); + + var okArg = IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg).GetGenericArguments()[0]; + Console.WriteLine("Adding: " + okArg); + + //get or generate the schema + var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository); + operation.Responses.Add("201", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } }); + } + else if (arg == typeof(BadRequest)) + { + operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error" }); + } + else if (IsSubclassOfRawGeneric(typeof(BadRequest<>), arg) != null) + { + if (operation.Responses.ContainsKey("400")) + operation.Responses.Remove("400"); + + var okArg = IsSubclassOfRawGeneric(typeof(BadRequest<>), arg).GetGenericArguments()[0]; + Console.WriteLine("Adding: " + okArg); + + //get or generate the schema + var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository); + operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } }); + } + else if (arg == typeof(FileStreamHttpResult)){ + operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/octet-stream", new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string", Format = "binary" } } } } }); + } + else + { + Console.WriteLine("Unknown type: " + arg); + } + } + } + + static Type? IsSubclassOfRawGeneric(Type generic, Type toCheck) + { + while (toCheck != null && toCheck != typeof(object)) + { + //if Task is used, we need to check the underlying type + var realTypeNoTask = toCheck.IsGenericType && toCheck.GetGenericTypeDefinition() == typeof(Task<>) ? toCheck.GetGenericArguments()[0] : toCheck; + var cur = realTypeNoTask.IsGenericType ? realTypeNoTask.GetGenericTypeDefinition() : realTypeNoTask; + //Console.WriteLine(cur); + if (generic == cur) + { + return realTypeNoTask; + } + toCheck = toCheck.BaseType; + } + return null; + } + } +} \ No newline at end of file diff --git a/Emails/EmailBase.cs b/Emails/EmailBase.cs new file mode 100644 index 0000000..82276d5 --- /dev/null +++ b/Emails/EmailBase.cs @@ -0,0 +1,38 @@ +using BlazorTemplater; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Mvc; +using NejAccountingAPI.Services.Email; +using PuppeteerSharp; +using System.Runtime.CompilerServices; + +namespace NejAccountingAPI.Emails +{ + public abstract class EmailBase : ComponentBase where T1 : EmailBase + { + public static string _css = ""; + public static string GetCss() + { + if (_css == "") + _css = File.ReadAllText("./NejCommon/Emails/wwwroot/output.css"); + + return _css; + } + + + [Parameter, EditorRequired] + public T2 Data { get; set; } = default!; + + public static async Task GetHTML(T2 dat) + { + string html = new ComponentRenderer().Set(x => x.Data, dat).Render(); + var result = PreMailer.Net.PreMailer.MoveCssInline(html, css: GetCss()); + + return result.Html; + } + + public virtual string GetSubject() + { + return "DEV: " + this.ToString(); + } + } +} diff --git a/Emails/GeneralMessage.razor b/Emails/GeneralMessage.razor new file mode 100644 index 0000000..9837c8e --- /dev/null +++ b/Emails/GeneralMessage.razor @@ -0,0 +1,23 @@ +@namespace NejAccountingAPI.Emails + +@inherits EmailBase + +@code { + + public struct GeneralMessageData + { + public GeneralMessageData(string subject, string message) + { + Message = message; + Subject = subject; + } + public string Message; + public string Subject; + } + + public override string GetSubject() + { + return base.GetSubject(); + } +} + diff --git a/Emails/__Imports.razor b/Emails/__Imports.razor new file mode 100644 index 0000000..2db782b --- /dev/null +++ b/Emails/__Imports.razor @@ -0,0 +1 @@ +@namespace NejAccountingAPI.Emails \ No newline at end of file diff --git a/Emails/wwwroot/output.css b/Emails/wwwroot/output.css new file mode 100644 index 0000000..59fba95 --- /dev/null +++ b/Emails/wwwroot/output.css @@ -0,0 +1,828 @@ +/* +! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.static { + position: static; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.h-full { + height: 100%; +} + +.h-16 { + height: 4rem; +} + +.h-48 { + height: 12rem; +} + +.w-full { + width: 100%; +} + +.w-16 { + width: 4rem; +} + +.w-48 { + width: 12rem; +} + +.flex-grow { + flex-grow: 1; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.self-end { + align-self: flex-end; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded { + border-radius: 0.25rem; +} + +.border-4 { + border-width: 4px; +} + +.bg-primary { + --tw-bg-opacity: 1; + background-color: rgb(var(--primary) / var(--tw-bg-opacity)); +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pl-8 { + padding-left: 2rem; +} + +.pr-8 { + padding-right: 2rem; +} + +.pr-4 { + padding-right: 1rem; +} + +.pt-1 { + padding-top: 0.25rem; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.font-semibold { + font-weight: 600; +} + +.font-bold { + font-weight: 700; +} + +.text-secondary { + --tw-text-opacity: 1; + color: rgb(var(--secondary-text) / var(--tw-text-opacity)); +} + +.text-primary { + --tw-text-opacity: 1; + color: rgb(var(--primary-text) / var(--tw-text-opacity)); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +:root { + /* + --primary: #3d3d3d; + --secondary: #535353; + --trinary: #2c2c2c; + --primary-text: #ffffff; + --secondary-text: #a0a0a0; + --primary-invert: #f3f3f3; + --secondary-invert: #dbdbdb; + --trinary-invert: #ffffff; + --primary-invert-text: #3d3d3d; + --secondary-invert-text: #535353; + --accent: #00C800; + --accent-dark: #008f00; + --accent-light: #00ed00; + --accent2: #3080FF; + --accent2-dark: #225ab4; + --accent3: #804000; + --accent3-dark: #472400; + --accent4: #F8B02C; + --accent4-dark: #bd8724; + --accent5: #9E3086; + --accent5-dark: #6b215b; + */ + --primary: 61 61 61; + --secondary: 83 83 83; + --trinary: 44 44 44; + --primary-text: 255 255 255; + --secondary-text: 237 246 255; + --primary-invert: 243 243 243; + --secondary-invert: 219 219 219; + --trinary-invert: 255 255 255; + --primary-invert-text: 61 61 61; + --secondary-invert-text: 83 83 83; + --accent: 57 172 231; + --accent-dark: 7 132 181; + --accent-light: 153 204 255; + --accent2: 48 128 255; + --accent2-dark: 34 90 180; + --accent3: 128 64 0; + --accent3-dark: 71 36 0; + --accent4: 248 176 44; + --accent4-dark: 189 135 36; + --accent5: 158 48 134; + --accent5-dark: 107 33 91; + --primary-border: 61 61 61; + --secondary-border: 83 83 83; + --trinary-border: 44 44 44; +} + +.light\-theme { + --primary: 243 243 243; + --secondary: 219 219 219; + --trinary: 255 255 255; + --primary-text: 61 61 61; + --secondary-text: 83 83 83; + --primary-invert: 61 61 61; + --secondary-invert: 83 83 83; + --trinary-invert: 44 44 44; + --primary-text-invert: 255 255 255; + --secondary-text-invert: 160 160 160; +} + +body { + -webkit-tap-highlight-color: var(--accent); +} + +::-webkit-scrollbar { + width: 0.5rem; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--secondary); + border-radius: 0.25rem; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent-dark); +} diff --git a/Emails/wwwroot/site.css b/Emails/wwwroot/site.css new file mode 100644 index 0000000..04599c4 --- /dev/null +++ b/Emails/wwwroot/site.css @@ -0,0 +1,87 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +:root { + /* + --primary: #3d3d3d; + --secondary: #535353; + --trinary: #2c2c2c; + --primary-text: #ffffff; + --secondary-text: #a0a0a0; + --primary-invert: #f3f3f3; + --secondary-invert: #dbdbdb; + --trinary-invert: #ffffff; + --primary-invert-text: #3d3d3d; + --secondary-invert-text: #535353; + --accent: #00C800; + --accent-dark: #008f00; + --accent-light: #00ed00; + --accent2: #3080FF; + --accent2-dark: #225ab4; + --accent3: #804000; + --accent3-dark: #472400; + --accent4: #F8B02C; + --accent4-dark: #bd8724; + --accent5: #9E3086; + --accent5-dark: #6b215b; + */ + --primary: 61 61 61; + --secondary: 83 83 83; + --trinary: 44 44 44; + --primary-text: 255 255 255; + --secondary-text: 237 246 255; + --primary-invert: 243 243 243; + --secondary-invert: 219 219 219; + --trinary-invert: 255 255 255; + --primary-invert-text: 61 61 61; + --secondary-invert-text: 83 83 83; + --accent: 57 172 231; + --accent-dark: 7 132 181; + --accent-light: 153 204 255; + --accent2: 48 128 255; + --accent2-dark: 34 90 180; + --accent3: 128 64 0; + --accent3-dark: 71 36 0; + --accent4: 248 176 44; + --accent4-dark: 189 135 36; + --accent5: 158 48 134; + --accent5-dark: 107 33 91; + --primary-border: 61 61 61; + --secondary-border: 83 83 83; + --trinary-border: 44 44 44; +} + +.light\-theme { + --primary: 243 243 243; + --secondary: 219 219 219; + --trinary: 255 255 255; + --primary-text: 61 61 61; + --secondary-text: 83 83 83; + --primary-invert: 61 61 61; + --secondary-invert: 83 83 83; + --trinary-invert: 44 44 44; + --primary-text-invert: 255 255 255; + --secondary-text-invert: 160 160 160; +} + +body { + -webkit-tap-highlight-color: var(--accent); +} + +::-webkit-scrollbar { + width: 0.5rem; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--secondary); + border-radius: 0.25rem; +} + + ::-webkit-scrollbar-thumb:hover { + background: var(--accent-dark); + } diff --git a/Models/Country.cs b/Models/Country.cs new file mode 100644 index 0000000..82d520e --- /dev/null +++ b/Models/Country.cs @@ -0,0 +1,1067 @@ + + +using System.Globalization; +using EntityFrameworkCore.Projectables; +using Nager.Country; + +namespace NejCommon.Models +{ + /// + /// Gets or Sets Country + /// + public enum Country + { + /// + /// Afghanistan + /// + AF = 1, + /// + /// Åland Islands + /// + AX = 2, + /// + /// Albania + /// + AL = 3, + /// + /// Algeria + /// + DZ = 4, + /// + /// American Samoa + /// + AS = 5, + /// + /// Andorra + /// + AD = 6, + /// + /// Angola + /// + AO = 7, + /// + /// Anguilla + /// + AI = 8, + /// + /// Antarctica + /// + AQ = 9, + /// + /// Antigua and Barbuda + /// + AG = 10, + /// + /// Argentina + /// + AR = 11, + /// + /// Armenia + /// + AM = 12, + /// + /// Aruba + /// + AW = 13, + /// + /// Australia + /// + AU = 14, + /// + /// Austria + /// + AT = 15, + /// + /// Azerbaijan + /// + AZ = 16, + /// + /// Bahamas + /// + BS = 17, + /// + /// Bahrain + /// + BH = 18, + /// + /// Bangladesh + /// + BD = 19, + /// + /// Barbados + /// + BB = 20, + /// + /// Belarus + /// + BY = 21, + /// + /// Belgium + /// + BE = 22, + /// + /// Belize + /// + BZ = 23, + /// + /// Benin + /// + BJ = 24, + /// + /// Bermuda + /// + BM = 25, + /// + /// Bhutan + /// + BT = 26, + /// + /// Bolivia (Plurinational State of) + /// + BO = 27, + /// + /// Bonaire, Sint Eustatius and Saba + /// + BQ = 28, + /// + /// Bosnia and Herzegovina + /// + BA = 29, + /// + /// Botswana + /// + BW = 30, + /// + /// Bouvet Island + /// + BV = 31, + /// + /// Brazil + /// + BR = 32, + /// + /// British Indian Ocean Territory + /// + IO = 33, + /// + /// Brunei Darussalam + /// + BN = 34, + /// + /// Bulgaria + /// + BG = 35, + /// + /// Burkina Faso + /// + BF = 36, + /// + /// Burundi + /// + BI = 37, + /// + /// Cabo Verde + /// + CV = 38, + /// + /// Cambodia + /// + KH = 39, + /// + /// Cameroon + /// + CM = 40, + /// + /// Canada + /// + CA = 41, + /// + /// Cayman Islands + /// + KY = 42, + /// + /// Central African Republic + /// + CF = 43, + /// + /// Chad + /// + TD = 44, + /// + /// Chile + /// + CL = 45, + /// + /// China + /// + CN = 46, + /// + /// Christmas Island + /// + CX = 47, + /// + /// Cocos (Keeling) Islands + /// + CC = 48, + /// + /// Colombia + /// + CO = 49, + /// + /// Comoros + /// + KM = 50, + /// + /// Congo + /// + CG = 51, + /// + /// Congo (Democratic Republic of the) + /// + CD = 52, + /// + /// Cook Islands + /// + CK = 53, + /// + /// Costa Rica + /// + CR = 54, + /// + /// Côte d'Ivoire + /// + CI = 55, + /// + /// Croatia + /// + HR = 56, + /// + /// Cuba + /// + CU = 57, + /// + /// Curaçao + /// + CW = 58, + /// + /// Cyprus + /// + CY = 59, + /// + /// Czechia + /// + CZ = 60, + /// + /// Denmark + /// + DK = 61, + /// + /// Djibouti + /// + DJ = 62, + /// + /// Dominica + /// + DM = 63, + /// + /// Dominican Republic + /// + DO = 64, + /// + /// Ecuador + /// + EC = 65, + /// + /// Egypt + /// + EG = 66, + /// + /// El Salvador + /// + SV = 67, + /// + /// Equatorial Guinea + /// + GQ = 68, + /// + /// Eritrea + /// + ER = 69, + /// + /// Estonia + /// + EE = 70, + /// + /// Ethiopia + /// + ET = 71, + /// + /// Falkland Islands (Malvinas) + /// + FK = 72, + /// + /// Faroe Islands + /// + FO = 73, + /// + /// Fiji + /// + FJ = 74, + /// + /// Finland + /// + FI = 75, + /// + /// France + /// + FR = 76, + /// + /// French Guiana + /// + GF = 77, + /// + /// French Polynesia + /// + PF = 78, + /// + /// French Southern Territories + /// + TF = 79, + /// + /// Gabon + /// + GA = 80, + /// + /// Gambia + /// + GM = 81, + /// + /// Georgia + /// + GE = 82, + /// + /// Germany + /// + DE = 83, + /// + /// Ghana + /// + GH = 84, + /// + /// Gibraltar + /// + GI = 85, + /// + /// Greece + /// + GR = 86, + /// + /// Greenland + /// + GL = 87, + /// + /// Grenada + /// + GD = 88, + /// + /// Guadeloupe + /// + GP = 89, + /// + /// Guam + /// + GU = 90, + /// + /// Guatemala + /// + GT = 91, + /// + /// Guernsey + /// + GG = 92, + /// + /// Guinea + /// + GN = 93, + /// + /// Guinea-Bissau + /// + GW = 94, + /// + /// Guyana + /// + GY = 95, + /// + /// Haiti + /// + HT = 96, + /// + /// Heard Island and McDonald Islands + /// + HM = 97, + /// + /// Holy See + /// + VA = 98, + /// + /// Honduras + /// + HN = 99, + /// + /// Hong Kong + /// + HK = 100, + /// + /// Hungary + /// + HU = 101, + /// + /// Iceland + /// + IS = 102, + /// + /// India + /// + IN = 103, + /// + /// Indonesia + /// + ID = 104, + /// + /// Iran (Islamic Republic of) + /// + IR = 105, + /// + /// Iraq + /// + IQ = 106, + /// + /// Ireland + /// + IE = 107, + /// + /// Isle of Man + /// + IM = 108, + /// + /// Israel + /// + IL = 109, + /// + /// Italy + /// + IT = 110, + /// + /// Jamaica + /// + JM = 111, + /// + /// Japan + /// + JP = 112, + /// + /// Jersey + /// + JE = 113, + /// + /// Jordan + /// + JO = 114, + /// + /// Kazakhstan + /// + KZ = 115, + /// + /// Kenya + /// + KE = 116, + /// + /// Kiribati + /// + KI = 117, + /// + /// Korea (Democratic People's Republic of) + /// + KP = 118, + /// + /// Korea (Republic of) + /// + KR = 119, + /// + /// Kuwait + /// + KW = 120, + /// + /// Kyrgyzstan + /// + KG = 121, + /// + /// Lao People's Democratic Republic + /// + LA = 122, + /// + /// Latvia + /// + LV = 123, + /// + /// Lebanon + /// + LB = 124, + /// + /// Lesotho + /// + LS = 125, + /// + /// Liberia + /// + LR = 126, + /// + /// Libya + /// + LY = 127, + /// + /// Liechtenstein + /// + LI = 128, + /// + /// Lithuania + /// + LT = 129, + /// + /// Luxembourg + /// + LU = 130, + /// + /// Macao + /// + MO = 131, + /// + /// Macedonia (the former Yugoslav Republic of) + /// + MK = 132, + /// + /// Madagascar + /// + MG = 133, + /// + /// Malawi + /// + MW = 134, + /// + /// Malaysia + /// + MY = 135, + /// + /// Maldives + /// + MV = 136, + /// + /// Mali + /// + ML = 137, + /// + /// Malta + /// + MT = 138, + /// + /// Marshall Islands + /// + MH = 139, + /// + /// Martinique + /// + MQ = 140, + /// + /// Mauritania + /// + MR = 141, + /// + /// Mauritius + /// + MU = 142, + /// + /// Mayotte + /// + YT = 143, + /// + /// Mexico + /// + MX = 144, + /// + /// Micronesia (Federated States of) + /// + FM = 145, + /// + /// Moldova (Republic of) + /// + MD = 146, + /// + /// Monaco + /// + MC = 147, + /// + /// Mongolia + /// + MN = 148, + /// + /// Montenegro + /// + ME = 149, + /// + /// Montserrat + /// + MS = 150, + /// + /// Morocco + /// + MA = 151, + /// + /// Mozambique + /// + MZ = 152, + /// + /// Myanmar + /// + MM = 153, + /// + /// Namibia + /// + NA = 154, + /// + /// Nauru + /// + NR = 155, + /// + /// Nepal + /// + NP = 156, + /// + /// Netherlands + /// + NL = 157, + /// + /// New Caledonia + /// + NC = 158, + /// + /// New Zealand + /// + NZ = 159, + /// + /// Nicaragua + /// + NI = 160, + /// + /// Niger + /// + NE = 161, + /// + /// Nigeria + /// + NG = 162, + /// + /// Niue + /// + NU = 163, + /// + /// Norfolk Island + /// + NF = 164, + /// + /// Northern Mariana Islands + /// + MP = 165, + /// + /// Norway + /// + NO = 166, + /// + /// Oman + /// + OM = 167, + /// + /// Pakistan + /// + PK = 168, + /// + /// Palau + /// + PW = 169, + /// + /// Palestine, State of + /// + PS = 170, + /// + /// Panama + /// + PA = 171, + /// + /// Papua New Guinea + /// + PG = 172, + /// + /// Paraguay + /// + PY = 173, + /// + /// Peru + /// + PE = 174, + /// + /// Philippines + /// + PH = 175, + /// + /// Pitcairn + /// + PN = 176, + /// + /// Poland + /// + PL = 177, + /// + /// Portugal + /// + PT = 178, + /// + /// Puerto Rico + /// + PR = 179, + /// + /// Qatar + /// + QA = 180, + /// + /// Réunion + /// + RE = 181, + /// + /// Romania + /// + RO = 182, + /// + /// Russian Federation + /// + RU = 183, + /// + /// Rwanda + /// + RW = 184, + /// + /// Saint Barthélemy + /// + BL = 185, + /// + /// Saint Helena, Ascension and Tristan da Cunha + /// + SH = 186, + /// + /// Saint Kitts and Nevis + /// + KN = 187, + /// + /// Saint Lucia + /// + LC = 188, + /// + /// Saint Martin (French part) + /// + MF = 189, + /// + /// Saint Pierre and Miquelon + /// + PM = 190, + /// + /// Saint Vincent and the Grenadines + /// + VC = 191, + /// + /// Samoa + /// + WS = 192, + /// + /// San Marino + /// + SM = 193, + /// + /// Sao Tome and Principe + /// + ST = 194, + /// + /// Saudi Arabia + /// + SA = 195, + /// + /// Senegal + /// + SN = 196, + /// + /// Serbia + /// + RS = 197, + /// + /// Seychelles + /// + SC = 198, + /// + /// Sierra Leone + /// + SL = 199, + /// + /// Singapore + /// + SG = 200, + /// + /// Sint Maarten (Dutch part) + /// + SX = 201, + /// + /// Slovakia + /// + SK = 202, + /// + /// Slovenia + /// + SI = 203, + /// + /// Solomon Islands + /// + SB = 204, + /// + /// Somalia + /// + SO = 205, + /// + /// South Africa + /// + ZA = 206, + /// + /// South Georgia and the South Sandwich Islands + /// + GS = 207, + /// + /// South Sudan + /// + SS = 208, + /// + /// Spain + /// + ES = 209, + /// + /// Sri Lanka + /// + LK = 210, + /// + /// Sudan + /// + SD = 211, + /// + /// Suriname + /// + SR = 212, + /// + /// Svalbard and Jan Mayen + /// + SJ = 213, + /// + /// Swaziland + /// + SZ = 214, + /// + /// Sweden + /// + SE = 215, + /// + /// Switzerland + /// + CH = 216, + /// + /// Syrian Arab Republic + /// + SY = 217, + /// + /// Taiwan, Province of China[a] + /// + TW = 218, + /// + /// Tajikistan + /// + TJ = 219, + /// + /// Tanzania, United Republic of + /// + TZ = 220, + /// + /// Thailand + /// + TH = 221, + /// + /// Timor-Leste + /// + TL = 222, + /// + /// Togo + /// + TG = 223, + /// + /// Tokelau + /// + TK = 224, + /// + /// Tonga + /// + TO = 225, + /// + /// Trinidad and Tobago + /// + TT = 226, + /// + /// Tunisia + /// + TN = 227, + /// + /// Turkey + /// + TR = 228, + /// + /// Turkmenistan + /// + TM = 229, + /// + /// Turks and Caicos Islands + /// + TC = 230, + /// + /// Tuvalu + /// + TV = 231, + /// + /// Uganda + /// + UG = 232, + /// + /// Ukraine + /// + UA = 233, + /// + /// United Arab Emirates + /// + AE = 234, + /// + /// United Kingdom of Great Britain and Northern Ireland + /// + GB = 235, + /// + /// United States of America + /// + US = 236, + /// + /// United States Minor Outlying Islands + /// + UM = 237, + /// + /// Uruguay + /// + UY = 238, + /// + /// Uzbekistan + /// + UZ = 239, + /// + /// Vanuatu + /// + VU = 240, + /// + /// Venezuela (Bolivarian Republic of) + /// + VE = 241, + /// + /// Viet Nam + /// + VN = 242, + /// + /// Virgin Islands (British) + /// + VG = 243, + /// + /// Virgin Islands (U.S.) + /// + VI = 244, + /// + /// Wallis and Futuna + /// + WF = 245, + /// + /// Western Sahara + /// + EH = 246, + /// + /// Yemen + /// + YE = 247, + /// + /// Zambia + /// + ZM = 248, + /// + /// Zimbabwe + /// + ZW = 249, + } + + public static class CountryExtensions + { + public static CountryProvider CountryProvider { get; } = new CountryProvider(); + [Projectable] + public static string GetCountryCode(this Country country) => country.ToString(); + public static CultureInfo GetCultureInfo(this Country country) => new CultureInfo(country.GetCountryCode()); + public static string GetDisplayName(this Country country) => CountryProvider.GetCountry(country.GetCountryCode()).NativeName; + + public static bool IsEEA(this Country country) => country switch + { + Country.AT => true, + Country.BE => true, + Country.BG => true, + Country.HR => true, + Country.CY => true, + Country.CZ => true, + Country.DK => true, + Country.EE => true, + Country.FI => true, + Country.FR => true, + Country.DE => true, + Country.GR => true, + Country.HU => true, + Country.IE => true, + Country.IT => true, + Country.LV => true, + Country.LT => true, + Country.LU => true, + Country.MT => true, + Country.NL => true, + Country.PL => true, + Country.PT => true, + Country.RO => true, + Country.SK => true, + Country.SI => true, + Country.ES => true, + Country.SE => true, + + //extras + Country.LI => true, + Country.IS => true, + Country.NO => true, + _ => false + }; + + public static bool IsSEPA(this Country country) => IsEEA(country) || (country switch{ + Country.CH => true, + Country.GB => true, + Country.SM => true, + Country.VA => true, + Country.AD => true, + Country.MC => true, + _ => false + }); + } +} diff --git a/Models/Currency.cs b/Models/Currency.cs new file mode 100644 index 0000000..4006367 --- /dev/null +++ b/Models/Currency.cs @@ -0,0 +1,631 @@ + + +namespace NejCommon.Models +{ + /// + /// Gets or Sets Currency + /// + public enum Currency + { + /// + /// AED + /// + AED, + /// + /// AFN + /// + AFN, + /// + /// ALL + /// + ALL, + /// + /// AMD + /// + AMD, + /// + /// ANG + /// + ANG, + /// + /// AOA + /// + AOA, + /// + /// ARS + /// + ARS, + /// + /// AUD + /// + AUD, + /// + /// AWG + /// + AWG, + /// + /// AZN + /// + AZN, + /// + /// BAM + /// + BAM, + /// + /// BBD + /// + BBD, + /// + /// BDT + /// + BDT, + /// + /// BGN + /// + BGN, + /// + /// BHD + /// + BHD, + /// + /// BIF + /// + BIF, + /// + /// BMD + /// + BMD, + /// + /// BND + /// + BND, + /// + /// BOB + /// + BOB, + /// + /// BRL + /// + BRL, + /// + /// BSD + /// + BSD, + /// + /// BTN + /// + BTN, + /// + /// BWP + /// + BWP, + /// + /// BYN + /// + BYN, + /// + /// BZD + /// + BZD, + /// + /// CAD + /// + CAD, + /// + /// CDF + /// + CDF, + /// + /// CHF + /// + CHF, + /// + /// CLP + /// + CLP, + /// + /// CNY + /// + CNY, + /// + /// COP + /// + COP, + /// + /// CRC + /// + CRC, + /// + /// CUP + /// + CUP, + /// + /// CVE + /// + CVE, + /// + /// CZK + /// + CZK, + /// + /// DJF + /// + DJF, + /// + /// DKK + /// + DKK, + /// + /// DOP + /// + DOP, + /// + /// DZD + /// + DZD, + /// + /// EGP + /// + EGP, + /// + /// ERN + /// + ERN, + /// + /// ETB + /// + ETB, + /// + /// EUR + /// + EUR, + /// + /// FJD + /// + FJD, + /// + /// FKP + /// + FKP, + /// + /// GBP + /// + GBP, + /// + /// GEL + /// + GEL, + /// + /// GHS + /// + GHS, + /// + /// GIP + /// + GIP, + /// + /// GMD + /// + GMD, + /// + /// GNF + /// + GNF, + /// + /// GTQ + /// + GTQ, + /// + /// GYD + /// + GYD, + /// + /// HKD + /// + HKD, + /// + /// HNL + /// + HNL, + /// + /// HRK + /// + HRK, + /// + /// HTG + /// + HTG, + /// + /// HUF + /// + HUF, + /// + /// IDR + /// + IDR, + /// + /// ILS + /// + ILS, + /// + /// INR + /// + INR, + /// + /// IQD + /// + IQD, + /// + /// IRR + /// + IRR, + /// + /// ISK + /// + ISK, + /// + /// JMD + /// + JMD, + /// + /// JOD + /// + JOD, + /// + /// JPY + /// + JPY, + /// + /// KES + /// + KES, + /// + /// KGS + /// + KGS, + /// + /// KHR + /// + KHR, + /// + /// KMF + /// + KMF, + /// + /// KPW + /// + KPW, + /// + /// KRW + /// + KRW, + /// + /// KWD + /// + KWD, + /// + /// KYD + /// + KYD, + /// + /// KZT + /// + KZT, + /// + /// LAK + /// + LAK, + /// + /// LBP + /// + LBP, + /// + /// LKR + /// + LKR, + /// + /// LRD + /// + LRD, + /// + /// LSL + /// + LSL, + /// + /// LYD + /// + LYD, + /// + /// MAD + /// + MAD, + /// + /// MDL + /// + MDL, + /// + /// MGA + /// + MGA, + /// + /// MKD + /// + MKD, + /// + /// MMK + /// + MMK, + /// + /// MNT + /// + MNT, + /// + /// MOP + /// + MOP, + /// + /// MRU + /// + MRU, + /// + /// MUR + /// + MUR, + /// + /// MVR + /// + MVR, + /// + /// MWK + /// + MWK, + /// + /// MXN + /// + MXN, + /// + /// MYR + /// + MYR, + /// + /// MZN + /// + MZN, + /// + /// NAD + /// + NAD, + /// + /// NGN + /// + NGN, + /// + /// NIO + /// + NIO, + /// + /// NOK + /// + NOK, + /// + /// NPR + /// + NPR, + /// + /// NZD + /// + NZD, + /// + /// OMR + /// + OMR, + /// + /// PAB + /// + PAB, + /// + /// PEN + /// + PEN, + /// + /// PGK + /// + PGK, + /// + /// PHP + /// + PHP, + /// + /// PKR + /// + PKR, + /// + /// PLN + /// + PLN, + /// + /// PYG + /// + PYG, + /// + /// QAR + /// + QAR, + /// + /// RON + /// + RON, + /// + /// RSD + /// + RSD, + /// + /// RUB + /// + RUB, + /// + /// RWF + /// + RWF, + /// + /// SAR + /// + SAR, + /// + /// SBD + /// + SBD, + /// + /// SCR + /// + SCR, + /// + /// SDG + /// + SDG, + /// + /// SEK + /// + SEK, + /// + /// SGD + /// + SGD, + /// + /// SHP + /// + SHP, + /// + /// SLL + /// + SLL, + /// + /// SOS + /// + SOS, + /// + /// SRD + /// + SRD, + /// + /// SSP + /// + SSP, + /// + /// STD + /// + STD, + /// + /// SYP + /// + SYP, + /// + /// SZL + /// + SZL, + /// + /// THB + /// + THB, + /// + /// TJS + /// + TJS, + /// + /// TMT + /// + TMT, + /// + /// TND + /// + TND, + /// + /// TOP + /// + TOP, + /// + /// TRY + /// + TRY, + /// + /// TTD + /// + TTD, + /// + /// TWD + /// + TWD, + /// + /// TZS + /// + TZS, + /// + /// UAH + /// + UAH, + /// + /// UGX + /// + UGX, + /// + /// USD + /// + USD, + /// + /// UYU + /// + UYU, + /// + /// UZS + /// + UZS, + /// + /// VEF + /// + VEF, + /// + /// VND + /// + VND, + /// + /// VUV + /// + VUV, + /// + /// WST + /// + WST, + /// + /// XAF + /// + XAF, + /// + /// XCD + /// + XCD, + /// + /// XOF + /// + XOF, + /// + /// XPF + /// + XPF, + /// + /// YER + /// + YER, + /// + /// ZAR + /// + ZAR, + /// + /// ZMW + /// + ZMW, + /// + /// ZWL + /// + ZWL + } +} diff --git a/Models/CurrencyValue.cs b/Models/CurrencyValue.cs new file mode 100644 index 0000000..b9e7d33 --- /dev/null +++ b/Models/CurrencyValue.cs @@ -0,0 +1,28 @@ + + +namespace NejCommon.Models +{ + /// + /// A value with a currency + /// + public class CurrencyValue + { + public CurrencyValue() + { + Currency = Currency.EUR; + Value = 0; + } + public CurrencyValue(Currency currency, decimal value) + { + Value = value; + Currency = currency; + } + public decimal Value { get; set; } + public Currency Currency { get; set; } + + public override string ToString() + { + return $"{Value} {Currency}"; + } + } +} diff --git a/Models/ModelBinder.cs b/Models/ModelBinder.cs new file mode 100644 index 0000000..7bbace6 --- /dev/null +++ b/Models/ModelBinder.cs @@ -0,0 +1,269 @@ +using AngleSharp.Dom; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using NejAccountingAPI.Documents; +using NejAccountingAPI.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace NejCommon.Models +{ + public abstract class RouteValue + { + public string Name { get; } + public bool Primary { get; } = false; + + public RouteValue(string name, bool primary = false) + { + Name = name; + Primary = primary; + } + } + + public class RouteValue : RouteValue where TEntity : class + { + public Expression> Expression { get; } + + public RouteValue(string name, Expression> expression) : base(name) + { + Expression = expression; + } + public RouteValue(string name, bool primary, Expression> expression) : base(name, primary) + { + Expression = expression; + } + } + + + public abstract class EntityBinder + { + } + + public abstract class EntityBinder : EntityBinder where TEntity : class + { + public EntityBinder(AppDbContext db, RouteValue[] routeValues) : base(db, routeValues) + { + } + } + + + //id should be Guid, int, string, etc. + public abstract class EntityBinder : EntityBinder, IModelBinder + where TEntity : class + where TIdType : IEquatable + { + private readonly AppDbContext _db; + private readonly RouteValue[] _routeValues; + + public RouteValue[] RouteValues => _routeValues; + + + public EntityBinder(AppDbContext db, RouteValue[] routeValues) + { + _db = db; + _routeValues = routeValues; + } + + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + + //get the binding parameter attributes + + // Fetch the route values from the route data + var capturedRouteData = new Dictionary(); + foreach (var routeValue in _routeValues) + { + var name = routeValue.Name; + if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName)) + { + name = bindingContext.ModelName; + } + + /* + Console.WriteLine("Route value: " + name); + Console.WriteLine(); + Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]); + */ + + var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name]; + + if (value is null) + { + bindingContext.ModelState.AddModelError(name, "Route value not found"); + } + else + { + capturedRouteData[routeValue.Name] = value; + } + } + + // Check if all the required route values are present + if (!bindingContext.ModelState.IsValid) + { + bindingContext.Result = ModelBindingResult.Failed(); + return; + } + + // Find the entity with the specified route values + var query = _db.Set().AsQueryable(); + foreach (var routeValue in _routeValues) + { + var value = capturedRouteData[routeValue.Name]; + + var par = Expression.Parameter(typeof(TEntity)); + var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value)); + var redExp = exp.Reduce(); + var compiledExp = Expression.Lambda>(redExp, par); + + query = query.Where(compiledExp); + } + + var model = await query.FirstOrDefaultAsync(); + + if (model == null) + { + bindingContext.HttpContext.Response.StatusCode = 404; + bindingContext.ModelState.AddModelError(bindingContext.FieldName, "Not found in DB"); + bindingContext.Result = ModelBindingResult.Failed(); + return; + } + bindingContext.Result = ModelBindingResult.Success(model); + bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry + { + SuppressValidation = true + }; + } + } + + public class EntityBinderOperationFilter : IOperationFilter + { + public object CreateDummyInstance(Type entityBinderType) + { + var constructors = entityBinderType.GetConstructors(); + var constructor = constructors.OrderBy(x => x.GetParameters().Count()).First(); + var constructorParameters = constructor.GetParameters(); + + var entityBinder = (EntityBinder)constructor.Invoke(constructorParameters.Select(x => (object?)null).ToArray()); + return entityBinder; + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var actionDescriptor = context.ApiDescription.ActionDescriptor; + + // Get the action parameters with a ModelBinder attribute + var modelBinderParameters = actionDescriptor.Parameters + .Where(p => p.BindingInfo?.BinderType != null && typeof(EntityBinder).IsAssignableFrom(p.BindingInfo.BinderType)) + .Where(x => x != null) + .ToList(); + if (modelBinderParameters.Count == 0) + { + return; + } + + //Console.WriteLine("Applying EntityBinderOperationFilter to: " + actionDescriptor.DisplayName); + + operation.Parameters = operation.Parameters.Where(p => !modelBinderParameters.Any(mp => mp.Name == p.Name)).ToList(); + + /* + // Get the EntityBinder RouteValues + foreach (var parameter in modelBinderParameters) + { + var entityBinderType = parameter.BindingInfo!.BinderType; + var routeValuesProperty = entityBinderType!.GetProperty("RouteValues"); + if (routeValuesProperty == null) + { + continue; + } + + var routeValues = (IEnumerable?)routeValuesProperty.GetValue(CreateDummyInstance(entityBinderType)); + if (routeValues == null) + { + continue; + } + foreach (var routeValue in routeValues) + { + operation.Parameters.Add(new OpenApiParameter + { + Name = routeValue.Name, + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + + } + }*/ + + // Exclude the EntityBinder parameter types from the document schemas + var schemaRep = context.SchemaRepository; + foreach (var parameter in modelBinderParameters) + { + var entityBinderType = parameter.BindingInfo?.BinderType; + + // Get the EntityBinder generic type argument + var entityType = GetBinderEntityType(entityBinderType); + if (entityType == null) + { + Console.WriteLine("Couldn't find entityType of: " + entityBinderType); + continue; + } + + if (schemaRep.Schemas.ContainsKey(entityType.Name)) + { + //Console.WriteLine("Removing schema: " + entityType.Name); + schemaRep.Schemas.Remove(entityType.Name); + } + } + } + + private Type? GetBinderEntityType(Type? binderType) + { + if (binderType == null) + return null; + if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(EntityBinder<>)) + { + return binderType.GetTypeInfo().GetGenericArguments().FirstOrDefault(); + } + return GetBinderEntityType(binderType.BaseType); + } + } + + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class EntityAttribute : Attribute, IPropertyValidationFilter, IApiDescriptionVisibilityProvider, IBinderTypeProviderMetadata where TEntityBinder : EntityBinder where TEntity : class + { + /// + public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) + { + return false; + } + + /// + public Type? BinderType => typeof(TEntityBinder); + + /// + public BindingSource? BindingSource => BindingSource.Custom; + + public bool IgnoreApi => true; + } +} \ No newline at end of file diff --git a/Services/Email/IEmailService.cs b/Services/Email/IEmailService.cs new file mode 100644 index 0000000..97a8f1a --- /dev/null +++ b/Services/Email/IEmailService.cs @@ -0,0 +1,23 @@ +using System.Net.Mail; +using System.Reflection; +using NejAccountingAPI.Emails; + +namespace NejAccountingAPI.Services.Email; + +public interface IEmailService +{ + Task SendEmailAsync(string recipient, string subject, string message, List? attachments = null); + + async Task SendDocumentAsync(string recipient, T2 data, List? attachments = null) where T : EmailBase + { + var func = typeof(T).GetMethod("GetHTML", BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public); + var obj = Activator.CreateInstance(); + obj.Data = data; + var subject = obj.GetSubject(); + + //invoke the GetHTML function asynchronously and wait for the result + var html = await (Task)func.Invoke(null, new object[] { data }); + + await SendEmailAsync(recipient, subject, html, attachments); + } +} \ No newline at end of file diff --git a/Services/Email/SMTPService.cs b/Services/Email/SMTPService.cs new file mode 100644 index 0000000..c91c770 --- /dev/null +++ b/Services/Email/SMTPService.cs @@ -0,0 +1,78 @@ +using System.Net.Mail; +using MailKit; +using MailKit.Net.Imap; +using MailKit.Net.Smtp; +using MimeKit; + +namespace NejAccountingAPI.Services.Email; + +public class SMTPService : IEmailService +{ + public record SMTPSettings(string From, string Host, int Port, int ImapPort, string Username, string Password); + + private readonly SMTPSettings _settings; + + private readonly ILogger Logger; + + public SMTPService(ILogger logger, SMTPSettings settings) + { + _settings = settings; + Logger = logger; + } + + public async Task SendEmailAsync(string recipient, string subject, string message, List? attachments = null) + { + var mess = new MimeMessage(); + mess.From.Add(MailboxAddress.Parse(_settings.From)); + mess.To.Add(MailboxAddress.Parse(recipient)); + mess.Subject = subject; + + var bodyBuilder = new BodyBuilder(); + bodyBuilder.HtmlBody = message; + bodyBuilder.TextBody = ""; + + foreach (var attachment in attachments ?? new List()) + bodyBuilder.Attachments.Add(attachment.Name, attachment.ContentStream, ContentType.Parse(attachment.ContentType.ToString())); + + mess.Body = bodyBuilder.ToMessageBody(); + + using (var client = new MailKit.Net.Smtp.SmtpClient()) + { + await client.ConnectAsync(_settings.Host, _settings.Port); + + // Note: only needed if the SMTP server requires authentication + await client.AuthenticateAsync(_settings.Username, _settings.Password); + + await client.SendAsync(mess); + await client.DisconnectAsync(true); + + using (var imap = new MailKit.Net.Imap.ImapClient()) + { + await imap.ConnectAsync(_settings.Host, _settings.ImapPort); // or ConnectSSL for SSL + await imap.AuthenticateAsync(_settings.Username, _settings.Password); + + //get the sent folder + IMailFolder sent = null; + if (imap.Capabilities.HasFlag(ImapCapabilities.SpecialUse)) + sent = imap.GetFolder(SpecialFolder.Sent); + + if (sent == null) + { + // get the default personal namespace root folder + var personal = imap.GetFolder(imap.PersonalNamespaces[0]); + + // This assumes the sent folder's name is "Sent", but use whatever the real name is + sent = await personal.GetSubfolderAsync("Sent").ConfigureAwait(false); + } + + if (sent != null) + { + await sent.AppendAsync(mess, MessageFlags.Seen); + } + + await imap.DisconnectAsync(true); + } + Logger.LogInformation("Email {0} sent to {1} - {2} attachments", subject, recipient, attachments?.Count ?? 0); + } + } +} \ No newline at end of file diff --git a/Utils/DateTimeConverter.cs b/Utils/DateTimeConverter.cs new file mode 100644 index 0000000..e30d161 --- /dev/null +++ b/Utils/DateTimeConverter.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NejCommon.Utils +{ + public class DateOnlyConverter : JsonConverter + { + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return DateOnly.Parse(reader.GetString() ?? string.Empty); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString("yyyy-MM-dd")); + } + } +} \ No newline at end of file diff --git a/Utils/Extensions.cs b/Utils/Extensions.cs new file mode 100644 index 0000000..45235a2 --- /dev/null +++ b/Utils/Extensions.cs @@ -0,0 +1,119 @@ +using System.Globalization; +using System.Linq.Expressions; +using AutoMapPropertyHelper; +using EntityFrameworkCore.Projectables; +using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.EntityFrameworkCore; +using NejAccountingAPI.Models; +using NejCommon.Controllers; +using NejCommon.Models; + +namespace NejCommon.Utils; + +public static class Extensions +{ + public static IQueryable ApplyPagination(this IQueryable query, Pagination pag) + { + query = query.Skip(pag.Offset); + query = query.Take(pag.Count); + return query; + } + + public static async Task> ApplyPaginationRes(this IQueryable query, IServiceProvider providers, Pagination pag) where TResponseType : IAutomappedAttribute, new() + { + var totalCount = await query.CountAsync(); + query = query.Skip(pag.Offset); + query = query.Take(pag.Count); + var projector = new TResponseType().GetProjectorFrom(providers); + + return new PaginationResponse + { + TotalCount = totalCount, + Offset = pag.Offset, + Count = pag.Count, + Data = query.Select(projector).AsAsyncEnumerable() + }; + } + + public static async Task> ApplySearchPaginationRes(this IQueryable query, string? search, IServiceProvider providers, Pagination pag, List>> matchers) + where TType : class + where TResponseType : IAutomappedAttribute, new() + where TEntityBinder : EntityBinder + { + if (search != null) + { + var searchers = new List>>(); + foreach (var matcher in matchers) + { + //reduce expression from TType, string, bool to TType, bool by passing the search string + + var par = Expression.Parameter(typeof(TType)); + var exp = Expression.Invoke(matcher, par, Expression.Constant(search)); + var redExp = exp.Reduce(); + var reducedMatcher = Expression.Lambda>(redExp, par); + + searchers.Add(reducedMatcher); + } + + var extPar = Expression.Parameter(typeof(TType)); + var binder = (TEntityBinder)typeof(TEntityBinder).GetConstructor(new[] { typeof(AppDbContext) })!.Invoke(new object?[] { null }); + var idMatcher = binder.RouteValues.First(x => x.Primary).Expression; + var reducedIdMatcher = Expression.Lambda>(Expression.Invoke(idMatcher, extPar, Expression.Constant(search)).Reduce(), extPar); + //Console.Writeline(reducedIdMatcher); + searchers.Add(reducedIdMatcher); + + // Create an expression that ORs all the searchers + var agrPar = Expression.Parameter(typeof(TType)); + + var orExp = searchers.Aggregate((Expression)null, (current, searcher) => + { + var body = Expression.Invoke(searcher, agrPar); + if (current == null) + { + return body; + } + return Expression.OrElse(current, body); + }); + + //reduce the epxression as much as possible + while (orExp.CanReduce) + { + orExp = orExp.Reduce(); + } + + var orLambda = Expression.Lambda>(orExp, agrPar); + //Console.Writeline(orLambda); + query = query.Where(orLambda); + } + + return await query.ApplyPaginationRes(providers, pag); + } + + [Projectable] + public static IQueryable ApplyDateOnlyFrameQ(this IQueryable query, Func dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate); + [Projectable] + public static IEnumerable ApplyDateOnlyFrameE(this ICollection query, Func dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate); + + [Projectable] + public static IQueryable ApplyDateTimeFrameQ(this IQueryable query, string datePropery, DateTimeFrame frame) => query.Where(e => EF.Property(e, datePropery) >= frame.FromDate && EF.Property(e, datePropery) <= frame.ToDate); + [Projectable] + public static IEnumerable ApplyDateTimeFrameE(this ICollection query, Func dateAcessor, DateTimeFrame frame) => query.Where(e => dateAcessor(e) >= frame.FromDate && dateAcessor(e) <= frame.ToDate); + + [Projectable] + public static DateOnly ToDateOnly(this DateTime date) => new DateOnly(date.Year, date.Month, date.Day); + [Projectable] + public static DateTime ToDateTime(this DateOnly date) => new DateTime(date.Year, date.Month, date.Day); + [Projectable] + public static decimal Round(this decimal value, int decimals = 2) => Math.Round(value, decimals); + [Projectable] + public static decimal Floor(this decimal value, int decimals = 2) => Math.Floor(value * (decimal)Math.Pow(10, decimals)) / (decimal)Math.Pow(10, decimals); + [Projectable] + public static int RountToInt(this decimal value) => (int)Math.Round(value); + + [Projectable] + public static int GetInt(this string value, NumberStyles? style = null, CultureInfo? culture = null) => int.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture); + [Projectable] + public static decimal GetDecimal(this string value, NumberStyles? style = null, CultureInfo? culture = null) => decimal.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture); + [Projectable] + public static float GetFloat(this string value, NumberStyles? style = null, CultureInfo? culture = null) => float.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture); +} \ No newline at end of file