diff --git a/ECommerce.TerrenceLGee/.editorconfig b/ECommerce.TerrenceLGee/.editorconfig new file mode 100644 index 00000000..a4f6e9ac --- /dev/null +++ b/ECommerce.TerrenceLGee/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent diff --git a/ECommerce.TerrenceLGee/.gitattributes b/ECommerce.TerrenceLGee/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/ECommerce.TerrenceLGee/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/ECommerce.TerrenceLGee/.gitignore b/ECommerce.TerrenceLGee/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/ECommerce.TerrenceLGee/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce API Collection.postman_collection.json b/ECommerce.TerrenceLGee/ECommerce API Collection.postman_collection.json new file mode 100644 index 00000000..9b71fd1a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce API Collection.postman_collection.json @@ -0,0 +1,1547 @@ +{ + "info": { + "_postman_id": "fde7c673-14e0-431c-8790-f148aaf4911b", + "name": "ECommerce API Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "48089995" + }, + "item": [ + { + "name": "Address Requests", + "item": [ + { + "name": "Add A New Address For Customer1", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"AddressLine1\" : \"321 Main Street\",\r\n \"AddressLine2\" : null,\r\n \"City\": \"New York City\",\r\n \"State\": \"New York\",\r\n \"PostalCode\": \"123456\",\r\n \"Country\": \"USA\",\r\n \"IsBillingAddress\": false,\r\n \"IsShippingAddress\": true\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/addresses/add", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "add" + ] + }, + "description": "Add a new address for Customer1 ([tjones@example.com](https://mailto:tjones@example.com)) must provide the JWT token in the authorization header that was returned upon successful login" + }, + "response": [] + }, + { + "name": "Update An Existing Address For Customer1", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"AddressLine1\" : \"321 Main Street\",\r\n \"AddressLine2\" : null,\r\n \"City\": \"New York City\",\r\n \"State\": \"New York\",\r\n \"PostalCode\": \"654321\",\r\n \"Country\": \"USA\",\r\n \"IsBillingAddress\": true,\r\n \"IsShippingAddress\": false\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/addresses/update/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "update", + "1" + ] + }, + "description": "Update an address for Customer1 ([tjones@example.com](https://mailto:tjones@example.com)) must provide the JWT token in the authorization header that was returned upon successful login" + }, + "response": [] + }, + { + "name": "Delete Address For Customer1", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses/2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "2" + ] + }, + "description": "Delete an address for Customer1 ([tjones@example.com](https://mailto:tjones@example.com)) must provide the JWT token in the authorization header that was returned upon successful login" + }, + "response": [] + }, + { + "name": "Get All Customer Addresses For Admin With Pagination", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses/admin?page=2&pageSize=2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "admin" + ], + "query": [ + { + "key": "page", + "value": "2" + }, + { + "key": "pageSize", + "value": "2" + } + ] + }, + "description": "Allows the admin user to retrieve all addresses in the database when providing the JWT token from login" + }, + "response": [] + }, + { + "name": "Get All Customer Addresses For Admin Filter By City", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses/admin?city=new york", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "admin" + ], + "query": [ + { + "key": "city", + "value": "new york" + } + ] + } + }, + "response": [] + }, + { + "name": "Get All Customer Addresses For Admin Filter By Count Order By Postal Code With Pagination", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses/admin?page=1&pageSize=3&country=usa&orderBy=postalCode", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "3" + }, + { + "key": "country", + "value": "usa" + }, + { + "key": "orderBy", + "value": "postalCode" + } + ] + }, + "description": "Allows the admin user to retrieve all addresses in the database when providing the JWT token from login" + }, + "response": [] + }, + { + "name": "Customer1 Get Address By Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "1" + ] + }, + "description": "Allows Customer1 to retrieve an address by id (one of their own addresses) when providing the JWT token from login" + }, + "response": [] + }, + { + "name": "Customer1 Gets All Addresses", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/addresses", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses" + ] + }, + "description": "Allows Customer1 to retrieve all of their addresses on record when providing the JWT token from login" + }, + "response": [] + }, + { + "name": "Admin Get A Customer Address", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"customerId\": \"de3c3a96-7089-4da3-9a17-1d5a6c9832cd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/addresses/admin/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "addresses", + "admin", + "1" + ] + }, + "description": "Admin gets a customer address by Id. Because of the random nature of the GUIDs assigned to customers I was unable to populate this in the JSON body, to test this endpoint, just as admin get all customer addresses from the endpoint and copy a customer id into the body of this requred and then you can test it that way. Also you must logged in as the admin user and be authorized with the JWT token from the login response. The reason this is a post instead of a get is because I am using JSON to add the customer address to the request" + }, + "response": [] + } + ] + }, + { + "name": "Auth Requests", + "item": [ + { + "name": "Admin Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"admin@example.com\",\r\n \"password\": \"Pa$$w0rd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/login", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "login" + ] + }, + "description": "Logins in the admin user" + }, + "response": [] + }, + { + "name": "Logout", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "https://localhost:7001/api/auth/logout", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "logout" + ] + }, + "description": "Allows any user to logout when providing the JWT token from login" + }, + "response": [] + }, + { + "name": "Register New Customer", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"firstName\": \"Homer\",\r\n \"lastName\": \"Simpson\",\r\n \"email\": \"hsimpson@example.com\",\r\n \"dateOfBirth\": \"1956-05-12\",\r\n \"billingAddress\": {\r\n \"addressLine1\": \"430 Spalding Way\",\r\n \"addressLine2\": null,\r\n \"city\": \"Springfield\",\r\n \"state\": \"Oregon\",\r\n \"postalCode\": \"89922\",\r\n \"country\": \"USA\",\r\n \"isBillingAddress\": true,\r\n \"isShippingAddress\": false\r\n },\r\n \"shippingAddress\": {\r\n \"addressLine1\": \"742 Evergreen Terrace\",\r\n \"addressLine2\": null,\r\n \"city\": \"Springfield\",\r\n \"state\": \"Oregon\",\r\n \"postalCode\": \"89001\",\r\n \"country\": \"USA\",\r\n \"isBillingAddress\": false,\r\n \"isShippingAddress\": true\r\n },\r\n \"password\": \"Pa$$w0rd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/register", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "register" + ] + }, + "description": "Registers a new customer" + }, + "response": [] + }, + { + "name": "New Customer Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"hsimpson@example.com\",\r\n \"password\": \"Pa$$w0rd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/login", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "login" + ] + }, + "description": "Testing the new customer hsimpson@example.com's login" + }, + "response": [] + }, + { + "name": "Customer 1 Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"tjones@example.com\",\r\n \"password\": \"Pa$$w0rd\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/login", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "login" + ] + }, + "description": "Login for Customer1 which will also allow the testing of any endpoints that need a JWT token and that are authroized for the 'customer' role" + }, + "response": [] + }, + { + "name": "New Customer Reset Password", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"hsimpson@example.com\",\r\n \"oldPassword\": \"Pa$$w0rd\",\r\n \"newPassword\": \"Pa$$w0rd123\",\r\n \"confirmPassword\": \"Pa$$w0rd123\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/reset", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "reset" + ] + }, + "description": "Allows the new customer (hsimpson@example.com) to reset his previous password to a new password, make sure the user is created first" + }, + "response": [] + }, + { + "name": "New Customer Login With Reset Password", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"hsimpson@example.com\",\r\n \"password\": \"Pa$$w0rd123\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/auth/login", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "auth", + "login" + ] + }, + "description": "After the new customer's (hsimpson@example.com) password has been changed, login with the new password to make sure that this password reset was successful." + }, + "response": [] + } + ] + }, + { + "name": "Category Requests", + "item": [ + { + "name": "Admin Add New Category", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Automotive\",\r\n \"description\": \"Everything from anti-freeze to zerk-fitting everything you could ever need for your automobile.\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/categories/admin/add", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories", + "admin", + "add" + ] + }, + "description": "Allows the admin to add a new category (if the category does not already exist) must be authorized by using the JWT token from the admin login in" + }, + "response": [] + }, + { + "name": "Admin Update Existing Category", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Clothing\",\r\n \"description\": \"A wide assortment of clothing in all sizes for the entire family.\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/categories/admin/update/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories", + "admin", + "update", + "1" + ] + }, + "description": "Allows the admin to update an existing category must be authorized by using the JWT token from the admin login in" + }, + "response": [] + }, + { + "name": "Admin Get Category By Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/categories/admin/2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories", + "admin", + "2" + ] + }, + "description": "Allows the admin to view an existing category by id must be authorized by using the JWT token from the admin login in" + }, + "response": [] + }, + { + "name": "Admin Get All Categories With Pagination, Filtering and Sorting", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/categories/admin?page=1&pageSize=3&orderBy=name asc&description=almost", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "3" + }, + { + "key": "orderBy", + "value": "name asc" + }, + { + "key": "description", + "value": "almost" + } + ] + }, + "description": "Allows the admin to view categories that include information that would be relevant to the admin like creation time etc. must be authorized by using the JWT token from the admin login in" + }, + "response": [] + }, + { + "name": "Get Category By Id", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/categories/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories", + "1" + ] + }, + "description": "Get a category by id, no authorization needed" + }, + "response": [] + }, + { + "name": "Get All Categories with Pagination and Sorting", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/categories?page=2&pageSize=3&orderBy=name desc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "categories" + ], + "query": [ + { + "key": "page", + "value": "2" + }, + { + "key": "pageSize", + "value": "3" + }, + { + "key": "orderBy", + "value": "name desc" + } + ] + }, + "description": "Get All Categories with Pagination and Sorting no authorization needed" + }, + "response": [] + } + ] + }, + { + "name": "Customer Requests", + "item": [ + { + "name": "Get Customer1 Profile", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/customers/profile", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "customers", + "profile" + ] + }, + "description": "Login as Customer1 and copy the JWT token from the login response and use it for authorization" + }, + "response": [] + }, + { + "name": "Admin Get All Customers", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/customers/admin?page=1&pageSize=2&orderBy=firstName desc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "customers", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "2" + }, + { + "key": "orderBy", + "value": "firstName desc" + } + ] + }, + "description": "Login as Admin and then use the JWT token for authorization. Retrieves all customers in the database, can be used with pagination, filtering, sorting etc." + }, + "response": [] + } + ] + }, + { + "name": "Product Requests", + "item": [ + { + "name": "Admin Add A New Product", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"categoryId\": 4,\r\n \"name\": \"Head First C#: A Learner's Guide to Real-World Programming with C# and .NET\",\r\n \"description\": \"Dive right in and build a cross-platform game in chapter one using C# and .NET MAUI--running on Windows, macOS, Android, and iOS. Our readers have become professional developers, team leads, and coding streamers.\",\r\n \"stockQuantity\": 50,\r\n \"unitPrice\": 69.99,\r\n \"discountPercentage\": 33,\r\n \"isDeleted\" : false,\r\n \"isInStock\": true,\r\n \"imageUrl\": \"https://m.media-amazon.com/images/I/41oEi8theaL._SX342_SY445_FMwebp_.jpg\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/products/admin/add", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin", + "add" + ] + }, + "description": "Allows admin to add a new product to an existing category. Must use the JWT Token from the admin login for authorization" + }, + "response": [] + }, + { + "name": "Admin Update Product", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"categoryId\": 4,\r\n \"name\": \"The C# Player's Guide (5th Edition)\",\r\n \"description\": \"One of the best books for beginners looking to begin their exiting and rewarding journey into C# Programming. By RB Whitaker.\",\r\n \"stockQuantity\": 75,\r\n \"discountPercentage\": 15,\r\n \"isDeleted\": false,\r\n \"isInStock\": true\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/products/admin/update/14", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin", + "update", + "14" + ] + }, + "description": "Allows admin to update an existing product product in a category. Must use the JWT Token from the admin login for authorization" + }, + "response": [] + }, + { + "name": "Admin Mark An Existing Product As Deleted", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products/admin/delete/5", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin", + "delete", + "5" + ] + }, + "description": "Allows admin to mark a product as deleted. Must use the JWT Token from the admin login for authorization" + }, + "response": [] + }, + { + "name": "Admin Restore A Product Previously Marked As Deleted", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products/admin/restore/5", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin", + "restore", + "5" + ] + }, + "description": "Allows admin to restore a product previously marked as deleted. Must use the JWT Token from the admin login for authorization" + }, + "response": [] + }, + { + "name": "Get Product By Id", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products/6", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "6" + ] + }, + "description": "Get a product by Id. No authorization needed" + }, + "response": [] + }, + { + "name": "Get Products", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products?page=3&pageSize=6&minStockQuantity=30&orderBy=name desc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products" + ], + "query": [ + { + "key": "page", + "value": "3" + }, + { + "key": "pageSize", + "value": "6" + }, + { + "key": "minStockQuantity", + "value": "30" + }, + { + "key": "orderBy", + "value": "name desc" + } + ] + }, + "description": "Get All Products (has pagination, sorting etc)" + }, + "response": [] + }, + { + "name": "Admin Get Product By Id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products/admin/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin", + "1" + ] + }, + "description": "Admin gets a product by Id. Must use the JWT token from the admin login for authorization" + }, + "response": [] + }, + { + "name": "Admin Get Products", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/products/admin?page=1&pageSize=4&categoryName=movies&orderBy=name asc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "products", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "4" + }, + { + "key": "categoryName", + "value": "movies" + }, + { + "key": "orderBy", + "value": "name asc" + } + ] + }, + "description": "Admin can retrieve all products can use with pagination, filtering etc. Must use the JWT token from the admin login for authorization" + }, + "response": [] + } + ] + }, + { + "name": "Sale Requests", + "item": [ + { + "name": "Customer1 Create New Sale (Checkout)", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"ShoppingCart\": [\r\n {\r\n \"ProductId\": 4,\r\n \"Quantity\": 2\r\n },\r\n {\r\n \"ProductId\": 6,\r\n \"Quantity\": 1\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/sales/checkout", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "checkout" + ] + }, + "description": "Login as Customer1. Use the JWT Token for authorization that is returned from the Customer1 login response" + }, + "response": [] + }, + { + "name": "Customer 1 Get Sale", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales/1", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "1" + ] + }, + "description": "Login as Customer1. Use the JWT Token for authorization that is returned from the Customer1 login response" + }, + "response": [] + }, + { + "name": "Customer1 Get All Sales with Pagination, Filtering and Sorting", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales?page=1&pageSize=6&status=pending&orderBy=createdAt asc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "6" + }, + { + "key": "status", + "value": "pending" + }, + { + "key": "orderBy", + "value": "createdAt asc" + } + ] + }, + "description": "Login as Customer1. Use the JWT Token for authorization that is returned from the Customer1 login response" + }, + "response": [] + }, + { + "name": "Customer1 Get Count Of All Their Sales", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales/count", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "count" + ] + }, + "description": "Login as Customer1. Use the JWT Token for authorization that is returned from the Customer1 login response" + }, + "response": [] + }, + { + "name": "Customer1 Cancel Sale", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales/cancel/1003", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "cancel", + "1003" + ] + }, + "description": "Login as Customer1. Use the JWT Token for authorization that is returned from the Customer1 login response" + }, + "response": [] + }, + { + "name": "Admin Get A Sale", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales/admin/2", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "admin", + "2" + ] + }, + "description": "Login as admin. Use the JWT Token for authorization that is returned from the admin login response" + }, + "response": [] + }, + { + "name": "Admin Get All Customer Sales", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:7001/api/sales/admin?page=1&pageSize=3&minTotalAmount=1000&orderBy=totalAmount desc", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "pageSize", + "value": "3" + }, + { + "key": "minTotalAmount", + "value": "1000" + }, + { + "key": "orderBy", + "value": "totalAmount desc" + } + ] + }, + "description": "Login as admin. Use the JWT Token for authorization that is returned from the admin login response" + }, + "response": [] + }, + { + "name": "Admin Update A Sale's Status", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"status\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:7001/api/sales/admin/update/4", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "7001", + "path": [ + "api", + "sales", + "admin", + "update", + "4" + ] + }, + "description": "Login as admin. Use the JWT Token for authorization that is returned from the admin login response" + }, + "response": [] + } + ] + } + ], + "auth": { + "type": "bearer" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AddressesController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AddressesController.cs new file mode 100644 index 00000000..0613387b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AddressesController.cs @@ -0,0 +1,262 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AddressesController : ControllerBase +{ + private readonly IAddressService _addressService; + private readonly UserManager _userManager; + + public AddressesController( + IAddressService addressService, + UserManager userManager) + { + _addressService = addressService; + _userManager = userManager; + } + + [HttpPost("add")] + [Authorize(Roles = "customer")] + public async Task>> AddAddress([FromBody] CreateAddressDto address) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + address.CustomerId = userId; + + var result = await _addressService.AddAddressAsync(address); + + var response = ApiResponse.GetEmptyResponse; + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(201, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpPut("update/{id:int}")] + [Authorize(Roles = "customer")] + public async Task>> UpdateAddress( + [FromBody] UpdateAddressDto address, + [FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + address.Id = id; + address.CustomerId = userId; + + var result = await _addressService.UpdateAddressAsync(address); + + var response = ApiResponse.GetEmptyResponse; + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpDelete("{id:int}")] + [Authorize(Roles = "customer")] + public async Task>> DeleteAddress([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var addressIdDto = new AddressIdDto + { + Id = id, + CustomerId = userId + }; + + var result = await _addressService.DeleteAddressAsync(addressIdDto); + + var response = ApiResponse.GetEmptyResponse; + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, "Address deleted successfully"); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("{id:int}")] + [Authorize(Roles = "customer")] + public async Task>> GetAddress([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var addressIdDto = new AddressIdDto + { + Id = id, + CustomerId = userId + }; + + var result = await _addressService.GetAddressAsync(addressIdDto); + + var response = ApiResponse.GetEmptyResponse; + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpPost("admin/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> GetCustomerAddressForAdmin([FromRoute] int id, [FromBody] AddressIdDto addressIdDto) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + addressIdDto.Id = id; + + var result = await _addressService.GetAddressAsync(addressIdDto); + + var response = ApiResponse.GetEmptyResponse; + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet] + [Authorize(Roles = "admin,customer")] + public async Task>> GetAddressesForCustomer([FromQuery] AddressQueryParams queryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var userRole = User.FindFirstValue(ClaimTypes.Role); + + if (!userRole!.Equals("admin", StringComparison.OrdinalIgnoreCase)) + { + queryParams.CustomerId = userId; + } + + var result = await _addressService.GetCustomerAddressesAsync(queryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin")] + [Authorize(Roles = "admin")] + public async Task>> GetAllCustomerAddressesForAdmin([FromQuery] AddressQueryParams queryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var result = await _addressService.GetAllCustomerAddressesForAdminAsync(queryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + private async Task<(bool isUserValid, ActionResult> response)> UserValidationAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } + + private async Task<(bool isUserValid, ActionResult> response)> UserValidationPagedAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserPagedAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AuthController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AuthController.cs new file mode 100644 index 00000000..20e0793b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/AuthController.cs @@ -0,0 +1,101 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + private readonly UserManager _userManager; + + public AuthController(IAuthService authService, UserManager userManager) + { + _authService = authService; + _userManager = userManager; + } + + [HttpPost("register")] + public async Task>> Register([FromBody] UserRegistrationDto userDto) + { + var response = ApiResponse.GetEmptyResponse; + + var result = await _authService.RegisterUserAsync(userDto); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, "Registration successful"); + return StatusCode(response.StatusCode, response); + } + + [HttpPost("login")] + public async Task>> Login([FromBody] UserLoginDto loginDto) + { + var response = ApiResponse.GetEmptyResponse; + + var result = await _authService.LoginUserAsync(loginDto); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + return StatusCode(response.StatusCode, response); + } + + [HttpPost("reset")] + public async Task>> ResetPassword([FromBody] UserResetPasswordDto resetDto) + { + var response = ApiResponse.GetEmptyResponse; + + var result = await _authService.ResetPasswordAsync(resetDto); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, $"Password reset successful"); + return StatusCode(response.StatusCode, response); + } + + [HttpPost("logout")] + [Authorize] + public async Task>> Logout() + { + var response = ApiResponse.GetEmptyResponse; + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (string.IsNullOrEmpty(userId)) + { + response = new ApiResponse(404, ["User not found"]); + return StatusCode(response.StatusCode, response); + } + + var result = await _authService.LogoutUserAsync(new UserLogoutDto { UserId = userId }); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, "Logout successful"); + return StatusCode(response.StatusCode, response); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CategoriesController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CategoriesController.cs new file mode 100644 index 00000000..3f0a28c8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CategoriesController.cs @@ -0,0 +1,188 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class CategoriesController : ControllerBase +{ + private readonly ICategoryService _categoryService; + private readonly UserManager _userManager; + + public CategoriesController(ICategoryService categoryService, UserManager userManager) + { + _categoryService = categoryService; + _userManager = userManager; + } + + [HttpPost("admin/add")] + [Authorize(Roles = "admin")] + public async Task>> AddCategory([FromBody] CreateCategoryDto category) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var response = ApiResponse.GetEmptyResponse; + + var result = await _categoryService.AddCategoryAsync(category); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(201, result.Value); + return StatusCode(response.StatusCode, response); + } + + [HttpPut("admin/update/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> UpdateCategory([FromBody] UpdateCategoryDto category, [FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + category.Id = id; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _categoryService.UpdateCategoryAsync(category); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("{id:int}")] + [AllowAnonymous] + public async Task>> GetCategoryById([FromRoute] int id) + { + var response = ApiResponse.GetEmptyResponse; + + var categoryParams = new CategoryParams { CategoryId = id }; + + var result = await _categoryService.GetCategoryAsync(categoryParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> GetCategoryByIdForAdmin([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var response = ApiResponse.GetEmptyResponse; + + var categoryParams = new CategoryParams { CategoryId = id }; + + var result = await _categoryService.GetCategoryForAdminAsync(categoryParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> GetCategories([FromQuery] CategoryQueryParams categoryQueryParams) + { + var result = await _categoryService.GetCategoriesAsync(categoryQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin")] + [Authorize(Roles = "admin")] + public async Task>> GetCategoriesForAdmin([FromQuery] CategoryQueryParams categoryQueryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + var result = await _categoryService.GetCategoriesForAdminAsync(categoryQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + private async Task<(bool isUserValid, ActionResult> response)> UserValidationAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } + + private async Task<(bool isUserValid, ActionResult> response)> UserValidationPagedAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserPagedAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CustomersController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CustomersController.cs new file mode 100644 index 00000000..1386c499 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/CustomersController.cs @@ -0,0 +1,103 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.CustomerDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class CustomersController : ControllerBase +{ + private readonly ICustomerService _customerService; + private readonly UserManager _userManager; + + public CustomersController(ICustomerService customerService, UserManager userManager) + { + _customerService = customerService; + _userManager = userManager; + } + + [HttpGet("profile")] + [Authorize(Roles = "admin,customer")] + public async Task>> GetProfile() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var customerRetrieval = new CustomerRetrievalDto + { + CustomerId = userId + }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _customerService.GetCustomerProfileAsync(customerRetrieval); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin")] + [Authorize(Roles = "admin")] + public async Task>> GetCustomersForAdmin([FromQuery] CustomerQueryParams customerQueryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isUserValid, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isUserValid) + { + return errorResponse; + } + + var result = await _customerService.GetAllCustomersForAdminAsync(customerQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationPagedAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserPagedAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/AuthHelper.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/AuthHelper.cs new file mode 100644 index 00000000..9884b9cb --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/AuthHelper.cs @@ -0,0 +1,89 @@ +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Entities.TerrenceLGee.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Controllers.Helpers; + +public static class AuthHelper +{ + public static async Task<(bool isValid, ApiResponse response)> IsValidUserAsync( + UserManager + userManager, string? userId) + { + ApiResponse response; + + if (string.IsNullOrEmpty(userId)) + { + response = new ApiResponse(404, ["User Id not found"]); + return (false, response); + } + + var userState = await IsUserValidAndAuthorized(userManager, userId); + + if (userState != UserState.Authorized) + { + response = userState switch + { + UserState.NotFound => new ApiResponse(404, ["User not found"]), + UserState.Unauthorized => new ApiResponse(401, ["Unauthorized"]), + _ => new ApiResponse(400, ["Bad Request"]) + }; + return (false, response); + } + return (true, null!); + } + + public static async Task<(bool isValid, ApiResponsePaged response)> IsValidUserPagedAsync( + UserManager userManager, + string? userId) + { + ApiResponsePaged response; + + if (string.IsNullOrEmpty(userId)) + { + response = new ApiResponsePaged(404, ["User Id not found"]); + return (false, response); + } + + var userState = await IsUserValidAndAuthorized(userManager, userId); + + if (userState != UserState.Authorized) + { + response = userState switch + { + UserState.NotFound => new ApiResponsePaged(404, ["User not found"]), + UserState.Unauthorized => new ApiResponsePaged(401, ["Unauthorized"]), + _ => new ApiResponsePaged(400, ["Bad Request"]) + }; + + return (false, response); + } + + return (true, null!); + } + + private static async Task IsUserValidAndAuthorized(UserManager userManager, string? userId) + { + var user = await userManager.Users + .Where(u => u.Id.Equals(userId)) + .Include(u => u.RefreshTokens) + .FirstOrDefaultAsync(); + + if (user is null) return UserState.NotFound; + + var refreshToken = user.RefreshTokens + .LastOrDefault(); + + if (refreshToken is null || refreshToken.IsRevoked) return UserState.Unauthorized; + + return UserState.Authorized; + } +} + +public enum UserState +{ + Authorized, + Unauthorized, + NotFound +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/FailureHelper.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/FailureHelper.cs new file mode 100644 index 00000000..37c1b694 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/Helpers/FailureHelper.cs @@ -0,0 +1,33 @@ +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Common.Results; + +namespace ECommerce.Api.TerrenceLGee.Controllers.Helpers; + +public class FailureHelper +{ + public static ApiResponse HandleFailureResult(Result result) + { + var response = new ApiResponse(); + return response = result.ErrorType switch + { + ErrorType.BadRequest => new ApiResponse(400, [result.ErrorMessage ?? "Bad request"]), + ErrorType.NotFound => new ApiResponse(404, [result.ErrorMessage ?? "Not found"]), + ErrorType.Conflict => new ApiResponse(409, [result.ErrorMessage ?? "Conflict"]), + ErrorType.Unauthorized => new ApiResponse(401, [result.ErrorMessage ?? "Unauthorized"]), + _ => new ApiResponse(500, [result.ErrorMessage ?? "Internal server error"]) + }; + } + + public static ApiResponse HandleFailureResult(Result result) + { + var response = new ApiResponse(); + return response = result.ErrorType switch + { + ErrorType.BadRequest => new ApiResponse(400, [result.ErrorMessage ?? "Bad request"]), + ErrorType.NotFound => new ApiResponse(404, [result.ErrorMessage ?? "Not found"]), + ErrorType.Conflict => new ApiResponse(409, [result.ErrorMessage ?? "Conflict"]), + ErrorType.Unauthorized => new ApiResponse(401, [result.ErrorMessage ?? "Unauthorized"]), + _ => new ApiResponse(500, [result.ErrorMessage ?? "Internal server error"]) + }; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/ProductsController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/ProductsController.cs new file mode 100644 index 00000000..4ce3cf4d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/ProductsController.cs @@ -0,0 +1,251 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class ProductsController : ControllerBase +{ + private readonly IProductService _productService; + private readonly UserManager _userManager; + + public ProductsController(IProductService productService, UserManager userManager) + { + _productService = productService; + _userManager = userManager; + } + + [HttpPost("admin/add")] + [Authorize(Roles = "admin")] + public async Task>> AddProduct([FromBody] CreateProductDto product) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var response = ApiResponse.GetEmptyResponse; + + var result = await _productService.AddProductAsync(product); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(201, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpPut("admin/update/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> UpdateProduct([FromBody] UpdateProductDto product, [FromRoute ]int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + product.Id = id; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _productService.UpdateProductAsync(product); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpDelete("admin/delete/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> DeleteProduct([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var productParams = new ProductParams { ProductId = id }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _productService.DeleteProductAsync(productParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, $"Product {id} successfully marked as deleted."); + + return StatusCode(response.StatusCode, response); + } + + [HttpPost("admin/restore/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> RestoreProduct([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var productParams = new ProductParams { ProductId = id }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _productService.RestoreProductAsync(productParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, $"Product {id} successfully restored."); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("{id:int}")] + [AllowAnonymous] + public async Task>> GetProduct([FromRoute] int id) + { + var response = ApiResponse.GetEmptyResponse; + + var productParams = new ProductParams { ProductId = id }; + + var result = await _productService.GetProductAsync(productParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + + [HttpGet] + [AllowAnonymous] + public async Task>> GetProducts([FromQuery] ProductQueryParams productQueryParams) + { + var result = await _productService.GetProductsAsync(productQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> GetProductForAdmin([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var productParams = new ProductParams { ProductId = id }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _productService.GetProductForAdminAsync(productParams); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin")] + [Authorize(Roles = "admin")] + public async Task>> GetProductsForAdmin([FromQuery] ProductQueryParams productQueryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var result = await _productService.GetProductsForAdminAsync(productQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationPagedAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserPagedAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/SalesController.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/SalesController.cs new file mode 100644 index 00000000..06716388 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Controllers/SalesController.cs @@ -0,0 +1,254 @@ +using ECommerce.Api.TerrenceLGee.Controllers.Helpers; +using ECommerce.Api.TerrenceLGee.Responses; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ECommerce.Api.TerrenceLGee.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class SalesController : ControllerBase +{ + private readonly ISaleService _saleService; + private readonly UserManager _userManager; + + public SalesController(ISaleService saleService, UserManager userManager) + { + _saleService = saleService; + _userManager = userManager; + } + + [HttpPost("checkout")] + [Authorize(Roles = "customer")] + public async Task>> AddSale([FromBody] CreateOrderDto order) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + order.CustomerId = userId; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _saleService.AddSaleAsync(order); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(201, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("{id:int}")] + [Authorize(Roles = "customer")] + public async Task>> GetSale([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var requestDto = new RequestSaleDto + { + SaleId = id, + CustomerId = userId + }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _saleService.GetSaleAsync(requestDto); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> GetSaleForAdmin([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var requestDto = new RequestSaleDto + { + SaleId = id + }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _saleService.GetSaleForAdminAsync(requestDto); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, result.Value); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet] + [Authorize(Roles = "customer")] + public async Task>> GetSales([FromQuery] SaleQueryParams saleQueryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + saleQueryParams.CustomerId = userId; + + var result = await _saleService.GetSalesAsync(saleQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + [HttpGet("admin")] + [Authorize(Roles = "admin")] + public async Task>> GetAllSalesForAdmin([FromQuery] SaleQueryParams saleQueryParams) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isUserValid, errorResponse) = await UserValidationPagedAsync(userId); + + if (!isUserValid) + { + return errorResponse; + } + + var result = await _saleService.GetAllSalesForAdminAsync(saleQueryParams); + + var response = new ApiResponsePaged(200, result.Value!); + + return StatusCode(response.StatusCode, response); + } + + [HttpPut("admin/update/{id:int}")] + [Authorize(Roles = "admin")] + public async Task>> AdminUpdateSaleStatus([FromRoute] int id, [FromBody] UpdateSaleStatusDto updateSaleStatus) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + updateSaleStatus.SaleId = id; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _saleService.AdminUpdateSaleStatusAsync(updateSaleStatus); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, $"Sale {id} status successfully updated to {updateSaleStatus.Status}"); + + return StatusCode(response.StatusCode, response); + } + + [HttpPost("cancel/{id:int}")] + [Authorize(Roles = "customer")] + public async Task>> CustomerCancelSale([FromRoute] int id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var (isValidUser, errorResponse) = await UserValidationAsync(userId); + + if (!isValidUser) + { + return errorResponse; + } + + var cancelSale = new CancelSaleDto + { + SaleId = id, + CustomerId = userId + }; + + var response = ApiResponse.GetEmptyResponse; + + var result = await _saleService.CustomerCancelSaleAsync(cancelSale); + + if (result.IsFailure) + { + response = FailureHelper.HandleFailureResult(result); + return StatusCode(response.StatusCode, response); + } + + response = new ApiResponse(200, $"Sale {id} successfully canceled"); + + return StatusCode(response.StatusCode, response); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } + + private async Task<(bool isUserValid, ActionResult>)> UserValidationPagedAsync(string? userId) + { + var (isValidUser, errorResponse) = await AuthHelper.IsValidUserPagedAsync(_userManager, userId); + + if (!isValidUser) + { + return (false, StatusCode(errorResponse.StatusCode, errorResponse)); + } + + return (true, null!); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/Configuration/AuthConfiguration.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/Configuration/AuthConfiguration.cs new file mode 100644 index 00000000..ed974a23 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/Configuration/AuthConfiguration.cs @@ -0,0 +1,10 @@ +namespace ECommerce.Api.TerrenceLGee.Data.Configuration; + +public class AuthConfiguration +{ + public string Section { get; set; } = "JwtConfiguration"; + public string Key { get; init; } = string.Empty; + public string Issuer { get; init; } = string.Empty; + public string Audience { get; init; } = string.Empty; + public int RefreshTokenExpirationDays { get; init; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/CustomerConfiguration.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/CustomerConfiguration.cs new file mode 100644 index 00000000..39bd61ab --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/CustomerConfiguration.cs @@ -0,0 +1,26 @@ +using ECommerce.Entities.TerrenceLGee.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ECommerce.Api.TerrenceLGee.Data.DatabaseConfigs; + +public class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasMany(c => c.Sales) + .WithOne(s => s.Customer) + .HasForeignKey(s => s.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany
(c => c.Addresses) + .WithOne(a => a.Customer) + .HasForeignKey(a => a.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(u => u.RefreshTokens) + .WithOne(rt => rt.User) + .HasForeignKey(rt => rt.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/SaleProductConfiguration.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/SaleProductConfiguration.cs new file mode 100644 index 00000000..48d7fded --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseConfigs/SaleProductConfiguration.cs @@ -0,0 +1,21 @@ +using ECommerce.Entities.TerrenceLGee.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ECommerce.Api.TerrenceLGee.Data.DatabaseConfigs; + +public class SaleProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(sp => new { sp.SaleId, sp.ProductId }); + + builder.HasOne(sp => sp.Sale) + .WithMany(s => s.SaleProducts) + .HasForeignKey(sp => sp.SaleId); + + builder.HasOne(sp => sp.Product) + .WithMany(p => p.SaleProducts) + .HasForeignKey(sp => sp.ProductId); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseSeeder.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseSeeder.cs new file mode 100644 index 00000000..70b60893 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/DatabaseSeeder.cs @@ -0,0 +1,871 @@ +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Enums; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Data; + +public static class DatabaseSeeder +{ + private static readonly string Admin = "admin"; + private static readonly string Customer = "customer"; + private static readonly string Password = "Pa$$w0rd"; + + public static async Task SeedUsersAsync( + ECommerceDbContext dbContext, + UserManager userManager, + RoleManager roleManager) + { + if (await dbContext.Users.AnyAsync()) return; + + var adminRole = new IdentityRole { Name = Admin }; + var customerRole = new IdentityRole { Name = Customer }; + + await roleManager.CreateAsync(adminRole); + await roleManager.CreateAsync(customerRole); + + var admin = new ApplicationUser + { + FirstName = "Gordon", + LastName = "Ramsay", + DateOfBirth = DateOnly.Parse("11-08-1966"), + RegistrationDate = new DateOnly(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day), + Email = "admin@example.com", + UserName = "admin@example.com" + }; + + await userManager.CreateAsync(admin, Password); + await userManager.AddToRoleAsync(admin, Admin); + + var customer1 = new ApplicationUser + { + FirstName = "Tom", + LastName = "Jones", + DateOfBirth = DateOnly.Parse("03-10-1993"), + RegistrationDate = new DateOnly(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day), + Email = "tjones@example.com", + UserName = "tjones@example.com", + Addresses = new[] + { + new Address() + { + AddressLine1 = "123 Main St.", + AddressLine2 = null, + City = "New York City", + State = "New York", + PostalCode = "123456", + Country = "USA", + IsBillingAddress = true, + IsShippingAddress = false + }, + new Address() + { + AddressLine1 = "999 State St.", + AddressLine2 = null, + City = "New York City", + State = "New York", + PostalCode = "654321", + Country = "USA", + IsBillingAddress = false, + IsShippingAddress = true + } + }, + }; + + await userManager.CreateAsync(customer1, Password); + await userManager.AddToRoleAsync(customer1, Customer); + + await SeedSalesAsync(dbContext, customer1.Id); + } + + public static async Task SeedCategoriesAsync(ECommerceDbContext dbContext) + { + if (await dbContext.Categories.AnyAsync()) return; + + var categories = new[] + { + new Category + { + Name = "Clothing", + Description = "A wide assortment of men's, women's and children's clothing in a variety of sizes.", + CreatedAt = DateTime.UtcNow, + }, + new Category + { + Name = "Electronics", + Description = "A wide assortment of electronics ranging from stereo systems to computers and everything in between.", + CreatedAt = DateTime.UtcNow, + }, + new Category + { + Name = "Music", + Description = "Almost any musical genre that you can image with a wide range of music artists available on both vinyl and CD.", + CreatedAt = DateTime.UtcNow, + }, + new Category + { + Name = "Books", + Description = "Books on almost every subject from A-Z.", + CreatedAt = DateTime.UtcNow, + }, + new Category + { + Name = "Movies", + Description = "Almost any movie or television show that you can think of it is available on DVD, we have it.", + CreatedAt = DateTime.UtcNow, + }, + new Category + { + Name = "Fitness", + Description = "Everything from cardiovascular machines to heavy weight lifting equipment.", + CreatedAt = DateTime.UtcNow, + } + }; + + await dbContext.Categories.AddRangeAsync(categories); + await dbContext.SaveChangesAsync(); + } + + public static async Task SeedProductsAsync(ECommerceDbContext dbContext) + { + if (await dbContext.Products.AnyAsync()) return; + + var categories = dbContext.Categories; + + if (!await categories.AnyAsync()) return; + + var clothingCategory = categories.FirstOrDefault(c => c.Name.Equals("Clothing")); + var electronicsCategory = categories.FirstOrDefault(c => c.Name.Equals("Electronics")); + var musicCategory = categories.FirstOrDefault(c => c.Name.Equals("Music")); + var booksCategory = categories.FirstOrDefault(c => c.Name.Equals("Books")); + var movieCategory = categories.FirstOrDefault(c => c.Name.Equals("Movies")); + var fitnessCategory = categories.FirstOrDefault(c => c.Name.Equals("Fitness")); + + if (clothingCategory is null + || electronicsCategory is null + || musicCategory is null + || booksCategory is null + || movieCategory is null + || fitnessCategory is null) return; + + var products = new[] + { + new Product + { + CategoryId = clothingCategory.Id, + Name = "Women's night gown", + Description = "A very comfortable women's night gown available in all sizes and a variety of colors. Perfect for a good night's sleep or just relaxing around the house", + StockQuantity = 30, + UnitPrice = 15.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/A1eIf+Hj4YL._AC_SX679_.jpg" + }, + new Product + { + CategoryId = clothingCategory.Id, + Name = "Unisex jogging suit", + Description = "2 piece casual unisex jogging suit, comfortable and available in S, M, L, XL and XXL. Comes in a wide choice of colors.", + StockQuantity = 45, + UnitPrice = 27.99m, + DiscountPercentage = 10, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71ZfDaCP4nL._AC_SX679_.jpg" + }, + new Product + { + CategoryId = clothingCategory.Id, + Name = "Children's pajams", + Description = "Made for children male or female ages 3 to 11. Comes in a variety of sizes and colors.", + StockQuantity = 120, + UnitPrice = 12.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71oGda6DyEL._AC_SX569_.jpg" + }, + new Product + { + CategoryId = clothingCategory.Id, + Name = "Men's dress shirt", + Description = "Perfect for everything from religious services to job interviews to date night at a nice resturant. Comes in White, Navy Blue, and Grey. Sizes: S, M, L, XL, XXL.", + StockQuantity = 20, + UnitPrice = 45.99m, + DiscountPercentage = 15, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/51rkKPruYvL._AC_SX679_.jpg" + }, + new Product + { + CategoryId = electronicsCategory.Id, + Name = "Apple iMac", + Description = "Apple iMac All-in-One Desktop Computer with M4 chip with 8-core CPU and 8-core GPU: Built for Apple Intelligence, 24-inch Retina Display, 16GB Unified Memory, 256GB SSD Storage.", + StockQuantity = 10, + UnitPrice = 1031.38m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71gqlRrQCuL.jpg" + }, + new Product + { + CategoryId = electronicsCategory.Id, + Name = "HP All-In-One Desktop PC", + Description = "HP 24 inch All-in-One Desktop PC, FHD Display, AMD Ryzen 7 7730U, 16 GB RAM, 512 GB SSD, AMD Radeon Graphics, Windows 11 Home, 24-cr0032.", + StockQuantity = 20, + UnitPrice = 779.20m, + DiscountPercentage = 5, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/81OlLtbjieL.jpg" + }, + new Product + { + CategoryId = electronicsCategory.Id, + Name = "PHILIPS FX10 Bluetooth Stereo System", + Description = "PHILIPS FX10 Bluetooth Stereo System for Home with CD Player , MP3, USB, FM Radio, Bass Reflex Speaker, 230 W, Remote Control Included.", + StockQuantity = 25, + UnitPrice = 249.99m, + DiscountPercentage = 12, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/815e4H+YBaL.jpg" + }, + new Product + { + CategoryId = electronicsCategory.Id, + Name = "MECHEN M30 HiFi MP3 Player", + Description = "Lossless DSD High Resolution Digital Audio Music Player, High-Res Portable Audio Player with 64GB Memory Card.", + StockQuantity = 30, + UnitPrice = 79.98m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71xJEJVlVwL.jpg" + }, + new Product + { + CategoryId = musicCategory.Id, + Name = "The Temptations With A Lot O' Soul", + Description = "Considered one of The Temptations best and most beloved albums.", + StockQuantity = 60, + UnitPrice = 21.99m, + DiscountPercentage = 5, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/61-AAZI6B1L._SY300_SX300_QL70_FMwebp_.jpg" + }, + new Product + { + CategoryId = musicCategory.Id, + Name = "Sgt. Pepper's Lonely Hearts Club Band Deluxe", + Description = "Deluxe edition of one of the most beloved albums of all time. The Beatles Sgt. Pepper's Lonely Hearts Club Band.", + StockQuantity = 50, + UnitPrice = 39.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/91wYFN2CBRL._SX425_.jpg" + }, + new Product + { + CategoryId = musicCategory.Id, + Name = "60 Greatest Hits of Sam Cooke", + Description = "Enjoy some of the greatest hits of Sam Cooke includes all of your favorites.", + StockQuantity = 40, + UnitPrice = 39.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71Q3UNQRYHL._SX425_.jpg" + }, + new Product + { + CategoryId = musicCategory.Id, + Name = "Black Sabbath Self-Title Debut", + Description = "Go back to where it started for the godfathers of heavy metal.", + StockQuantity = 30, + UnitPrice = 15.99m, + DiscountPercentage = 5, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/81RqWZVeexL._SX425_.jpg" + }, + new Product + { + CategoryId = booksCategory.Id, + Name = "YOU TOO CAN BE PROSPEROUS", + Description = "Learn the spiritual secrets of an abundant, successful and prosperous life. By Robert A. Russel.", + StockQuantity = 15, + UnitPrice = 9.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/51nO--WOBkL._SY445_SX342_FMwebp_.jpg" + }, + new Product + { + CategoryId = booksCategory.Id, + Name = "The C# Player's Guide (5th Edition)", + Description = "One of the best books for beginners looking to begin their journey into C# Programming. By RB Whitaker.", + StockQuantity = 45, + UnitPrice = 34.99m, + DiscountPercentage = 15, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/619vzxml9jL._AC_UF1000,1000_QL80_.jpg" + }, + new Product + { + CategoryId = booksCategory.Id, + Name = "C# Data Structures and Algorithms 2nd Edition", + Description = "When you are ready to take your C# development and programming know how to the next level. By Marcin Jamro.", + StockQuantity = 30, + UnitPrice = 26.37m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/610Svp+mOkL._AC_UF1000,1000_QL80_.jpg" + }, + new Product + { + CategoryId = booksCategory.Id, + Name = "Clean Code with C# 2nd Edition", + Description = "Enhance your programming skills through code reviews, TDD and BDD implementation, and API design to overcome code inefficiency, redundancy, and other issues arising from bad code Key Features. By Jason Alls.", + StockQuantity = 15, + UnitPrice = 45.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/418LKBgGA2L._SX342_SY445_FMwebp_.jpg" + }, + new Product + { + CategoryId = movieCategory.Id, + Name = "Rope", + Description = "Classic Alfred Hitchcock movie from 1948 in color. Starring James Stewart, Farley Granger and John Dall.", + StockQuantity = 30, + UnitPrice = 6.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/513VY7Vkt9L._SY300_SX300_QL70_FMwebp_.jpg" + }, + new Product + { + CategoryId = movieCategory.Id, + Name = "Blue Steel", + Description = "Action packed thriller that will have you on the edge of your seat. Starring Jamie Lee Curtis and Ron Silver.", + StockQuantity = 50, + UnitPrice = 13.29m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71gwSJPuX6L._SY445_.jpg" + }, + new Product + { + CategoryId = movieCategory.Id, + Name = "Saw: 10-Film Collection", + Description = "Relive all of the terror of Jigsaw in this 10 film collection. Starring Tobin Bell, Shawnee Smith and others", + StockQuantity = 50, + UnitPrice = 39.96m, + DiscountPercentage = 10, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/51IQSyPXYBL._SY300_SX300_QL70_FMwebp_.jpg" + }, + new Product + { + CategoryId = movieCategory.Id, + Name = "4 Film Favorites: Denzel Washington", + Description = "Enjoy 4 action packed films starring Denzel Washington in this budget priced collection.", + StockQuantity = 50, + UnitPrice = 14.97m, + DiscountPercentage = 10, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/8164b7a5DvL._SX342_.jpg" + }, + new Product + { + CategoryId = fitnessCategory.Id, + Name = "Adjustable Dumbbells Set of 2", + Description = "Free Weights Dumbbells Set,Adjustable Dumbbell Set,52.5 lbs pair 105 lbs,15 in 1,for Men/Women Gym Equipment for Home Strength Training Equipment.", + StockQuantity = 30, + UnitPrice = 249.99m, + DiscountPercentage = 0, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71V3y5K2QRL._AC_SY300_SX300_QL70_FMwebp_.jpg" + }, + new Product + { + CategoryId = fitnessCategory.Id, + Name = "Body-Solid Multi-Station", + Description = "Single Weight Stack Home Gym Machine, Arm & Leg Strength Training Functional Exercise Workout Station, 210lbs. Black Weight Stacks.", + StockQuantity = 50, + UnitPrice = 1695.00m, + DiscountPercentage = 30, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/61NGD9LBiVL._AC_SL1028_.jpg" + }, + new Product + { + CategoryId = fitnessCategory.Id, + Name = "Niceday Elliptical Machine", + Description = "Elliptical Exercise Machine for Home with Hyper-Quiet Magnetic Driving System, Elliptical Trainer with 15.5IN & 20IN Stride, 16 Resistance Levels, 500LBS Loading Capacity.", + StockQuantity = 10, + UnitPrice = 369.99m, + DiscountPercentage = 20, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71out2dpOAL._AC_UF1000,1000_QL80_.jpg" + }, + new Product + { + CategoryId = fitnessCategory.Id, + Name = "MERACH Recumbent Exercise Bike", + Description = "For Home useage with Smart Bluetooth Equipment Exercise Bikes App,LCD,Heart Rate Handle Stationary Bikes for Home, Magnetic Recumbent Exercise Bike for Seniors Gym S08/S23.", + StockQuantity = 50, + UnitPrice = 170.88m, + DiscountPercentage = 15, + IsDeleted = false, + IsInStock = true, + CreatedAt = DateTime.UtcNow, + ImageUrl = "https://m.media-amazon.com/images/I/71bd5H+u3PL._AC_SY300_SX300_QL70_FMwebp_.jpg" + }, + }; + + await dbContext.Products.AddRangeAsync(products); + await dbContext.SaveChangesAsync(); + } + + private static async Task SeedSalesAsync(ECommerceDbContext dbContext, string? customerId) + { + if (string.IsNullOrEmpty(customerId)) return; + + if (await dbContext.Sales.AnyAsync()) return; + + var products = await dbContext.Products.ToListAsync(); + + var sales = new[] + { + new Sale() + { + CustomerId = customerId, + TotalBaseAmount = 403.26m, + TotalDiscountAmount = 73.99m, + TotalAmount = 329.27m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleStatus = SaleStatus.Processing, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Niceday Elliptical Machine")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 369.99m, + Discount = 73.99m, + TotalPrice = 296.00m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("YOU TOO CAN BE PROSPEROUS")) + .Select(p => p.Id) + .First(), + Quantity = 2, + Price = 9.99m, + Discount = 0m, + TotalPrice = 19.98m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Blue Steel")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 13.29m, + Discount = 0m, + TotalPrice = 13.29m + } + } + }, + new Sale() + { + CustomerId = customerId, + TotalBaseAmount = 1992.93m, + TotalDiscountAmount = 529.20m, + TotalAmount = 1643.73m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleStatus = SaleStatus.Shipped, + SaleProducts = new List + { + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("Body-Solid Multi-Station")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 1695.00m, + Discount = 508.50m, + TotalPrice = 1186.50m + }, + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("MECHEN M30 HiFi MP3 Player")) + .Select(p => p.Id) + .First(), + Quantity = 2, + Price = 79.98m, + Discount = 0m, + TotalPrice = 159.96m + }, + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("Men's dress shirt")) + .Select(p => p.Id) + .First(), + Quantity = 3, + Price = 45.99m, + Discount = 6.90m, + TotalPrice = 117.27m + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Delivered, + TotalBaseAmount = 2314.98m, + TotalDiscountAmount = 582.50m, + TotalAmount = 1732.48m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("Adjustable Dumbbells Set of 2")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 249.99m, + Discount = 0m, + TotalPrice = 249.99m + }, + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("Niceday Elliptical Machine")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 369.99m, + Discount = 74.00m, + TotalPrice = 295.99m + }, + new SaleProduct + { + ProductId = products.Where(p => p.Name.Equals("Body-Solid Multi-Station")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 1695.00m, + Discount = 508.50m, + TotalPrice = 1186.50m, + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Pending, + TotalBaseAmount = 85.97m, + TotalDiscountAmount = 11.30m, + TotalAmount = 74.67m, + CreatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Black Sabbath Self-Title Debut")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 15.99m, + Discount = 0.80m, + TotalPrice = 15.19m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("The C# Player's Guide (5th Edition)")) + .Select(p => p.Id) + .First(), + Quantity = 2, + Price = 34.99m, + Discount = 5.25m, + TotalPrice = 59.48m + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Delivered, + TotalBaseAmount = 845.56m, + TotalDiscountAmount = 38.96m, + TotalAmount = 806.60m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("C# Data Structures and Algorithms 2nd Edition")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 26.37m, + Discount = 0.0m, + TotalPrice = 26.37m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("HP All-In-One Desktop PC")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 779.20m, + Discount = 38.96m, + TotalPrice = 740.24m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("60 Greatest Hits of Sam Cooke")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 39.99m, + Discount = 0.0m, + TotalPrice = 39.99m + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Canceled, + TotalBaseAmount = 95.94m, + TotalDiscountAmount = 0.0m, + TotalAmount = 95.94m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Women's night gown")) + .Select(p => p.Id) + .First(), + Quantity = 6, + Price = 15.99m, + Discount = 0.0m, + TotalPrice = 95.94m + }, + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Shipped, + TotalBaseAmount = 2472.68m, + TotalDiscountAmount = 257.81m, + TotalAmount = 2214.87m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Unisex jogging suit")) + .Select(p => p.Id) + .First(), + Quantity = 3, + Price = 27.99m, + Discount = 2.80m, + TotalPrice = 75.57m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Apple iMac")) + .Select(p => p.Id) + .First(), + Quantity = 2, + Price = 1031.38m, + Discount = 0.0m, + TotalPrice = 2062.76m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("PHILIPS FX10 Bluetooth Stereo System")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 249.99m, + Discount = 30.00m, + TotalPrice = 219.99m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("The Temptations With A Lot O' Soul")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 21.99m, + Discount = 1.10m, + TotalPrice = 20.89m, + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Sgt. Pepper's Lonely Hearts Club Band Deluxe")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 39.99m, + Discount = 0.0m, + TotalPrice = 39.99m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Rope")) + .Select(p => p.Id) + .First(), + Quantity = 2, + Price = 6.99m, + Discount = 0.0m, + TotalPrice = 13.98m + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Pending, + TotalBaseAmount = 256.83m, + TotalDiscountAmount = 36.53m, + TotalAmount = 220.30m, + CreatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Saw: 10-Film Collection")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 39.96m, + Discount = 4.00m, + TotalPrice = 35.96m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("MERACH Recumbent Exercise Bike")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 170.88m, + Discount = 25.63m, + TotalPrice = 145.25m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Men's dress shirt")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 45.99m, + Discount = 6.90m, + TotalPrice = 39.09m + } + } + }, + new Sale() + { + CustomerId = customerId, + SaleStatus = SaleStatus.Canceled, + TotalBaseAmount = 514.95m, + TotalDiscountAmount = 31.50m, + TotalAmount = 483.45m, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + SaleProducts = new List + { + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("PHILIPS FX10 Bluetooth Stereo System")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 249.99m, + Discount = 30.00m, + TotalPrice = 219.99m, + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("4 Film Favorites: Denzel Washington")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 14.97m, + Discount = 1.50m, + TotalPrice = 13.47m + }, + new SaleProduct() + { + ProductId = products.Where(p => p.Name.Equals("Adjustable Dumbbells Set of 2")) + .Select(p => p.Id) + .First(), + Quantity = 1, + Price = 249.99m, + Discount = 0.0m, + TotalPrice = 249.99m + } + } + } + }; + + await dbContext.Sales.AddRangeAsync(sales); + await dbContext.SaveChangesAsync(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/ECommerceDbContext.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/ECommerceDbContext.cs new file mode 100644 index 00000000..36e444ee --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Data/ECommerceDbContext.cs @@ -0,0 +1,28 @@ +using ECommerce.Api.TerrenceLGee.Data.Configuration; +using ECommerce.Api.TerrenceLGee.Data.DatabaseConfigs; +using ECommerce.Entities.TerrenceLGee.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Data; + +public class ECommerceDbContext : IdentityDbContext +{ + public DbSet
Addresses { get; set; } + public DbSet Categories { get; set; } + public DbSet Products { get; set; } + public DbSet RefreshTokens { get; set; } + public DbSet Sales { get; set; } + public DbSet SaleProducts { get; set; } + + public ECommerceDbContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ApplyConfiguration(new CustomerConfiguration()); + builder.ApplyConfiguration(new SaleProductConfiguration()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.csproj b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.csproj new file mode 100644 index 00000000..cb1ed852 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.http b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.http new file mode 100644 index 00000000..20bd8e7b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/ECommerce.Api.TerrenceLGee.http @@ -0,0 +1,6 @@ +@ECommerce.Api.TerrenceLGee_HostAddress = http://localhost:5117 + +GET {{ECommerce.Api.TerrenceLGee_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.Designer.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.Designer.cs new file mode 100644 index 00000000..34d826ea --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.Designer.cs @@ -0,0 +1,608 @@ +// +using System; +using ECommerce.Api.TerrenceLGee.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ECommerce.Api.TerrenceLGee.Migrations +{ + [DbContext(typeof(ECommerceDbContext))] + [Migration("20260307235031_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressLine1") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AddressLine2") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(450)"); + + b.Property("IsBillingAddress") + .HasColumnType("bit"); + + b.Property("IsShippingAddress") + .HasColumnType("bit"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RegistrationDate") + .HasColumnType("date"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DiscountPercentage") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsInStock") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StockQuantity") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("IsRevoked") + .HasColumnType("bit"); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CustomerId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SaleStatus") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalBaseAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalDiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.SaleProduct", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleProducts"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Address", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.RefreshToken", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "Customer") + .WithMany("Sales") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.SaleProduct", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", b => + { + b.Navigation("Addresses"); + + b.Navigation("RefreshTokens"); + + b.Navigation("Sales"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.cs new file mode 100644 index 00000000..ecb1d4b9 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/20260307235031_InitialMigration.cs @@ -0,0 +1,420 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ECommerce.Api.TerrenceLGee.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + DateOfBirth = table.Column(type: "date", nullable: false), + RegistrationDate = table.Column(type: "date", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CustomerId = table.Column(type: "nvarchar(450)", nullable: true), + AddressLine1 = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + AddressLine2 = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + City = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + State = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + PostalCode = table.Column(type: "nvarchar(max)", nullable: false), + Country = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + IsBillingAddress = table.Column(type: "bit", nullable: false), + IsShippingAddress = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + table.ForeignKey( + name: "FK_Addresses_AspNetUsers_CustomerId", + column: x => x.CustomerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Token = table.Column(type: "nvarchar(max)", nullable: false), + JwtId = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + Expires = table.Column(type: "datetime2", nullable: false), + IsRevoked = table.Column(type: "bit", nullable: false), + RevokedAt = table.Column(type: "datetime2", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Sales", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CustomerId = table.Column(type: "nvarchar(450)", nullable: false), + TotalBaseAmount = table.Column(type: "decimal(18,2)", nullable: false), + TotalDiscountAmount = table.Column(type: "decimal(18,2)", nullable: false), + TotalAmount = table.Column(type: "decimal(18,2)", nullable: false), + SaleStatus = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.Id); + table.ForeignKey( + name: "FK_Sales_AspNetUsers_CustomerId", + column: x => x.CustomerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CategoryId = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + StockQuantity = table.Column(type: "int", nullable: false), + UnitPrice = table.Column(type: "decimal(18,2)", nullable: false), + DiscountPercentage = table.Column(type: "int", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false), + IsInStock = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SaleProducts", + columns: table => new + { + SaleId = table.Column(type: "int", nullable: false), + ProductId = table.Column(type: "int", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + Discount = table.Column(type: "decimal(18,2)", nullable: false), + TotalPrice = table.Column(type: "decimal(18,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaleProducts", x => new { x.SaleId, x.ProductId }); + table.ForeignKey( + name: "FK_SaleProducts_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SaleProducts_Sales_SaleId", + column: x => x.SaleId, + principalTable: "Sales", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UserId", + table: "RefreshTokens", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleProducts_ProductId", + table: "SaleProducts", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_Sales_CustomerId", + table: "Sales", + column: "CustomerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "RefreshTokens"); + + migrationBuilder.DropTable( + name: "SaleProducts"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Sales"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/ECommerceDbContextModelSnapshot.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/ECommerceDbContextModelSnapshot.cs new file mode 100644 index 00000000..1d378c72 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Migrations/ECommerceDbContextModelSnapshot.cs @@ -0,0 +1,605 @@ +// +using System; +using ECommerce.Api.TerrenceLGee.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ECommerce.Api.TerrenceLGee.Migrations +{ + [DbContext(typeof(ECommerceDbContext))] + partial class ECommerceDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressLine1") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AddressLine2") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(450)"); + + b.Property("IsBillingAddress") + .HasColumnType("bit"); + + b.Property("IsShippingAddress") + .HasColumnType("bit"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RegistrationDate") + .HasColumnType("date"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DiscountPercentage") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsInStock") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StockQuantity") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("IsRevoked") + .HasColumnType("bit"); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RevokedAt") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CustomerId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SaleStatus") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalBaseAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalDiscountAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.SaleProduct", b => + { + b.Property("SaleId") + .HasColumnType("int"); + + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("Discount") + .HasColumnType("decimal(18,2)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.HasKey("SaleId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("SaleProducts"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Address", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.RefreshToken", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", "Customer") + .WithMany("Sales") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.SaleProduct", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Product", "Product") + .WithMany("SaleProducts") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.Sale", "Sale") + .WithMany("SaleProducts") + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Sale"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.ApplicationUser", b => + { + b.Navigation("Addresses"); + + b.Navigation("RefreshTokens"); + + b.Navigation("Sales"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Category", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Product", b => + { + b.Navigation("SaleProducts"); + }); + + modelBuilder.Entity("ECommerce.Entities.TerrenceLGee.Models.Sale", b => + { + b.Navigation("SaleProducts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Program.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Program.cs new file mode 100644 index 00000000..6bfddba1 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Program.cs @@ -0,0 +1,122 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Data.Configuration; +using ECommerce.Api.TerrenceLGee.Repositories; +using ECommerce.Api.TerrenceLGee.Services; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.IdentityModel.Tokens; +using Serilog; +using System.Text; + +var loggingDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Logs"); +Directory.CreateDirectory(loggingDirectory); +var filePath = Path.Combine(loggingDirectory, "app-.txt"); +var outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .WriteTo.File(filePath, rollingInterval: RollingInterval.Day, outputTemplate: outputTemplate) + .CreateLogger(); + +var builder = WebApplication.CreateBuilder(args); + + +builder.Services.AddControllers(); + +builder.Services.AddOpenApi(); + +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) + .ConfigureWarnings(warnings => + { + warnings.Ignore(RelationalEventId.PendingModelChangesWarning); + }); +}); + +builder.Services.AddIdentity(options => +{ + options.Password.RequireDigit = true; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + options.User.RequireUniqueEmail = true; +}) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + +var authConfiguration = new AuthConfiguration(); +builder.Configuration.GetSection(authConfiguration.Section) + .Bind(authConfiguration); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = authConfiguration.Issuer, + ValidAudience = authConfiguration.Audience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authConfiguration.Key)) + }; + }); + +builder.Services.Configure(builder.Configuration.GetSection(authConfiguration.Section)); + +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(); + +var app = builder.Build(); + + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + await dbContext.Database.EnsureCreatedAsync(); + + await DatabaseSeeder.SeedCategoriesAsync(dbContext); + await DatabaseSeeder.SeedProductsAsync(dbContext); + await DatabaseSeeder.SeedUsersAsync(dbContext, userManager, roleManager); +} + +app.Run(); diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Properties/launchSettings.json b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Properties/launchSettings.json new file mode 100644 index 00000000..f3b051b7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/AddressRepository.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/AddressRepository.cs new file mode 100644 index 00000000..a95d11fe --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/AddressRepository.cs @@ -0,0 +1,221 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Repositories.Helpers; +using ECommerce.Contracts.TerrenceLGee.Common.Extensions; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Repositories; + +public class AddressRepository : IAddressRepository +{ + private readonly ECommerceDbContext _context; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public AddressRepository(ECommerceDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task AddAddressAsync(Address address) + { + try + { + await _context.Addresses.AddAsync(address); + await _context.SaveChangesAsync(); + + return address; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(AddAddressAsync)}\n" + + $"There was an unexpected error adding the address for customer with Id {address.CustomerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + + } + + public async Task UpdateAddressAsync(Address address) + { + try + { + var addressToUpdate = await _context.Addresses + .FirstOrDefaultAsync(a => a.Id == address.Id && + !string.IsNullOrEmpty(a.CustomerId) && + a.CustomerId.Equals(address.CustomerId)); + + if (addressToUpdate is null) return null; + + addressToUpdate.AddressLine1 = address.AddressLine1; + addressToUpdate.AddressLine2 = address.AddressLine2; + addressToUpdate.City = address.City; + addressToUpdate.State = address.State; + addressToUpdate.PostalCode = address.PostalCode; + addressToUpdate.Country = address.Country; + addressToUpdate.IsBillingAddress = address.IsBillingAddress; + addressToUpdate.IsShippingAddress = address.IsShippingAddress; + + await _context.SaveChangesAsync(); + + return addressToUpdate; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(UpdateAddressAsync)}\n" + + $"There was an unexpected error updating {address.Id} for customer {address.CustomerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task DeleteAddressAsync(int addressId, string? customerId) + { + try + { + var addressToDelete = await _context.Addresses + .FirstOrDefaultAsync(a => a.Id == addressId && + !string.IsNullOrEmpty(a.CustomerId) && + a.CustomerId.Equals(customerId)); + + if (addressToDelete is null) return false; + + _context.Addresses.Remove(addressToDelete); + await _context.SaveChangesAsync(); + + return true; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(DeleteAddressAsync)}\n" + + $"There was an unexpected error deleting address {addressId} for customer {customerId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return false; + } + } + + public async Task GetAddressAsync(int addressId, string? customerId) + { + try + { + var address = await _context.Addresses + .Include(a => a.Customer) + .AsNoTracking() + .FirstOrDefaultAsync(a => a.Id == addressId && + !string.IsNullOrEmpty(a.CustomerId) && + a.CustomerId.Equals(customerId)); + + return address; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(GetAddressAsync)}\n" + + $"There was an unexpected error retrieving address {addressId} for customer {customerId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task> GetCustomerAddressesAsync(AddressQueryParams addressQueryParams) + { + try + { + var addresses = _context.Addresses + .Where(a => a.CustomerId != null && + a.CustomerId.Equals(addressQueryParams.CustomerId)) + .Include(a => a.Customer) + .AsNoTracking(); + + SetFilteringAndSorting(ref addresses, addressQueryParams); + + return await addresses.ToPagedListAsync(addresses.Count(), addressQueryParams.Page, addressQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(GetAddressAsync)}\n" + + $"There was an unexpected error retrieving the addresses for customer {addressQueryParams.CustomerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + + } + + public async Task> GetAllCustomerAddressesForAdminAsync(AddressQueryParams addressQueryParams) + { + try + { + var addresses = _context.Addresses + .Include(a => a.Customer) + .AsNoTracking(); + + SetFilteringAndSorting(ref addresses, addressQueryParams); + + return await addresses.ToPagedListAsync(addresses.Count(), addressQueryParams.Page, addressQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressRepository)}\n" + + $"Method: {nameof(GetAllCustomerAddressesForAdminAsync)}\n" + + $"There was an unexpected error retrieving all customer addresses: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + + private static void SetFilteringAndSorting(ref IQueryable
addresses, AddressQueryParams addressQueryParams) + { + if (!string.IsNullOrEmpty(addressQueryParams.CustomerId)) + { + addresses = addresses + .Where(a => a.Customer != null && !string.IsNullOrEmpty(a.CustomerId) && + a.CustomerId.Equals(addressQueryParams.CustomerId)); + } + + if (!string.IsNullOrEmpty(addressQueryParams.CustomerFirstName)) + { + addresses = addresses + .Where(a => a.Customer != null && + a.Customer.FirstName.ToLower() + .Equals(addressQueryParams.CustomerFirstName.ToLower())); + } + + if (!string.IsNullOrEmpty(addressQueryParams.CustomerLastName)) + { + addresses = addresses + .Where(a => a.Customer != null && + a.Customer.LastName.ToLower() + .Equals(addressQueryParams.CustomerLastName.ToLower())); + } + + if (!string.IsNullOrEmpty(addressQueryParams.City)) + { + addresses = addresses + .Where(a => a.City.ToLower().Equals(addressQueryParams.City.ToLower())); + } + + if (!string.IsNullOrEmpty(addressQueryParams.State)) + { + addresses = addresses + .Where(a => a.State.ToLower().Equals(addressQueryParams.State.ToLower())); + } + + if (!string.IsNullOrEmpty(addressQueryParams.Country)) + { + addresses = addresses + .Where(a => a.Country.ToLower().Equals(addressQueryParams.Country.ToLower())); + } + + addresses = SortHelper
.ApplySorting(addresses, addressQueryParams.OrderBy); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CategoryRepository.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CategoryRepository.cs new file mode 100644 index 00000000..c4d497a5 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CategoryRepository.cs @@ -0,0 +1,131 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Repositories.Helpers; +using ECommerce.Contracts.TerrenceLGee.Common.Extensions; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Repositories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly ECommerceDbContext _context; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public CategoryRepository( + ECommerceDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task AddCategoryAsync(Category category) + { + try + { + var categoryName = category.Name.ToLower(); + + var categoryAlreadyExists = await _context.Categories + .AnyAsync(c => c.Name.ToLower().Equals(categoryName)); + + if (categoryAlreadyExists) return null; + + category.CreatedAt = DateTime.UtcNow; + + await _context.Categories.AddAsync(category); + await _context.SaveChangesAsync(); + + return category; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryRepository)}\n" + + $"Method: {nameof(AddCategoryAsync)}\n" + + $"There was an unexpected error adding a new category: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task UpdateCategoryAsync(Category category) + { + try + { + var categoryToUpdate = await _context.Categories + .Include(c => c.Products) + .FirstOrDefaultAsync(c => c.Id == category.Id); + + if (categoryToUpdate is null) return null; + + categoryToUpdate.Name = category.Name; + categoryToUpdate.Description = category.Description; + categoryToUpdate.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return categoryToUpdate; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryRepository)}\n" + + $"Method: {nameof(UpdateCategoryAsync)}\n" + + $"There was an unexpected error updating category {category.Id}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task GetCategoryAsync(int categoryId) + { + try + { + var category = await _context.Categories + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == categoryId); + + return category; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryRepository)}\n" + + $"Method: {nameof(GetCategoryAsync)}\n" + + $"There was an unexpected error retrieving category {categoryId}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task> GetCategoriesAsync(CategoryQueryParams categoryQueryParams) + { + try + { + var categories = _context.Categories + .AsNoTracking(); + + SetFilteringAndSorting(ref categories, categoryQueryParams); + + return await categories.ToPagedListAsync(categories.Count(), categoryQueryParams.Page, categoryQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryRepository)}\n" + + $"Method: {nameof(GetCategoriesAsync)}\n" + + $"There was an unexpected error retrieving the categories: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + private static void SetFilteringAndSorting(ref IQueryable categories, CategoryQueryParams categoryQueryParams) + { + if (!string.IsNullOrEmpty(categoryQueryParams.Description)) + { + categories = categories.Where(c => !string.IsNullOrEmpty(c.Description) + && c.Description.ToLower().Contains(categoryQueryParams.Description.ToLower())); + } + + categories = SortHelper.ApplySorting(categories, categoryQueryParams.OrderBy); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CustomerRepository.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CustomerRepository.cs new file mode 100644 index 00000000..d51d3d24 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/CustomerRepository.cs @@ -0,0 +1,103 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Repositories.Helpers; +using ECommerce.Contracts.TerrenceLGee.Common.Extensions; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Repositories; + +public class CustomerRepository : ICustomerRepository +{ + private readonly ECommerceDbContext _context; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public CustomerRepository(ECommerceDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task GetCustomerProfileAsync(string? customerId) + { + try + { + var customer = await _context.Users + .FirstOrDefaultAsync(c => c.Id.Equals(customerId)); + + return customer; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CustomerRepository)}\n" + + $"Method: {nameof(GetCustomerProfileAsync)}\n" + + $"There was an unexpected error retrieving customer {customerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task> GetAllCustomersForAdminAsync(CustomerQueryParams customerQueryParams) + { + try + { + var customers = _context.Users + .Where(c => !string.IsNullOrEmpty(c.UserName) && !c.UserName.ToLower().Equals("admin@example.com")) + .AsNoTracking(); + + SetFilteringAndSorting(ref customers, customerQueryParams); + + return await customers.ToPagedListAsync(customers.Count(), customerQueryParams.Page, customerQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CustomerRepository)}\n" + + $"Method: {nameof(GetAllCustomersForAdminAsync)}\n" + + $"There was an unexpected error retrieving all customers: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + + private void SetFilteringAndSorting(ref IQueryable customers, CustomerQueryParams customerQueryParams) + { + if (customerQueryParams.MinSaleCount.HasValue && customerQueryParams.MaxSaleCount.HasValue) + { + if (customerQueryParams.IsValidSaleCountRange) + { + customers = customers.Where(c => c.Sales.Count >= customerQueryParams.MinSaleCount.Value && + c.Sales.Count <= customerQueryParams.MaxSaleCount.Value); + } + } + else if (customerQueryParams.MinSaleCount.HasValue && !customerQueryParams.MaxSaleCount.HasValue) + { + customers = customers.Where(c => c.Sales.Count >= customerQueryParams.MinSaleCount); + } + else if (customerQueryParams.MaxSaleCount.HasValue && !customerQueryParams.MinSaleCount.HasValue) + { + customers = customers.Where(c => c.Sales.Count <= customerQueryParams.MaxSaleCount.Value); + } + + if (customerQueryParams.MinTotalSpent.HasValue && customerQueryParams.MaxTotalSpent.HasValue) + { + if (customerQueryParams.IsValidTotalSpentRange) + { + customers = customers.Where(c => c.Sales.Sum(s => s.TotalAmount) >= customerQueryParams.MinTotalSpent.Value && + c.Sales.Sum(s => s.TotalAmount) <= customerQueryParams.MaxTotalSpent.Value); + } + } + else if (customerQueryParams.MinTotalSpent.HasValue && !customerQueryParams.MaxTotalSpent.HasValue) + { + customers = customers.Where(c => c.Sales.Sum(s => s.TotalAmount) >= customerQueryParams.MinTotalSpent.Value); + } + else if (customerQueryParams.MaxTotalSpent.HasValue && !customerQueryParams.MinTotalSpent.HasValue) + { + customers = customers.Where(c => c.Sales.Sum(s => s.TotalAmount) <= customerQueryParams.MaxTotalSpent.Value); + } + + customers = SortHelper.ApplySorting(customers, customerQueryParams.OrderBy); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/Helpers/SortHelper.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/Helpers/SortHelper.cs new file mode 100644 index 00000000..7e36a2a4 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/Helpers/SortHelper.cs @@ -0,0 +1,43 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using System.Text; + +namespace ECommerce.Api.TerrenceLGee.Repositories.Helpers; + +public static class SortHelper +{ + public static IQueryable ApplySorting(IQueryable entities, string? orderByQueryString) + { + if (!entities.Any()) return entities; + + if (string.IsNullOrWhiteSpace(orderByQueryString)) return entities; + + var orderParams = orderByQueryString.Trim().Split(','); + var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); + var orderQueryBuilder = new StringBuilder(); + + foreach (var param in orderParams) + { + if (string.IsNullOrWhiteSpace(param)) continue; + + var propertyFromQueryName = param.Split(" ")[0]; + var objectProperty = propertyInfos + .FirstOrDefault(pi => pi.Name.Equals(propertyFromQueryName, + StringComparison.InvariantCultureIgnoreCase)); + + if (objectProperty is null) continue; + + var sortingOrder = param.EndsWith(" desc") + ? "descending" + : "ascending"; + + orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {sortingOrder}, "); + } + + var orderQuery = orderQueryBuilder + .ToString() + .TrimEnd(',', ' '); + + return entities.OrderBy(orderQuery); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/ProductRepository.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/ProductRepository.cs new file mode 100644 index 00000000..2cf9d50e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/ProductRepository.cs @@ -0,0 +1,276 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Repositories.Helpers; +using ECommerce.Contracts.TerrenceLGee.Common.Extensions; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Repositories; + +public class ProductRepository : IProductRepository +{ + private readonly ECommerceDbContext _context; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public ProductRepository(ECommerceDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task AddProductAsync(Product product) + { + try + { + var productAlreadyExists = await _context.Products + .AnyAsync(p => p.Name.ToLower().Equals(product.Name.ToLower())); + + if (productAlreadyExists) return null; + + product.CreatedAt = DateTime.UtcNow; + + await _context.Products.AddAsync(product); + await _context.SaveChangesAsync(); + + return await _context.Products + .Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == product.Id); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(AddProductAsync)}\n" + + $"There was an unexpected error adding a new product to category {product.CategoryId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + + } + + public async Task UpdateProductAsync(Product product) + { + try + { + var productToUpdate = await _context.Products + .Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == product.Id); + + if (productToUpdate is null) return null; + + productToUpdate.Name = product.Name; + productToUpdate.Description = product.Description; + productToUpdate.StockQuantity = product.StockQuantity; + productToUpdate.DiscountPercentage = product.DiscountPercentage; + productToUpdate.ImageUrl = product.ImageUrl; + + if (productToUpdate.StockQuantity <= 0) + { + productToUpdate.IsInStock = false; + } + else + { + productToUpdate.IsInStock = true; + } + + productToUpdate.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return productToUpdate; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(UpdateProductAsync)}\n" + + $"There was an unexpected error updating product {product.Id} in category {product.CategoryId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task DeleteProductAsync(int productId) + { + try + { + var productToDelete = await _context.Products + .FirstOrDefaultAsync(p => p.Id == productId); + + if (productToDelete is null || productToDelete.IsDeleted) return false; + + productToDelete.IsDeleted = true; + productToDelete.IsInStock = false; + productToDelete.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(DeleteProductAsync)}\n" + + $"There was an unexpected error 'deleting' product: {productId}:" + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return false; + } + } + + public async Task RestoreProductAsync(int productId) + { + try + { + var productToRestore = await _context.Products + .FirstOrDefaultAsync(p => p.Id == productId && p.IsDeleted); + + if (productToRestore is null || !productToRestore.IsDeleted) return false; + + productToRestore.IsDeleted = false; + if (productToRestore.StockQuantity > 0) productToRestore.IsInStock = true; + productToRestore.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(RestoreProductAsync)}\n" + + $"There was an unexpected error 'restoring' product {productId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return false; + } + } + + public async Task GetProductAsync(int productId) + { + try + { + var product = await _context.Products + .Include(p => p.Category) + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == productId); + + return product; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(GetProductAsync)}\n" + + $"There was an unexpected error retrieving product {productId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task> GetProductsAsync(ProductQueryParams productQueryParams) + { + try + { + var products = _context.Products + .Include(p => p.Category) + .AsNoTracking(); + + SetFilteringAndSorting(ref products, productQueryParams); + + return await products.ToPagedListAsync(products.Count(), productQueryParams.Page, productQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductRepository)}\n" + + $"Method: {nameof(GetProductsAsync)}\n" + + $"There was an unexpected error retrieving all products: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + + private void SetFilteringAndSorting(ref IQueryable products, ProductQueryParams productQueryParams) + { + if (!string.IsNullOrEmpty(productQueryParams.CategoryName)) + { + products = products.Where(p => p.Category != null && + p.Category.Name.ToLower().Equals(productQueryParams.CategoryName.ToLower())); + } + + if (!string.IsNullOrEmpty(productQueryParams.Description)) + { + products = products.Where(p => !string.IsNullOrEmpty(p.Description) && + p.Description.ToLower().Contains(productQueryParams.Description.ToLower())); + } + + if (productQueryParams.MinUnitPrice.HasValue && productQueryParams.MaxUnitPrice.HasValue) + { + if (productQueryParams.IsValidUnitPriceRange) + { + products = products.Where(p => p.UnitPrice >= productQueryParams.MinUnitPrice && + p.UnitPrice <= productQueryParams.MaxUnitPrice); + } + } + else if (productQueryParams.MinUnitPrice.HasValue && !productQueryParams.MaxUnitPrice.HasValue) + { + products = products.Where(p => p.UnitPrice >= productQueryParams.MinUnitPrice); + } + else if (productQueryParams.MaxUnitPrice.HasValue && !productQueryParams.MinUnitPrice.HasValue) + { + products = products.Where(p => p.UnitPrice <= productQueryParams.MaxUnitPrice); + } + + if (productQueryParams.MinStockQuantity.HasValue && productQueryParams.MaxStockQuantity.HasValue) + { + if (productQueryParams.IsValidStockQuantityRange) + { + products = products.Where(p => p.StockQuantity >= productQueryParams.MinStockQuantity && + p.StockQuantity <= productQueryParams.MaxStockQuantity); + } + } + else if (productQueryParams.MinStockQuantity.HasValue && !productQueryParams.MaxStockQuantity.HasValue) + { + products = products.Where(p => p.StockQuantity >= productQueryParams.MinStockQuantity); + } + else if (productQueryParams.MaxStockQuantity.HasValue && !productQueryParams.MinStockQuantity.HasValue) + { + products = products.Where(p => p.StockQuantity <= productQueryParams.MaxStockQuantity); + } + + if (productQueryParams.MinDiscountPercentage.HasValue && productQueryParams.MaxDiscountPercentage.HasValue) + { + if (productQueryParams.IsValidDiscountPercentageRange) + { + products = products.Where(p => p.DiscountPercentage >= productQueryParams.MinDiscountPercentage && + p.DiscountPercentage <= productQueryParams.MaxDiscountPercentage); + } + } + else if (productQueryParams.MinDiscountPercentage.HasValue && !productQueryParams.MaxDiscountPercentage.HasValue) + { + products = products.Where(p => p.DiscountPercentage >= productQueryParams.MinDiscountPercentage); + } + else if (productQueryParams.MaxDiscountPercentage.HasValue && !productQueryParams.MinDiscountPercentage.HasValue) + { + products = products.Where(p => p.DiscountPercentage <= productQueryParams.MaxDiscountPercentage); + } + + if (productQueryParams.InStock.HasValue) + { + products = products.Where(p => p.IsInStock == productQueryParams.InStock); + } + + if (productQueryParams.IsDeleted.HasValue) + { + products = products.Where(p => p.IsDeleted == productQueryParams.IsDeleted); + } + + if (productQueryParams.CategoryId.HasValue) + { + products = products.Where(p => p.CategoryId == productQueryParams.CategoryId); + } + + products = SortHelper.ApplySorting(products, productQueryParams.OrderBy); + } + + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/SaleRepository.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/SaleRepository.cs new file mode 100644 index 00000000..1df9d78d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Repositories/SaleRepository.cs @@ -0,0 +1,293 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Repositories.Helpers; +using ECommerce.Contracts.TerrenceLGee.Common.Extensions; +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.Enums; +using ECommerce.Shared.TerrenceLGee.Parameters; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using Microsoft.EntityFrameworkCore; + +namespace ECommerce.Api.TerrenceLGee.Repositories; + +public class SaleRepository : ISaleRepository +{ + private readonly ECommerceDbContext _context; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public SaleRepository(ECommerceDbContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task AddSaleAsync(Sale sale) + { + try + { + var customer = await _context.Users.FirstOrDefaultAsync(c => c.Id.Equals(sale.CustomerId)); + + if (customer is null) return null; + + sale.CreatedAt = DateTime.UtcNow; + + await _context.Sales.AddAsync(sale); + await _context.SaveChangesAsync(); + + return await _context.Sales + .Include(s => s.Customer) + .Include(s => s.SaleProducts) + .ThenInclude(sp => sp.Product) + .FirstOrDefaultAsync(s => s.Id == sale.Id); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(AddSaleAsync)}\n" + + $"There was an unexpected error creating the sale for customer: {sale.CustomerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task GetSaleAsync(int saleId, string? customerId) + { + try + { + var sale = await _context.Sales + .AsNoTracking() + .Include(s => s.Customer) + .Include(s => s.SaleProducts) + .ThenInclude(sp => sp.Product) + .FirstOrDefaultAsync(s => s.Id == saleId && s.Customer != null && s.Customer.Id.Equals(customerId)); + + return sale; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(GetSaleAsync)}\n" + + $"There was an unexpected error retrieving sale {saleId} for customer {customerId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task GetSaleForAdminAsync(int saleId) + { + try + { + var sale = await _context.Sales + .AsNoTracking() + .Include(s => s.Customer) + .Include(s => s.SaleProducts) + .ThenInclude(sp => sp.Product) + .FirstOrDefaultAsync(s => s.Id == saleId); + + return sale; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(GetSaleForAdminAsync)}\n" + + $"There was an unexpected error retrieving sale {saleId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return null; + } + } + + public async Task<(bool, SaleStatus)> AdminUpdateSaleStatusAsync(int saleId, SaleStatus status) + { + try + { + var saleToUpdate = await _context.Sales + .FirstOrDefaultAsync(s => s.Id == saleId); + + if (saleToUpdate is null) return (false, SaleStatus.None); + + if (status == SaleStatus.Canceled) + { + var result = await RestockAsync(saleToUpdate.SaleProducts); + if (!result) return (false, SaleStatus.None); + } + + if (saleToUpdate.SaleStatus == status) return (false, saleToUpdate.SaleStatus); + if (saleToUpdate.SaleStatus == SaleStatus.Delivered || saleToUpdate.SaleStatus == SaleStatus.Canceled) return (false, saleToUpdate.SaleStatus); + if (saleToUpdate.SaleStatus == SaleStatus.Processing && status == SaleStatus.Pending) return (false, saleToUpdate.SaleStatus); + if (saleToUpdate.SaleStatus == SaleStatus.Shipped && status == SaleStatus.Processing) return (false, saleToUpdate.SaleStatus); + + + saleToUpdate.SaleStatus = status; + saleToUpdate.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return (true, saleToUpdate.SaleStatus); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(AdminUpdateSaleStatusAsync)}\n" + + $"There was an unexpected error updating the status of sale {saleId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return (false, SaleStatus.None); + } + } + + public async Task<(bool, SaleStatus)> CustomerCancelSaleAsync(int saleId, string? customerId) + { + try + { + var saleToCancel = await _context.Sales + .Include(s => s.SaleProducts) + .FirstOrDefaultAsync(s => s.Id == saleId && s.Customer != null && s.CustomerId.Equals(customerId)); + + if (saleToCancel is null) return (false, SaleStatus.None); + + if (saleToCancel.SaleStatus == SaleStatus.Shipped + || saleToCancel.SaleStatus == SaleStatus.Delivered + || saleToCancel.SaleStatus == SaleStatus.Canceled) return (false, saleToCancel.SaleStatus); + + var result = await RestockAsync(saleToCancel.SaleProducts); + + if (result) + { + saleToCancel.SaleStatus = SaleStatus.Canceled; + saleToCancel.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + + return (result, SaleStatus.None); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(CustomerCancelSaleAsync)}\n" + + $"There was an unexpected error cancelling sale {saleId} for customer {customerId}: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return (false, SaleStatus.None); + } + } + + public async Task> GetSalesAsync(SaleQueryParams saleQueryParams) + { + try + { + var sales = _context.Sales + .Where(s => s.CustomerId.Equals(saleQueryParams.CustomerId)) + .Include(s => s.Customer) + .Include(s => s.SaleProducts) + .AsNoTracking(); + + SetFilteringAndSorting(ref sales, saleQueryParams); + + return await sales.ToPagedListAsync(sales.Count(), saleQueryParams.Page, saleQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(GetSalesAsync)}\n" + + $"There was an unexpected error retrieving the sales for customer {saleQueryParams.CustomerId}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + + public async Task> GetAllSalesForAdminAsync(SaleQueryParams saleQueryParams) + { + try + { + var sales = _context.Sales + .Include(s => s.Customer) + .Include(s => s.SaleProducts) + .AsNoTracking(); + + SetFilteringAndSorting(ref sales, saleQueryParams); + + return await sales.ToPagedListAsync(sales.Count(), saleQueryParams.Page, saleQueryParams.PageSize); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleRepository)}\n" + + $"Method: {nameof(GetAllSalesForAdminAsync)}\n" + + $"There was an unexpected error retrieving all sales: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return []; + } + } + + private void SetFilteringAndSorting(ref IQueryable sales, SaleQueryParams saleQueryParams) + { + if (!string.IsNullOrEmpty(saleQueryParams.Status)) + { + sales = saleQueryParams.Status.ToLower() switch + { + "pending" => sales.Where(s => s.SaleStatus == SaleStatus.Pending), + "processing" => sales.Where(s => s.SaleStatus == SaleStatus.Processing), + "shipped" => sales.Where(s => s.SaleStatus == SaleStatus.Shipped), + "delivered" => sales.Where(s => s.SaleStatus == SaleStatus.Delivered), + "canceled" => sales.Where(s => s.SaleStatus == SaleStatus.Canceled), + _ => sales + }; + } + + if (saleQueryParams.MinTotalAmount.HasValue && saleQueryParams.MaxTotalAmount.HasValue) + { + if (saleQueryParams.IsValidAmountRange) + { + sales = sales.Where(s => s.TotalAmount >= saleQueryParams.MinTotalAmount && s.TotalAmount <= + saleQueryParams.MaxTotalAmount); + } + } + else if (saleQueryParams.MinTotalAmount.HasValue && !saleQueryParams.MaxTotalAmount.HasValue) + { + sales = sales.Where(s => s.TotalAmount >= saleQueryParams.MinTotalAmount); + } + else if (saleQueryParams.MaxTotalAmount.HasValue && !saleQueryParams.MinTotalAmount.HasValue) + { + sales = sales.Where(s => s.TotalAmount <= saleQueryParams.MaxTotalAmount); + } + + if (!string.IsNullOrEmpty(saleQueryParams.CustomerFirstName)) + { + sales = sales.Where(s => s.Customer != null && + s.Customer.FirstName.ToLower().Equals(saleQueryParams.CustomerFirstName.ToLower())); + } + + if (!string.IsNullOrEmpty(saleQueryParams.CustomerLastName)) + { + sales = sales.Where(s => s.Customer != null && + s.Customer.LastName.ToLower().Equals(saleQueryParams.CustomerLastName.ToLower())); + } + + if (!string.IsNullOrEmpty(saleQueryParams.CustomerId)) + { + sales = sales.Where(s => s.Customer != null && s.CustomerId.Equals(saleQueryParams.CustomerId)); + } + + sales = SortHelper.ApplySorting(sales, saleQueryParams.OrderBy); + } + + private async Task RestockAsync(IEnumerable saleProducts) + { + foreach (var saleProduct in saleProducts) + { + var product = await _context.Products + .FirstOrDefaultAsync(p => p.Id == saleProduct.ProductId); + + if (product is null) return false; + + var isProductPreviouslyOutOfStock = !product.IsInStock; + + product.StockQuantity += saleProduct.Quantity; + + if (isProductPreviouslyOutOfStock && product.StockQuantity > 0) product.IsInStock = true; + } + await _context.SaveChangesAsync(); + + return true; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponse.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponse.cs new file mode 100644 index 00000000..bcb712b0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponse.cs @@ -0,0 +1,28 @@ +namespace ECommerce.Api.TerrenceLGee.Responses; + +public class ApiResponse +{ + public int StatusCode { get; set; } + public bool IsSuccess { get; set; } + public T? Data { get; set; } + public List Errors { get; set; } = []; + + public ApiResponse() { } + + public ApiResponse(int statusCode, T? data) + { + StatusCode = statusCode; + IsSuccess = true; + Data = data; + Errors = []; + } + + public ApiResponse(int statusCode, List errors) + { + StatusCode = statusCode; + IsSuccess = false; + Errors = errors; + } + + public static ApiResponse GetEmptyResponse => new(); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponsePaged.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponsePaged.cs new file mode 100644 index 00000000..21079920 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Responses/ApiResponsePaged.cs @@ -0,0 +1,34 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; + +namespace ECommerce.Api.TerrenceLGee.Responses; + +public class ApiResponsePaged +{ + public int StatusCode { get; set; } + public bool IsSuccess { get; set; } + public PagedList Data { get; set; } = []; + public int PageNumber { get; set; } + public int TotalPages { get; set; } + public int TotalItemsRetrieved { get; set; } + public int TotalItems { get; set; } + public List Errors { get; set; } = []; + + public ApiResponsePaged(int statusCode, PagedList data) + { + StatusCode = statusCode; + IsSuccess = true; + Data = data; + PageNumber = data.PageNumber; + TotalPages = data.TotalPages; + TotalItemsRetrieved = data.Count; + TotalItems = data.TotalEntities; + Errors = []; + } + + public ApiResponsePaged(int statusCode, List errors) + { + StatusCode = statusCode; + IsSuccess = false; + Errors = errors; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AddressService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AddressService.cs new file mode 100644 index 00000000..97c475e5 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AddressService.cs @@ -0,0 +1,89 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.AddressMappings; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class AddressService : IAddressService +{ + private readonly IAddressRepository _addressRepository; + + public AddressService(IAddressRepository addressRepository) + { + _addressRepository = addressRepository; + } + + public async Task> AddAddressAsync(CreateAddressDto address) + { + var addedAddress = await _addressRepository.AddAddressAsync(address.FromCreateAddressDto()); + + if (addedAddress is null) + { + return Result.Fail("Unable to add address at this time.", ErrorType.BadRequest); + } + + return Result.Ok(addedAddress.ToRetrievedAddressDto()); + } + + public async Task> UpdateAddressAsync(UpdateAddressDto address) + { + var updatedAddress = await _addressRepository.UpdateAddressAsync(address.FromUpdateAddressDto()); + + if (updatedAddress is null) + { + return Result.Fail($"Unable to update address {address.Id}", ErrorType.BadRequest); + } + + return Result.Ok(updatedAddress.ToRetrievedAddressDto()); + } + + public async Task DeleteAddressAsync(AddressIdDto addressIdDto) + { + var deletion = await _addressRepository.DeleteAddressAsync(addressIdDto.Id, addressIdDto.CustomerId); + + if (!deletion) + { + return Result.Fail($"Unable to delete address {addressIdDto.Id}", ErrorType.BadRequest); + } + + return Result.Ok(); + } + + public async Task> GetAddressAsync(AddressIdDto addressIdDto) + { + var address = await _addressRepository.GetAddressAsync(addressIdDto.Id, addressIdDto.CustomerId); + + if (address is null) + { + return Result.Fail($"Unable to retrieve address {addressIdDto.Id}", ErrorType.NotFound); + } + + return Result.Ok(address.ToRetrievedAddressDto()); + } + + public async Task>> GetCustomerAddressesAsync(AddressQueryParams addressQueryParams) + { + var addresses = await _addressRepository.GetCustomerAddressesAsync(addressQueryParams); + + return Result>.Ok(new PagedList( + addresses.Select(a => a.ToRetrievedAddressDto()), + addresses.TotalEntities, + addressQueryParams.Page, + addressQueryParams.PageSize)); + } + + public async Task>> GetAllCustomerAddressesForAdminAsync(AddressQueryParams addressQueryParams) + { + var addresses = await _addressRepository.GetAllCustomerAddressesForAdminAsync(addressQueryParams); + + return Result>.Ok(new PagedList( + addresses.Select(a => a.ToRetrievedAddressDto()), + addresses.TotalEntities, + addressQueryParams.Page, + addressQueryParams.PageSize)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AuthService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AuthService.cs new file mode 100644 index 00000000..a1a4e06a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/AuthService.cs @@ -0,0 +1,271 @@ +using ECommerce.Api.TerrenceLGee.Data; +using ECommerce.Api.TerrenceLGee.Data.Configuration; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.AddressMappings; +using ECommerce.Entities.TerrenceLGee.Models; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class AuthService : IAuthService +{ + private readonly IOptions _authOptions; + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly SignInManager _signInManager; + private readonly ECommerceDbContext _context; + private readonly AuthConfiguration _authConfiguration; + private readonly ILogger _logger; + private string _errorMessage = string.Empty; + + public AuthService( + IOptions authOptions, + UserManager userManager, + RoleManager roleManager, + SignInManager signInManager, + ECommerceDbContext context, + ILogger logger) + { + _authOptions = authOptions; + _authConfiguration = _authOptions.Value; + _userManager = userManager; + _roleManager = roleManager; + _signInManager = signInManager; + _context = context; + _logger = logger; + } + + public async Task RegisterUserAsync(UserRegistrationDto user) + { + try + { + var existingUser = await _userManager + .FindByEmailAsync(user.Email); + + if (existingUser is not null) + { + return Result.Fail("User already exists. Unable to register a previously registered user.", ErrorType.Conflict); + } + + var newUser = new ApplicationUser + { + FirstName = user.FirstName, + LastName = user.LastName, + DateOfBirth = user.DateOfBirth, + RegistrationDate = new DateOnly(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day), + Email = user.Email, + UserName = user.Email, + Addresses = new List
+ { + (user.BillingAddress is not null) ? user.BillingAddress.FromCreateAddressDto() : new Address(), + (user.ShippingAddress is not null) ? user.ShippingAddress.FromCreateAddressDto() : new Address() + } + }; + + var result = await _userManager.CreateAsync(newUser, user.Password); + + if (!result.Succeeded) + { + return Result.Fail($"Unable to register new user at this time. Please check your input and try again later.", ErrorType.BadRequest); + } + + result = await _userManager.AddToRoleAsync(newUser, "customer"); + + if (!result.Succeeded) + { + return Result.Fail("Unable to add user as a customer. Please try again later", ErrorType.BadRequest); + } + + return Result.Ok(); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {RegisterUserAsync}\n" + + $"An unexpected error occurred while registering the new user: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return Result.Fail($"An unexpected error occurred while registering the new user", ErrorType.InternalServerError); + } + } + + public async Task> LoginUserAsync(UserLoginDto userDto) + { + try + { + var user = await _userManager.FindByEmailAsync(userDto.Email); + + if (user is null) + { + return Result.Fail("User not found", ErrorType.NotFound); + } + + var result = await _signInManager.CheckPasswordSignInAsync(user, userDto.Password, false); + + if (!result.Succeeded) + { + return Result.Fail("Login failed", ErrorType.BadRequest); + } + + var roles = await _userManager.GetRolesAsync(user); + var userRole = roles.FirstOrDefault() ?? "customer"; + + var role = await _roleManager.FindByNameAsync(userRole); + var roleClaims = (role is not null) + ? await _roleManager.GetClaimsAsync(role) + : []; + + var jwtId = string.Empty; + + var jwtToken = GenerateJwtToken(user, userRole, roleClaims, out jwtId); + var refreshToken = GenerateRefreshToken(jwtId, user.Id); + + await _context.RefreshTokens.AddAsync(refreshToken); + await _context.SaveChangesAsync(); + + var response = new AuthenticationResponseDto + { + AccessToken = jwtToken, + RefreshToken = refreshToken.Token, + Roles = roles.ToList() + }; + + return Result.Ok(response); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {LoginUserAsync}\n" + + $"An unexpected error occurred during user login: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return Result.Fail("An unexpected error occurred during user login", ErrorType.InternalServerError); + } + } + + public async Task ResetPasswordAsync(UserResetPasswordDto userDto) + { + try + { + var user = await _context.Users + .FirstOrDefaultAsync(u => !string.IsNullOrEmpty(u.Email) && u.Email.ToLower().Equals(userDto.Email.ToLower())); + + if (user is null) + { + return Result.Fail("User not found", ErrorType.NotFound); + } + + var result = await _userManager.ChangePasswordAsync(user, userDto.OldPassword, userDto.NewPassword); + + if (!result.Succeeded) + { + return Result.Fail($"Unable to change password for {user.FirstName} {user.LastName}", ErrorType.BadRequest); + } + + return Result.Ok(); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(ResetPasswordAsync)}\n" + + $"There was an unexpected error attempting to change the password for user {userDto.Email}: " + + $"{ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return Result.Fail("Unexpected error occurred while changing your password", ErrorType.InternalServerError); + } + } + + public async Task LogoutUserAsync(UserLogoutDto userDto) + { + try + { + var refreshToken = await _context.RefreshTokens + .FirstOrDefaultAsync(rt => rt.UserId.Equals(userDto.UserId) && !rt.IsRevoked); + + if (refreshToken is null) + { + return Result.Fail("Unable to proceed with logout. Invalid authorization", ErrorType.Unauthorized); + } + + refreshToken.IsRevoked = true; + refreshToken.RevokedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + return Result.Ok(); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {LogoutUserAsync}\n" + + $"An unexpected error occurred during user logout: {ex.Message}"; + _logger.LogError(ex, "{msg}\n\n", _errorMessage); + return Result.Fail("Unexpected error occurred during user logout.", ErrorType.InternalServerError); + } + + } + + private string GenerateJwtToken( + ApplicationUser user, + string userRole, + IList roleClaims, + out string jwtId) + { + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authConfiguration.Key)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + jwtId = Guid.NewGuid().ToString(); + + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id), + new Claim(JwtRegisteredClaimNames.Jti, jwtId), + new Claim(JwtRegisteredClaimNames.Email, user.Email!), + new Claim(JwtRegisteredClaimNames.Iss, _authConfiguration.Issuer), + new Claim(JwtRegisteredClaimNames.Aud, _authConfiguration.Audience), + new Claim("role", userRole) + }; + + foreach (var roleClaim in roleClaims) + { + claims.Add(new Claim(roleClaim.Type, roleClaim.Value)); + } + + var token = new JwtSecurityToken( + issuer: _authConfiguration.Issuer, + audience: _authConfiguration.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(30), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + private RefreshToken GenerateRefreshToken(string jwtId, string userId) + { + var randomBytes = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomBytes); + + var refreshTokenExpirationDays = _authConfiguration.RefreshTokenExpirationDays; + + return new RefreshToken + { + Token = Convert.ToBase64String(randomBytes), + JwtId = jwtId, + Expires = DateTime.UtcNow.AddDays(refreshTokenExpirationDays), + UserId = userId, + CreatedAt = DateTime.UtcNow, + IsRevoked = false, + RevokedAt = null + }; + } + + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CategoryService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CategoryService.cs new file mode 100644 index 00000000..7e78a1de --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CategoryService.cs @@ -0,0 +1,93 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.CategoryMappings; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class CategoryService : ICategoryService +{ + private readonly ICategoryRepository _categoryRepository; + + public CategoryService(ICategoryRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + public async Task> AddCategoryAsync(CreateCategoryDto category) + { + var addedCategory = await _categoryRepository.AddCategoryAsync(category.FromCreateCategoryDto()); + + if (addedCategory is null) + { + return Result.Fail("Unable to add new category", ErrorType.BadRequest); + } + + return Result.Ok(addedCategory.ToRetrievedCategoryForAdminDto()); + } + + public async Task> UpdateCategoryAsync(UpdateCategoryDto category) + { + var updatedCategory = await _categoryRepository.UpdateCategoryAsync(category.FromUpdateCategoryDto()); + + if (updatedCategory is null) + { + return Result.Fail($"Unable to update category {category.Id}", ErrorType.BadRequest); + } + + return Result.Ok(updatedCategory.ToRetrievedCategoryForAdminDto()); + } + + public async Task> GetCategoryAsync(CategoryParams categoryParams) + { + var category = await _categoryRepository.GetCategoryAsync(categoryParams.CategoryId); + + if (category is null) + { + return Result.Fail($"Unable to retrieve category {categoryParams.CategoryId}", ErrorType.NotFound); + } + + return Result.Ok(category.ToRetrievedCategoryDto()); + } + + public async Task> GetCategoryForAdminAsync(CategoryParams categoryParams) + { + var category = await _categoryRepository.GetCategoryAsync(categoryParams.CategoryId); + + if (category is null) + { + return Result.Fail($"Unable to retrieve category {categoryParams.CategoryId}", ErrorType.NotFound); + } + + return Result.Ok(category.ToRetrievedCategoryForAdminDto()); + } + + public async Task>> GetCategoriesAsync(CategoryQueryParams categoryQueryParams) + { + var categories = await _categoryRepository.GetCategoriesAsync(categoryQueryParams); + + var pagedCategories = new PagedList( + categories.Select(c => c.ToRetrievedCategorySummaryDto()), + categories.TotalEntities, + categoryQueryParams.Page, + categoryQueryParams.PageSize); + + return Result>.Ok(pagedCategories); + } + + public async Task>> GetCategoriesForAdminAsync(CategoryQueryParams categoryQueryParams) + { + var categories = await _categoryRepository.GetCategoriesAsync(categoryQueryParams); + + var pagedCategories = new PagedList( + categories.Select(c => c.ToRetrievedCategorySummaryForAdminDto()), + categories.TotalEntities, + categoryQueryParams.Page, + categoryQueryParams.PageSize); + + return Result>.Ok(pagedCategories); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CustomerService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CustomerService.cs new file mode 100644 index 00000000..26092767 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/CustomerService.cs @@ -0,0 +1,42 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.CustomerMappings; +using ECommerce.Shared.TerrenceLGee.DTOs.CustomerDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class CustomerService : ICustomerService +{ + private readonly ICustomerRepository _customerRepository; + + public CustomerService(ICustomerRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task> GetCustomerProfileAsync(CustomerRetrievalDto customerRetrieval) + { + var customer = await _customerRepository.GetCustomerProfileAsync(customerRetrieval.CustomerId); + + if (customer is null) + { + return Result.Fail("Unable to retrieve customer profile", ErrorType.NotFound); + } + + return Result.Ok(customer.ToRetrievedCustomerDto()); + } + + public async Task>> GetAllCustomersForAdminAsync(CustomerQueryParams customerQueryParams) + { + var customers = await _customerRepository.GetAllCustomersForAdminAsync(customerQueryParams); + + return Result>.Ok(new PagedList( + customers.Select(c => c.ToRetrievedCustomerDto()), + customers.TotalEntities, + customerQueryParams.Page, + customerQueryParams.PageSize)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/ProductService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/ProductService.cs new file mode 100644 index 00000000..3a836b38 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/ProductService.cs @@ -0,0 +1,114 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.ProductMappings; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class ProductService : IProductService +{ + private readonly IProductRepository _productRepository; + + public ProductService(IProductRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task> AddProductAsync(CreateProductDto product) + { + var addedProduct = await _productRepository.AddProductAsync(product.FromCreateProductDto()); + + if (addedProduct is null) + { + return Result.Fail("Unable to add new product at this time.", ErrorType.BadRequest); + } + + return Result.Ok(addedProduct.ToRetrievedProductForAdminDto()); + } + + public async Task> UpdateProductAsync(UpdateProductDto product) + { + var updatedProduct = await _productRepository.UpdateProductAsync(product.FromUpdateProductDto()); + + if (updatedProduct is null) + { + return Result.Fail($"Unable to update product {product.Id}.", ErrorType.BadRequest); + } + + return Result.Ok(updatedProduct.ToRetrievedProductForAdminDto()); + } + + public async Task DeleteProductAsync(ProductParams productParams) + { + var deleted = await _productRepository.DeleteProductAsync(productParams.ProductId); + + if (!deleted) + { + return Result.Fail($"Deletion of product {productParams.ProductId} failed.", ErrorType.NotFound); + } + + return Result.Ok(); + } + + public async Task RestoreProductAsync(ProductParams productParams) + { + var restored = await _productRepository.RestoreProductAsync(productParams.ProductId); + + if (!restored) + { + return Result.Fail($"Restoration of product {productParams.ProductId} failed", ErrorType.NotFound); + } + + return Result.Ok(); + } + + public async Task> GetProductAsync(ProductParams productParams) + { + var product = await _productRepository.GetProductAsync(productParams.ProductId); + + if (product is null) + { + return Result.Fail($"Unable to retrieve product {productParams.ProductId}", ErrorType.NotFound); + } + + return Result.Ok(product.ToRetrievedProductDto()); + } + + public async Task> GetProductForAdminAsync(ProductParams productParams) + { + var product = await _productRepository.GetProductAsync(productParams.ProductId); + + if (product is null) + { + return Result.Fail($"Unable to retrieve product {productParams.ProductId}", ErrorType.NotFound); + } + + return Result.Ok(product.ToRetrievedProductForAdminDto()); + } + + public async Task>> GetProductsAsync(ProductQueryParams productQueryParams) + { + var products = await _productRepository.GetProductsAsync(productQueryParams); + + return Result>.Ok(new PagedList( + products.Select(p => p.ToRetrievedProductDto()), + products.TotalEntities, + productQueryParams.Page, + productQueryParams.PageSize)); + } + + + public async Task>> GetProductsForAdminAsync(ProductQueryParams productQueryParams) + { + var products = await _productRepository.GetProductsAsync(productQueryParams); + + return Result>.Ok(new PagedList( + products.Select(p => p.ToRetrievedProductForAdminDto()), + products.TotalEntities, + productQueryParams.Page, + productQueryParams.PageSize)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/SaleService.cs b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/SaleService.cs new file mode 100644 index 00000000..2be4ca9d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/Services/SaleService.cs @@ -0,0 +1,172 @@ +using ECommerce.Contracts.TerrenceLGee.Common.Pagination; +using ECommerce.Contracts.TerrenceLGee.Common.Results; +using ECommerce.Contracts.TerrenceLGee.Interfaces.RepositoryInterfaces; +using ECommerce.Contracts.TerrenceLGee.Interfaces.ServiceInterfaces; +using ECommerce.Contracts.TerrenceLGee.Mappings.SaleMappings; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleProductDTOs; +using ECommerce.Shared.TerrenceLGee.Enums; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; + +namespace ECommerce.Api.TerrenceLGee.Services; + +public class SaleService : ISaleService +{ + private readonly ISaleRepository _saleRepository; + private readonly IProductRepository _productRepository; + + public SaleService(ISaleRepository saleRepository, IProductRepository productRepository) + { + _saleRepository = saleRepository; + _productRepository = productRepository; + } + + public async Task> AddSaleAsync(CreateOrderDto order) + { + if (order.ShoppingCart.Count == 0) + { + return Result.Fail("Your shopping cart must include at least one item in order to make a sale.", ErrorType.BadRequest); + } + + var totalBaseAmount = 0.0m; + var totalDiscountAmount = 0.0m; + var totalAmount = 0.0m; + + var saleProducts = new List(); + + foreach (var item in order.ShoppingCart) + { + var product = await _productRepository.GetProductAsync(item.ProductId); + + if (product is null) + { + return Result.Fail($"Product {item.ProductId} not found.", ErrorType.NotFound); + } + + if (!product.IsInStock) + { + return Result.Fail($"{product.Name} is not in stock.", ErrorType.NotFound); + } + + if (item.Quantity > product.StockQuantity) + { + return Result.Fail($"You ordered {item.Quantity} of {product.Name}, but currently " + + $"there are only {product.StockQuantity} of this product in stock.", ErrorType.BadRequest); + } + + var baseAmount = item.Quantity * product.UnitPrice; + var discountAmount = (product.DiscountPercentage / 100.0m) * product.UnitPrice; + var totalPriceForItem = baseAmount - discountAmount; + + totalBaseAmount += baseAmount; + totalDiscountAmount += discountAmount; + + product.StockQuantity -= item.Quantity; + await _productRepository.UpdateProductAsync(product); + + saleProducts.Add(new CreateSaleProductDto + { + ProductId = product.Id, + Quantity = item.Quantity, + Price = product.UnitPrice, + Discount = discountAmount, + TotalPrice = totalPriceForItem + }); + } + + totalAmount = totalBaseAmount - totalDiscountAmount; + + var newSale = new CreateSaleDto + { + CustomerId = order.CustomerId, + TotalBaseAmount = totalBaseAmount, + TotalDiscountAmount = totalDiscountAmount, + TotalAmount = totalAmount, + SaleStatus = SaleStatus.Pending, + SaleProducts = saleProducts + }; + + var sale = await _saleRepository.AddSaleAsync(newSale.FromCreateSaleDto()); + + if (sale is null) + { + return Result.Fail($"Unable to complete sale at this time due to unexpected error.", ErrorType.InternalServerError); + } + + return Result.Ok(sale.ToRetrievedSaleDto()); + } + + public async Task> GetSaleAsync(RequestSaleDto request) + { + var sale = await _saleRepository.GetSaleAsync(request.SaleId, request.CustomerId); + + if (sale is null) + { + return Result.Fail($"Unable to retrieve sale {request.SaleId}.", ErrorType.NotFound); + } + + return Result.Ok(sale.ToRetrievedSaleDto()); + } + + public async Task> GetSaleForAdminAsync(RequestSaleDto request) + { + var sale = await _saleRepository.GetSaleForAdminAsync(request.SaleId); + + if (sale is null) + { + return Result.Fail($"Unable to retrieve sale {request.SaleId}.", ErrorType.NotFound); + } + + return Result.Ok(sale.ToRetrievedSaleDto()); + } + + public async Task>> GetSalesAsync(SaleQueryParams saleQueryParams) + { + var sales = await _saleRepository.GetSalesAsync(saleQueryParams); + + return Result>.Ok(new PagedList( + sales.Select(s => s.ToRetrievedSaleSummaryDto()), + sales.TotalEntities, + saleQueryParams.Page, + saleQueryParams.PageSize)); + } + + public async Task>> GetAllSalesForAdminAsync(SaleQueryParams saleQueryParams) + { + var sales = await _saleRepository.GetAllSalesForAdminAsync(saleQueryParams); + + return Result>.Ok(new PagedList( + sales.Select(s => s.ToRetrievedSaleSummaryDto()), + sales.TotalEntities, + saleQueryParams.Page, + saleQueryParams.PageSize)); + } + + public async Task AdminUpdateSaleStatusAsync(UpdateSaleStatusDto sale) + { + var (saleStatusUpdated, status) = await _saleRepository.AdminUpdateSaleStatusAsync(sale.SaleId, sale.Status); + + if (!saleStatusUpdated) + { + return Result.Fail($"Sale status update failed.\n" + + $"You are trying to update the status from {status} to {sale.Status} " + + $"which is not allowed", ErrorType.BadRequest); + } + + return Result.Ok(); + } + + public async Task CustomerCancelSaleAsync(CancelSaleDto cancel) + { + var (saleCanceled, status) = await _saleRepository.CustomerCancelSaleAsync(cancel.SaleId, cancel.CustomerId); + + if (!saleCanceled) + { + return Result.Fail($"Sale cancellation failed because this sale's status is " + + $"{status} and not eligible to canceled", ErrorType.BadRequest); + } + + return Result.Ok(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/appsettings.json b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/appsettings.json new file mode 100644 index 00000000..6bc4ea00 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.Api.TerrenceLGee/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "JwtConfiguration": { + "Key": "GH+NIF3ilghmkVlBFjF64qtj2I9BHALytS2CsMlfNbykTqY+U57kFPoBaXtO9nAlBNUUhJuJc7unWyeh2aN1stoRVdsi97XSSjqWJnh0fyXmOn/xiWq3ScxK1twZ+SO0pqte", + "Issuer": "ECommerceAPI", + "Audience": "ECommerceClient", + "RefreshTokenExpirationDays": 7 + } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml new file mode 100644 index 00000000..502186e0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml.cs new file mode 100644 index 00000000..deea2160 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/App.axaml.cs @@ -0,0 +1,212 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Services; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Handlers; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; +using ECommerce.AvaloniaClient.TerrenceLGee.Views; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using System; +using System.IO; +using System.Linq; + +namespace ECommerce.AvaloniaClient.TerrenceLGee; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + var services = new ServiceCollection(); + + LoggingSetup(); + + services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + services.AddSingleton(); + services.AddTransient(); + + services.AddHttpClient("client", c => + { + c.BaseAddress = new Uri(Urls.BaseUrl); + c.DefaultRequestHeaders.Accept.Add(new("application/json")); + }) + .AddHttpMessageHandler(); + + services.AddSingleton(WeakReferenceMessenger.Default); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddTransient(); + + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + DisableAvaloniaDataAnnotationValidation(); + + var mainWindowViewModel = serviceProvider + .GetRequiredService(); + + ShowWelcomeView(); + + void ShowWelcomeView() + { + var welcomePageViewModel = serviceProvider.GetRequiredService(); + + welcomePageViewModel.LoginRequested += ShowLoginView; + welcomePageViewModel.RegistrationRequested += ShowRegistrationView; + welcomePageViewModel.PasswordResetRequested += ShowPasswordResetView; + + mainWindowViewModel.CurrentView = welcomePageViewModel; + } + + void ShowLoginView() + { + var loginViewModel = serviceProvider.GetRequiredService(); + loginViewModel.LoginSuccessful += OnLoginSuccessful; + loginViewModel.BackRequested += ShowWelcomeView; + mainWindowViewModel.CurrentView = loginViewModel; + } + + void ShowRegistrationView() + { + var registrationViewModel = serviceProvider.GetRequiredService(); + registrationViewModel.RegistrationSuccessful += ShowLoginView; + registrationViewModel.BackRequested += ShowWelcomeView; + mainWindowViewModel.CurrentView = registrationViewModel; + } + + void ShowPasswordResetView() + { + var passwordResetViewModel = serviceProvider.GetRequiredService(); + passwordResetViewModel.BackRequested += ShowWelcomeView; + passwordResetViewModel.LoginRequested += ShowLoginView; + mainWindowViewModel.CurrentView = passwordResetViewModel; + } + + void OnLoginSuccessful(bool isAdmin) + { + var authService = serviceProvider.GetRequiredService(); + var messenger = serviceProvider.GetRequiredService(); + var mainUserViewModel = new MainUserViewModel(isAdmin, serviceProvider, authService, messenger); + mainUserViewModel.LogoutRequested += OnLogoutRequested; + + mainWindowViewModel.CurrentView = mainUserViewModel; + } + + void OnLogoutRequested() + { + ShowWelcomeView(); + } + + desktop.MainWindow = new MainWindow + { + DataContext = mainWindowViewModel + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } + + void LoggingSetup() + { + var loggingDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Logs"); + Directory.CreateDirectory(loggingDirectory); + var filePath = Path.Combine(loggingDirectory, "app-.txt"); + var outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.File( + path: filePath, + rollingInterval: RollingInterval.Day, + outputTemplate: outputTemplate) + .CreateLogger(); + } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/avalonia-logo.ico b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/avalonia-logo.ico new file mode 100644 index 00000000..f7da8bb5 Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/avalonia-logo.ico differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/categories.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/categories.jpg new file mode 100644 index 00000000..a8433016 Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/categories.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/customers.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/customers.jpg new file mode 100644 index 00000000..3c2c4bfe Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/customers.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/ecommerce.ico b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/ecommerce.ico new file mode 100644 index 00000000..ffb9851a Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/ecommerce.ico differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/home.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/home.jpg new file mode 100644 index 00000000..56ce5fc6 Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/home.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/login.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/login.jpg new file mode 100644 index 00000000..3ae38e1c Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/login.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/products.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/products.jpg new file mode 100644 index 00000000..5a8b696c Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/products.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/register.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/register.jpg new file mode 100644 index 00000000..888cf16b Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/register.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/reset.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/reset.jpg new file mode 100644 index 00000000..f52f65fd Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/reset.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/welcome.jpg b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/welcome.jpg new file mode 100644 index 00000000..e34c337e Binary files /dev/null and b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Assets/welcome.jpg differ diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressData.cs new file mode 100644 index 00000000..7efd464f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressData.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +public class AddressData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("customerId")] + public string? CustomerId { get; set; } + + [JsonPropertyName("customerName")] + public string? CustomerName { get; set; } + + [JsonPropertyName("addressLine1")] + public string AddressLine1 { get; set; } = string.Empty; + + [JsonPropertyName("addressLine2")] + public string? AddressLine2 { get; set; } + + [JsonPropertyName("city")] + public string City { get; set; } = string.Empty; + + [JsonPropertyName("state")] + public string State { get; set; } = string.Empty; + + [JsonPropertyName("postalCode")] + public string PostalCode { get; set; } = string.Empty; + + [JsonPropertyName("country")] + public string Country { get; set; } = string.Empty; + + [JsonPropertyName("isBillingAddress")] + public bool IsBillingAddress { get; set; } + + [JsonPropertyName("isShippingAddress")] + public bool IsShippingAddress { get; set; } + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressDeletionRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressDeletionRoot.cs new file mode 100644 index 00000000..f24a0450 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressDeletionRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +public class AddressDeletionRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressRoot.cs new file mode 100644 index 00000000..0dde7f60 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +public class AddressRoot : Root +{ + [JsonPropertyName("data")] + public AddressData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressesRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressesRoot.cs new file mode 100644 index 00000000..4e2018be --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Address/AddressesRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +public class AddressesRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthData.cs new file mode 100644 index 00000000..3891beac --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthData.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; + +public class AuthData +{ + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } = string.Empty; + + [JsonPropertyName("refreshToken")] + public string RefreshToken { get; set; } = string.Empty; + + [JsonPropertyName("roles")] + public List Roles { get; set; } = []; + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthRoot.cs new file mode 100644 index 00000000..9112ec77 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/AuthRoot.cs @@ -0,0 +1,6 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; + +public class AuthRoot : Root +{ + public AuthData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/LogoutRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/LogoutRoot.cs new file mode 100644 index 00000000..6bb0e230 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/LogoutRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; + +public class LogoutRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/PasswordResetRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/PasswordResetRoot.cs new file mode 100644 index 00000000..ac297e97 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/PasswordResetRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; + +public class PasswordResetRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/RegistrationRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/RegistrationRoot.cs new file mode 100644 index 00000000..b19b727b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Auth/RegistrationRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; + +public class RegistrationRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesAdminRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesAdminRoot.cs new file mode 100644 index 00000000..d0baf128 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesAdminRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoriesAdminRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesRoot.cs new file mode 100644 index 00000000..36c25753 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoriesRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoriesRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminData.cs new file mode 100644 index 00000000..a5665a0d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminData.cs @@ -0,0 +1,26 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoryAdminData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminRoot.cs new file mode 100644 index 00000000..b0180f99 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoryAdminRoot : Root +{ + [JsonPropertyName("data")] + public CategoryAdminData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminSummaryData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminSummaryData.cs new file mode 100644 index 00000000..b8839bd0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryAdminSummaryData.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoryAdminSummaryData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryData.cs new file mode 100644 index 00000000..ed5f0338 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryData.cs @@ -0,0 +1,18 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoryData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryRoot.cs new file mode 100644 index 00000000..a634d57f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategoryRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategoryRoot : Root +{ + [JsonPropertyName("data")] + public CategoryData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategorySummaryData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategorySummaryData.cs new file mode 100644 index 00000000..db6dcf8c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Category/CategorySummaryData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +public class CategorySummaryData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerData.cs new file mode 100644 index 00000000..99c239ec --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerData.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +public class CustomerData +{ + [JsonPropertyName("customerId")] + public string CustomerId { get; set; } = string.Empty; + + [JsonPropertyName("firstName")] + public string FirstName { get; set; } = string.Empty; + + [JsonPropertyName("lastName")] + public string LastName { get; set; } = string.Empty; + + [JsonPropertyName("emailAddress")] + public string EmailAddress { get; set; } = string.Empty; + + [JsonPropertyName("dateOfBirth")] + public DateOnly DateOfBirth { get; set; } + + [JsonPropertyName("registrationDate")] + public string RegistrationDate { get; set; } = string.Empty; + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerRoot.cs new file mode 100644 index 00000000..b712c59d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomerRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +public class CustomerRoot : Root +{ + [JsonPropertyName("data")] + public CustomerData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomersAdminRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomersAdminRoot.cs new file mode 100644 index 00000000..a5a08cbe --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Customer/CustomersAdminRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +public class CustomersAdminRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminData.cs new file mode 100644 index 00000000..668f903a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminData.cs @@ -0,0 +1,48 @@ +using System; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductAdminData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("categoryId")] + public int CategoryId { get; set; } + + [JsonPropertyName("categoryName")] + public string CategoryName { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("stockQuantity")] + public int StockQuantity { get; set; } + + [JsonPropertyName("unitPrice")] + public decimal UnitPrice { get; set; } + + [JsonPropertyName("discountPercentage")] + public int DiscountPercentage { get; set; } + + [JsonPropertyName("isDeleted")] + public bool IsDeleted { get; set; } + + [JsonPropertyName("isInStock")] + public bool IsInStock { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("imageUrl")] + public string? ImageUrl { get; set; } + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminRoot.cs new file mode 100644 index 00000000..17509c06 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductAdminRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductAdminRoot : Root +{ + [JsonPropertyName("data")] + public ProductAdminData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductData.cs new file mode 100644 index 00000000..28f83d3e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductData.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("categoryName")] + public string CategoryName { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("stockQuantity")] + public int StockQuantity { get; set; } + + [JsonPropertyName("unitPrice")] + public decimal UnitPrice { get; set; } + + [JsonPropertyName("discountPercentage")] + public int DiscountPercentage { get; set; } + + [JsonPropertyName("isInStock")] + public bool IsInStock { get; set; } + + [JsonPropertyName("imageUrl")] + public string? ImageUrl { get; set; } + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductDeletionRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductDeletionRoot.cs new file mode 100644 index 00000000..29ee1932 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductDeletionRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductDeletionRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRestorationRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRestorationRoot.cs new file mode 100644 index 00000000..be9edde3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRestorationRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductRestorationRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRoot.cs new file mode 100644 index 00000000..c8f461ac --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductRoot : Root +{ + [JsonPropertyName("data")] + public ProductData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsAdminRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsAdminRoot.cs new file mode 100644 index 00000000..d333e71a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsAdminRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductsAdminRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsRoot.cs new file mode 100644 index 00000000..10af20ec --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Product/ProductsRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +public class ProductsRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Root.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Root.cs new file mode 100644 index 00000000..7183a40d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Root.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models; + +public abstract class Root +{ + [JsonPropertyName("statusCode")] + public int StatusCode { get; set; } + + [JsonPropertyName("isSuccess")] + public bool IsSuccess { get; set; } + + [JsonPropertyName("errors")] + public List Errors { get; set; } = []; + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleAdminUpdateRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleAdminUpdateRoot.cs new file mode 100644 index 00000000..497726f8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleAdminUpdateRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleAdminUpdateRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleCustomerCancelRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleCustomerCancelRoot.cs new file mode 100644 index 00000000..d7ba8441 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleCustomerCancelRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleCustomerCancelRoot : Root +{ + [JsonPropertyName("data")] + public string? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleData.cs new file mode 100644 index 00000000..6fbaeb28 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleData.cs @@ -0,0 +1,41 @@ +using ECommerce.Shared.TerrenceLGee.Enums; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("customerId")] + public string CustomerId { get; set; } = string.Empty; + + [JsonPropertyName("customerName")] + public string CustomerName { get; set; } = string.Empty; + + [JsonPropertyName("totalBaseAmount")] + public double TotalBaseAmount { get; set; } + + [JsonPropertyName("totalDiscountAmount")] + public double TotalDiscountAmount { get; set; } + + [JsonPropertyName("totalAmount")] + public double TotalAmount { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("saleStatus")] + public SaleStatus SaleStatus { get; set; } + + [JsonPropertyName("saleProducts")] + public List SaleProducts { get; set; } = []; + + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleProductData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleProductData.cs new file mode 100644 index 00000000..b7c0d43c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleProductData.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleProductData +{ + [JsonPropertyName("saleId")] + public int SaleId { get; set; } + + [JsonPropertyName("productId")] + public int ProductId { get; set; } + + [JsonPropertyName("productName")] + public string ProductName { get; set; } = string.Empty; + + [JsonPropertyName("quantity")] + public int Quantity { get; set; } + + [JsonPropertyName("price")] + public double Price { get; set; } + + [JsonPropertyName("discount")] + public double Discount { get; set; } + + [JsonPropertyName("totalPrice")] + public double TotalPrice { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleRoot.cs new file mode 100644 index 00000000..2c45292e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleRoot.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleRoot : Root +{ + [JsonPropertyName("data")] + public SaleData? Data { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleSummaryData.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleSummaryData.cs new file mode 100644 index 00000000..db26f69c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SaleSummaryData.cs @@ -0,0 +1,38 @@ +using ECommerce.Shared.TerrenceLGee.Enums; +using System; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SaleSummaryData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("customerId")] + public string CustomerId { get; set; } = string.Empty; + + [JsonPropertyName("customerName")] + public string CustomerName { get; set; } = string.Empty; + + [JsonPropertyName("saleProductCount")] + public int SaleProductCount { get; set; } + + [JsonPropertyName("totalBaseAmount")] + public double TotalBaseAmount { get; set; } + + [JsonPropertyName("totalDiscountAmount")] + public double TotalDiscountAmount { get; set; } + + [JsonPropertyName("totalAmount")] + public double TotalAmount { get; set; } + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updatedAt")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("saleStatus")] + public SaleStatus SaleStatus { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SalesRoot.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SalesRoot.cs new file mode 100644 index 00000000..c15df84c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Models/Sale/SalesRoot.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +public class SalesRoot : Root +{ + [JsonPropertyName("data")] + public List Data { get; set; } = []; + + [JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } + + [JsonPropertyName("totalPages")] + public int TotalPages { get; set; } + + [JsonPropertyName("totalItemsRetrieved")] + public int TotalItemsRetrieved { get; set; } + + [JsonPropertyName("totalItems")] + public int TotalItems { get; set; } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Urls.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Urls.cs new file mode 100644 index 00000000..30115e87 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Data/Urls.cs @@ -0,0 +1,40 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Data; + +public static class Urls +{ + public static string BaseUrl => "https://localhost:7001/api/"; + public static string RegisterUrl => "auth/register"; + public static string LoginUrl => "auth/login"; + public static string PasswordResetUrl => "auth/reset"; + public static string LogoutUrl => "auth/logout"; + public static string AddAddressUrl => "addresses/add"; + public static string UpdateAddressUrl => "addresses/update/"; + public static string DeleteAddressUrl => "addresses/"; + public static string GetAddressForCustomerUrl => "addresses/"; + public static string GetCustomerAddressForAdminUrl => "addresses/admin/"; + public static string GetAllAddressesForCustomerUrl => "addresses"; + public static string GetAllAddressesForAdminUrl => "addresses/admin"; + public static string AdminAddCategoryUrl => "categories/admin/add"; + public static string AdminUpdateCategoryUrl => "categories/admin/update/"; + public static string AdminGetCategoryByIdUrl => "categories/admin/"; + public static string AdminGetCategoriesUrl => "categories/admin"; + public static string CustomerGetCategoryByIdUrl => "categories/"; + public static string CustomerGetCategoriesUrl => "categories"; + public static string CustomerGetProfileUrl => "customers/profile"; + public static string GetAllCustomersForAdminUrl => "customers/admin"; + public static string AdminAddProductUrl => "products/admin/add"; + public static string AdminUpdateProductUrl => "products/admin/update/"; + public static string AdminDeleteProductUrl => "products/admin/delete/"; + public static string AdminRestoreProductUrl => "products/admin/restore/"; + public static string CustomerGetProductByIdUrl => "products/"; + public static string CustomerGetProductsUrl => "products"; + public static string AdminGetProductByIdUrl => "products/admin/"; + public static string AdminGetProductsUrl => "products/admin"; + public static string CustomerCreateSaleUrl => "sales/checkout"; + public static string CustomerGetSaleByIdUrl => "sales/"; + public static string CustomerGetSalesUrl => "sales"; + public static string CustomerCancelSaleUrl => "sales/cancel/"; + public static string AdminGetSaleByIdUrl => "sales/admin/"; + public static string AdminGetAllSalesUrl => "sales/admin"; + public static string AdminUpdateSaleStatusUrl => "sales/admin/update/"; +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee.csproj b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee.csproj new file mode 100644 index 00000000..fc932ed8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee.csproj @@ -0,0 +1,52 @@ + + + WinExe + net10.0 + enable + app.manifest + true + + + + + + + + + + + + + + + + + None + All + + + + + + + + + + + + + + + + + + + + + DisplayAddedCategoryView.axaml + + + DisplayCustomerOrderDetailForAdminView.axaml + + + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/AdminMenu.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/AdminMenu.cs new file mode 100644 index 00000000..0bbb241c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/AdminMenu.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Enums; + +public enum AdminMenu +{ + [Display(Name = "Categories")] + Categories, + [Display(Name = "Products")] + Products, + [Display(Name = "Customers")] + Customers, + [Display(Name = "Logout")] + Logout +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/CustomerMenu.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/CustomerMenu.cs new file mode 100644 index 00000000..0faa16ad --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Enums/CustomerMenu.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Enums; + +public enum CustomerMenu +{ + [Display(Name = "My Information")] + ViewProfile, + [Display(Name = "Shop")] + AddSale, + [Display(Name = "Logout")] + Logout +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Helpers/FilterHelper.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Helpers/FilterHelper.cs new file mode 100644 index 00000000..be311e9b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Helpers/FilterHelper.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Helpers; + +public static class FilterHelper +{ + public static async Task OnFilterChangedAsync(int page, Func loadMethod) + { + await Task.Delay(500); + page = 1; + await loadMethod(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddAddressMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddAddressMessage.cs new file mode 100644 index 00000000..e25a0b90 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddAddressMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record AddAddressMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressAddedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressAddedMessage.cs new file mode 100644 index 00000000..e6c7f306 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressAddedMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record AddressAddedMessage(AddressData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForDetailMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForDetailMessage.cs new file mode 100644 index 00000000..7a19f25a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForDetailMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record AddressSelectedForDetailMessage(int AddressId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForUpdateMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForUpdateMessage.cs new file mode 100644 index 00000000..78bdc889 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressSelectedForUpdateMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record AddressSelectedForUpdateMessage(AddressData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressUpdatedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressUpdatedMessage.cs new file mode 100644 index 00000000..27dde806 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/AddressUpdatedMessage.cs @@ -0,0 +1,6 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record AddressUpdatedMessage(AddressData Data); + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/CustomerAddresseSelectedForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/CustomerAddresseSelectedForAdminMessage.cs new file mode 100644 index 00000000..cde04dfa --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/CustomerAddresseSelectedForAdminMessage.cs @@ -0,0 +1,4 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record CustomerAddresseSelectedForAdminMessage(int AddressId, string? CustomerId); + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAddAddressMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAddAddressMessage.cs new file mode 100644 index 00000000..68695f38 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAddAddressMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record NavigateBackToAddAddressMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesAfterAddMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesAfterAddMessage.cs new file mode 100644 index 00000000..fd2be66c --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesAfterAddMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record NavigateBackToAllAddressesAfterAddMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesMessage.cs new file mode 100644 index 00000000..a1c7d6f9 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllAddressesMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record NavigateBackToAllAddressesMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllCustomerAddressesForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllCustomerAddressesForAdminMessage.cs new file mode 100644 index 00000000..97e89217 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToAllCustomerAddressesForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record NavigateBackToAllCustomerAddressesForAdminMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToUpdateAddressMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToUpdateAddressMessage.cs new file mode 100644 index 00000000..edb79da4 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/AddressMessages/NavigateBackToUpdateAddressMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +public record NavigateBackToUpdateAddressMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/AddCategoryMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/AddCategoryMessage.cs new file mode 100644 index 00000000..56712f4b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/AddCategoryMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record AddCategoryMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryAddedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryAddedMessage.cs new file mode 100644 index 00000000..8adb2d03 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryAddedMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record CategoryAddedMessage(CategoryAdminData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryProductSelectedForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryProductSelectedForAdminMessage.cs new file mode 100644 index 00000000..ea561356 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryProductSelectedForAdminMessage.cs @@ -0,0 +1,4 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record CategoryProductSelectedForAdminMessage(int ProductId, int CategoryId); + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForAdminMessage.cs new file mode 100644 index 00000000..7874ac0b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record CategorySelectedForAdminMessage(int CategoryId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForUpdateMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForUpdateMessage.cs new file mode 100644 index 00000000..40734254 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategorySelectedForUpdateMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record CategorySelectedForUpdateMessage(CategoryAdminSummaryData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryUpdatedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryUpdatedMessage.cs new file mode 100644 index 00000000..3ba4c989 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/CategoryUpdatedMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record CategoryUpdatedMessage(CategoryAdminData Data); \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAddCategoryMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAddCategoryMessage.cs new file mode 100644 index 00000000..dacd4ca9 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAddCategoryMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToAddCategoryMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAdminCategoryDetailView.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAdminCategoryDetailView.cs new file mode 100644 index 00000000..a9e51078 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAdminCategoryDetailView.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToAdminCategoryDetailView(int CategoryId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesFromUpdateView.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesFromUpdateView.cs new file mode 100644 index 00000000..41dd8d06 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesFromUpdateView.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToAllAdminCategoriesFromUpdateView; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesMessage.cs new file mode 100644 index 00000000..138a8f78 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToAllAdminCategoriesMessage.cs @@ -0,0 +1,4 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToAllAdminCategoriesMessage; + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToCategoryPageMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToCategoryPageMessage.cs new file mode 100644 index 00000000..f498e814 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToCategoryPageMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToCategoryPageMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToViewCategoriesForUpdateCategoryMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToViewCategoriesForUpdateCategoryMessage.cs new file mode 100644 index 00000000..0f727759 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateBackToViewCategoriesForUpdateCategoryMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateBackToViewCategoriesForUpdateCategoryMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateToAllCustomerCategoriesMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateToAllCustomerCategoriesMessage.cs new file mode 100644 index 00000000..52ec83b8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/NavigateToAllCustomerCategoriesMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record NavigateToAllCustomerCategoriesMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/UpdateCategoryMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/UpdateCategoryMessage.cs new file mode 100644 index 00000000..cb7c075b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/CategoryMessages/UpdateCategoryMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +public record UpdateCategoryMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminSelectedCustomerOrderForDetailMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminSelectedCustomerOrderForDetailMessage.cs new file mode 100644 index 00000000..f3cb6f9f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminSelectedCustomerOrderForDetailMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record AdminSelectedCustomerOrderForDetailMessage(int SaleId, CustomerData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminUpdatedCustomerOrderMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminUpdatedCustomerOrderMessage.cs new file mode 100644 index 00000000..7bf52e39 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/AdminUpdatedCustomerOrderMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record AdminUpdatedCustomerOrderMessage(CustomerData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerAddressDetailForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerAddressDetailForAdminMessage.cs new file mode 100644 index 00000000..bd1caed3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerAddressDetailForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record DisplayCustomerAddressDetailForAdminMessage(int AddressId, string customerId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerDetailsForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerDetailsForAdminMessage.cs new file mode 100644 index 00000000..05bb50d3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerDetailsForAdminMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record DisplayCustomerDetailsForAdminMessage(CustomerData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerProfileMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerProfileMessage.cs new file mode 100644 index 00000000..e6bd83b3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/DisplayCustomerProfileMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record DisplayCustomerProfileMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerDetailsMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerDetailsMessage.cs new file mode 100644 index 00000000..7c06acdb --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerDetailsMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record NavigateBackToCustomerDetailsMessage(CustomerData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerPageMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerPageMessage.cs new file mode 100644 index 00000000..9d74227b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/NavigateBackToCustomerPageMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record NavigateBackToCustomerPageMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomerSaleProductDetailForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomerSaleProductDetailForAdminMessage.cs new file mode 100644 index 00000000..5d30d639 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomerSaleProductDetailForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record ViewCustomerSaleProductDetailForAdminMessage(int ProductId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomersForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomersForAdminMessage.cs new file mode 100644 index 00000000..dd1a55c7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/Customer/ViewCustomersForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +public record ViewCustomersForAdminMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/OtherMessages/NavigateBackToPreviousPageMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/OtherMessages/NavigateBackToPreviousPageMessage.cs new file mode 100644 index 00000000..f53f33b2 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/OtherMessages/NavigateBackToPreviousPageMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; + +public record NavigateBackToPreviousPageMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/AddProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/AddProductMessage.cs new file mode 100644 index 00000000..66ae150e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/AddProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record AddProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/DeleteProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/DeleteProductMessage.cs new file mode 100644 index 00000000..9aac94ec --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/DeleteProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record DeleteProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAddProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAddProductMessage.cs new file mode 100644 index 00000000..146db47a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAddProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record NavigateBackToAddProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsFromUpdateView.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsFromUpdateView.cs new file mode 100644 index 00000000..7ec3d0d5 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsFromUpdateView.cs @@ -0,0 +1,4 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record NavigateBackToAllAdminProductsFromUpdateView; + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsMessage.cs new file mode 100644 index 00000000..97bb44b4 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToAllAdminProductsMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record NavigateBackToAllAdminProductsMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToProductPageMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToProductPageMessage.cs new file mode 100644 index 00000000..4b7407b7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToProductPageMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record NavigateBackToProductPageMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToUpdateProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToUpdateProductMessage.cs new file mode 100644 index 00000000..15975ae3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/NavigateBackToUpdateProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record NavigateBackToUpdateProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductAddedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductAddedMessage.cs new file mode 100644 index 00000000..6e2bf092 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductAddedMessage.cs @@ -0,0 +1,6 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record ProductAddedMessage(ProductAdminData Data); + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForAdminMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForAdminMessage.cs new file mode 100644 index 00000000..189c8846 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForAdminMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record ProductSelectedForAdminMessage(int ProductId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForUpdateMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForUpdateMessage.cs new file mode 100644 index 00000000..0ca13f4b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductSelectedForUpdateMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record ProductSelectedForUpdateMessage(ProductAdminData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductUpdatedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductUpdatedMessage.cs new file mode 100644 index 00000000..2c1b8179 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/ProductUpdatedMessage.cs @@ -0,0 +1,6 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record ProductUpdatedMessage(ProductAdminData Data); + diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/RestoreProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/RestoreProductMessage.cs new file mode 100644 index 00000000..ce063eee --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/RestoreProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record RestoreProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/UpdateProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/UpdateProductMessage.cs new file mode 100644 index 00000000..3016c25d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/ProductMessages/UpdateProductMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +public record UpdateProductMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CategorySelectedForSaleMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CategorySelectedForSaleMessage.cs new file mode 100644 index 00000000..dfd2052f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CategorySelectedForSaleMessage.cs @@ -0,0 +1,7 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record CategorySelectedForSaleMessage(int CategoryId, List ShoppingCart); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CheckoutMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CheckoutMessage.cs new file mode 100644 index 00000000..cbd820bc --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/CheckoutMessage.cs @@ -0,0 +1,6 @@ +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record CheckoutMessage(List ShoppingCart); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackFromViewCart.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackFromViewCart.cs new file mode 100644 index 00000000..3420ecf7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackFromViewCart.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record NavigateBackFromViewCart; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesForSale.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesForSale.cs new file mode 100644 index 00000000..e4fa8eeb --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesForSale.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record NavigateBackToAllCategoriesForSale; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesOrderCanceledMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesOrderCanceledMessage.cs new file mode 100644 index 00000000..50a91cb7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToAllCategoriesOrderCanceledMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public class NavigateBackToAllCategoriesOrderCanceledMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToProductsFromSelectedProductMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToProductsFromSelectedProductMessage.cs new file mode 100644 index 00000000..ed951af4 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/NavigateBackToProductsFromSelectedProductMessage.cs @@ -0,0 +1,7 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record NavigateBackToProductsFromSelectedProductMessage(int CategoryId, List ShoppingCart); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/OrderCompletedMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/OrderCompletedMessage.cs new file mode 100644 index 00000000..56f07316 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/OrderCompletedMessage.cs @@ -0,0 +1,5 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record OrderCompletedMessage(SaleData Data); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ProductSelectedForSaleMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ProductSelectedForSaleMessage.cs new file mode 100644 index 00000000..a1acb2b8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ProductSelectedForSaleMessage.cs @@ -0,0 +1,7 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record ProductSelectedForSaleMessage(ProductData Data, int CategoryId, List ShoppingCart); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleProductSelectedForCustomerDetailMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleProductSelectedForCustomerDetailMessage.cs new file mode 100644 index 00000000..ef913ae1 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleProductSelectedForCustomerDetailMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record SaleProductSelectedForCustomerDetailMessage(int ProductId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleSelectedForCustomerDetailMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleSelectedForCustomerDetailMessage.cs new file mode 100644 index 00000000..3e26b842 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/SaleSelectedForCustomerDetailMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record SaleSelectedForCustomerDetailMessage(int SaleId); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ShopAgainMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ShopAgainMessage.cs new file mode 100644 index 00000000..2fdc4e60 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ShopAgainMessage.cs @@ -0,0 +1,3 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record ShopAgainMessage; diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ViewCartMessage.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ViewCartMessage.cs new file mode 100644 index 00000000..c84d422f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Messages/SaleMessages/ViewCartMessage.cs @@ -0,0 +1,6 @@ +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; + +public record ViewCartMessage(List ShoppingCart); diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Program.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Program.cs new file mode 100644 index 00000000..4173cfb0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace ECommerce.AvaloniaClient.TerrenceLGee; + +internal sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AddressService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AddressService.cs new file mode 100644 index 00000000..bccd51c0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AddressService.cs @@ -0,0 +1,373 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class AddressService : IAddressService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public AddressService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task AddAddressAsync(CreateAddressDto address) + { + var addressDataForError = new AddressData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AddAddressUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(address), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressAddedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressAddedResponse is null) + { + addressDataForError.ErrorMessage = $"Unable to add address at this time, please try again later"; + return addressDataForError; + } + + if (!addressAddedResponse.IsSuccess || addressAddedResponse.StatusCode != 201) + { + addressDataForError.ErrorMessage = $"Unable to add address: {string.Join('\n', addressAddedResponse.Errors)}"; + return addressDataForError; + } + + return addressAddedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(AddAddressAsync)}\n" + + $"There was an API error adding the address: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an API error adding the address"; + return addressDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(AddAddressAsync)}\n" + + $"There was an unexpected error adding the address: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an unexpected error adding the address"; + return addressDataForError; + } + } + + public async Task UpdateAddressAsync(UpdateAddressDto address) + { + var addressDataForError = new AddressData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.UpdateAddressUrl}{address.Id}"; + + var content = new StringContent(JsonSerializer.Serialize(address), Encoding.UTF8, MediaType); + var response = await httpClient.PutAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressUpdatedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressUpdatedResponse is null) + { + addressDataForError.ErrorMessage = $"Unable to updated address {address.Id} at this time, please try again later"; + return addressDataForError; + } + + if (!addressUpdatedResponse.IsSuccess || addressUpdatedResponse.StatusCode != 200) + { + addressDataForError.ErrorMessage = $"Unable to update address {address.Id}: {string.Join('\n', addressUpdatedResponse.Errors)}"; + return addressDataForError; + } + + return addressUpdatedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(UpdateAddressAsync)}\n" + + $"There was an API error updating address {address.Id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an API error updating address {address.Id}"; + return addressDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(UpdateAddressAsync)}\n" + + $"There was an unexpected error updating address {address.Id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an unexpected error updating address {address.Id}"; + return addressDataForError; + } + } + + public async Task<(bool, string?)> DeleteAddressAsync(int addressId) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.DeleteAddressUrl}{addressId}"; + + var response = await httpClient.DeleteAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressDeletedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressDeletedResponse is null) + { + return (false, $"Unable to delete address {addressId} at this time, please try again later"); + } + + if (!addressDeletedResponse.IsSuccess || addressDeletedResponse.StatusCode != 200) + { + return (false, $"Unable to delete address {addressId}: {string.Join('\n', addressDeletedResponse.Errors)}"); + } + + return (true, addressDeletedResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(DeleteAddressAsync)}\n" + + $"There was an API error deleting address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an API error deleting address {addressId}"); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(DeleteAddressAsync)}\n" + + $"There was an unexpected error deleting address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an unexpected error deleting address {addressId}"); + } + } + + public async Task GetAddressAsync(int addressId) + { + var addressDataForError = new AddressData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.GetAddressForCustomerUrl}{addressId}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressResponse is null) + { + addressDataForError.ErrorMessage = $"Unable to retrieve address {addressId} at this time, please try again later"; + return addressDataForError; + } + + if (!addressResponse.IsSuccess || addressResponse.StatusCode != 200) + { + addressDataForError.ErrorMessage = $"Unable to retrieve address {addressId}: {string.Join('\n', addressResponse.Errors)}"; + return addressDataForError; + } + + return addressResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAddressAsync)}\n" + + $"There was an API error retrieving address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an API error retrieving address {addressId}"; + return addressDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAddressAsync)}\n" + + $"There was an unexpected error retrieving address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an unexpected error retrieving address {addressId}"; + return addressDataForError; + } + } + + public async Task GetCustomerAddressForAdminAsync(int addressId, string? customerId) + { + var addressDataForError = new AddressData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.GetCustomerAddressForAdminUrl}{addressId}"; + + var addressIdDto = new AddressIdDto { CustomerId = customerId }; + + var content = new StringContent(JsonSerializer.Serialize(addressIdDto), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressResponse is null) + { + addressDataForError.ErrorMessage = $"Unable to retrieve address {addressId} at this time, plesae try again later"; + return addressDataForError; + } + + if (!addressResponse.IsSuccess || addressResponse.StatusCode != 200) + { + addressDataForError.ErrorMessage = $"Unable to retrieve address {addressId}: {string.Join('\n', addressResponse.Errors)}"; + return addressDataForError; + } + + return addressResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetCustomerAddressForAdminAsync)}\n" + + $"There was an API error retrieving address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an API error retrieving address {addressId}"; + return addressDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetCustomerAddressForAdminAsync)}\n" + + $"There was an unexpected error retrieving address {addressId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + addressDataForError.ErrorMessage = $"There was an unexpected error retrieving address {addressId}"; + return addressDataForError; + } + } + + public async Task GetAddressesForCustomerAsync(AddressQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.GetAllAddressesForCustomerUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressesResponse is null) return null; + + return addressesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAddressesForCustomerAsync)}\n" + + $"There was an API error attempting to retrieve al of the addresses: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAddressesForCustomerAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the addresses: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + public async Task GetAllCustomerAddressesForAdminAsync(AddressQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.GetAllAddressesForAdminUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var addressesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (addressesResponse is null) return null; + + return addressesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAllCustomerAddressesForAdminAsync)}\n" + + $"There was an API error attempting to retrieve all of the addresses: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AddressService)}\n" + + $"Method: {nameof(GetAllCustomerAddressesForAdminAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the addresses: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + private static string BuildQueryString(AddressQueryParams queryParams) + { + var query = new StringBuilder(); + query.Append($"?page={queryParams.Page}&pageSize={queryParams.PageSize}"); + + if (!string.IsNullOrEmpty(queryParams.CustomerId)) + { + query.Append($"&customerId={queryParams.CustomerId}"); + } + + if (!string.IsNullOrEmpty(queryParams.City)) + { + query.Append($"&city={queryParams.City}"); + } + + if (!string.IsNullOrEmpty(queryParams.State)) + { + query.Append($"&state={queryParams.State}"); + } + + if (!string.IsNullOrEmpty(queryParams.Country)) + { + query.Append($"&country={queryParams.Country}"); + } + + return query.ToString(); + } + + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AuthService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AuthService.cs new file mode 100644 index 00000000..ea9e3243 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/AuthService.cs @@ -0,0 +1,220 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class AuthService : IAuthService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly IAuthTokenHolder _tokenHolder; + public string? JwtToken { get; set; } + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public AuthService(IHttpClientFactory clientFactory, ILogger logger, IAuthTokenHolder tokenHolder) + { + _clientFactory = clientFactory; + _logger = logger; + _tokenHolder = tokenHolder; + } + + public async Task<(bool, string?)> RegisterUserAsync(UserRegistrationDto userDto) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.RegisterUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(userDto), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var registrationResponse = JsonSerializer.Deserialize(responseContent, options); + + if (registrationResponse is null) + { + return (false, "Unable to register user at this time."); + } + + if (!registrationResponse.IsSuccess || registrationResponse.StatusCode != 200) + { + var errors = string.Join('\n', registrationResponse.Errors); + return (false, errors); + } + + return (true, registrationResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(RegisterUserAsync)}\n" + + $"There was an API error attempting to register a new user: {ex.Message}\n"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, "Registration request failed."); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(RegisterUserAsync)}\n" + + $"There was an unexpected error attempting to register a new user: {ex.Message}\n"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, "Unexpected error during user registration\nRegistration failed"); + } + } + + public async Task<(bool, AuthData?)> LoginUserAsync(UserLoginDto userDto) + { + var authDataForLoginFailure = new AuthData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.LoginUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(userDto), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + if (!response.IsSuccessStatusCode) + { + _tokenHolder.SetToken(null); + authDataForLoginFailure.ErrorMessage = $"Login failed\nReason: {response.ReasonPhrase}."; + return (false, authDataForLoginFailure); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var loginResponse = JsonSerializer.Deserialize(responseContent, options); + + if (loginResponse is null) + { + _tokenHolder.SetToken(null); + authDataForLoginFailure.ErrorMessage = "Unable to login at this time."; + return (false, authDataForLoginFailure); + } + + if (loginResponse.Data?.AccessToken is null) + { + _tokenHolder.SetToken(null); + authDataForLoginFailure.ErrorMessage = $"Unable to retrieve valid authorization credientals."; + return (false, authDataForLoginFailure); + } + + _tokenHolder.SetToken(loginResponse.Data.AccessToken); + JwtToken = loginResponse.Data.AccessToken; + return (true, loginResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(LoginUserAsync)}\n" + + $"There was an API error during the user's login attempt: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + authDataForLoginFailure.ErrorMessage = "Error occurred during connection to the endpoint\nLogin failed."; + return (false, authDataForLoginFailure); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(LoginUserAsync)}\n" + + $"There was an unexpected error during the user's login attempt: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + authDataForLoginFailure.ErrorMessage = "Unable to connect\nLogin failed."; + return (false, authDataForLoginFailure); + } + } + + public async Task<(bool, string?)> ResetUserPasswordAsync(UserResetPasswordDto userDto) + { + try + { + var httpClient = _clientFactory.CreateClient(); + var url = $"{Urls.BaseUrl}{Urls.PasswordResetUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(userDto), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var resetResponse = JsonSerializer.Deserialize(responseContent, options); + + if (resetResponse is null) + { + return (false, $"Unable to reset password, please try again later"); + } + + if (!resetResponse.IsSuccess || resetResponse.StatusCode != 200) + { + return (false, $"{string.Join('\n', resetResponse.Errors)}"); + } + + return (true, resetResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(ResetUserPasswordAsync)}\n" + + $"There was an API error during the user's password reset attempt: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, "Error occurred during connection to the endpoint\nPassword reset failed."); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(ResetUserPasswordAsync)}\n" + + $"There was an unexpected error during the user's password reset attempt: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, "Unable to connect\nPassword reset failed."); + } + } + + public async Task LogoutUserAsync() + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.LogoutUrl}"; + + var request = new HttpRequestMessage(HttpMethod.Post, url); + var response = await httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) return false; + + var responseContent = await response.Content.ReadAsStringAsync(); + var logoutResponse = JsonSerializer.Deserialize(responseContent, options); + + if (logoutResponse is null || string.IsNullOrEmpty(logoutResponse.Data)) return false; + + if (!logoutResponse.IsSuccess || logoutResponse.StatusCode != 200) return false; + + return true; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(LogoutUserAsync)}\n" + + $"There was an API error logging the user out of the system: {ex.Message}\n"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return false; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(AuthService)}\n" + + $"Method: {nameof(LogoutUserAsync)}\n" + + $"There was an unexpected error logging the user out of the system: {ex.Message}\n"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return false; + } + } +} \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CategoryService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CategoryService.cs new file mode 100644 index 00000000..6706c0c9 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CategoryService.cs @@ -0,0 +1,313 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class CategoryService : ICategoryService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public CategoryService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task AddCategoryAsync(CreateCategoryDto category) + { + var categoryAdminDataForError = new CategoryAdminData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminAddCategoryUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(category), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoryAddedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoryAddedResponse is null) + { + categoryAdminDataForError.ErrorMessage = $"Unable to add new category at this time, please try again later"; + return categoryAdminDataForError; + } + + if (!categoryAddedResponse.IsSuccess || categoryAddedResponse.StatusCode != 201) + { + categoryAdminDataForError.ErrorMessage = $"Unable to add new category: {string.Join('\n', categoryAddedResponse.Errors)}"; + return categoryAdminDataForError; + } + + return categoryAddedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(AddCategoryAsync)}\n" + + $"There was an API error attempting to add the new category: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = "There was an API error attempting to add the new category."; + return categoryAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(AddCategoryAsync)}\n" + + $"There was an unexpected error attempting to add the new category: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = "There was an unexpected error attempting to add the new category."; + return categoryAdminDataForError; + } + } + + public async Task UpdateCategoryAsync(UpdateCategoryDto category) + { + var categoryAdminDataForError = new CategoryAdminData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminUpdateCategoryUrl}{category.Id}"; + + var content = new StringContent(JsonSerializer.Serialize(category), Encoding.UTF8, MediaType); + + var response = await httpClient.PutAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoryUpdatedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoryUpdatedResponse is null) + { + categoryAdminDataForError.ErrorMessage = $"Unable to category {category.Id} at this time, please try again later."; + return categoryAdminDataForError; + } + + if (!categoryUpdatedResponse.IsSuccess || categoryUpdatedResponse.StatusCode != 200) + { + categoryAdminDataForError.ErrorMessage = $"Unable to update category {category.Id}: {string.Join('\n', categoryUpdatedResponse.Errors)}"; + return categoryAdminDataForError; + } + + return categoryUpdatedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(UpdateCategoryAsync)}\n" + + $"There was an API error attempting to update category {category.Id}: {ex.Message}."; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = $"There was an API error attempting to update category {category.Id}."; + return categoryAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(UpdateCategoryAsync)}\n" + + $"There was an unexpected error attempting to update category {category.Id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = $"There was an unexpected error attempting to update category {category.Id}"; + return categoryAdminDataForError; + } + } + + public async Task GetCategoryAsync(int id) + { + var categoryDataForError = new CategoryData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetCategoryByIdUrl}{id}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoryResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoryResponse is null) + { + categoryDataForError.ErrorMessage = $"Unable to retrieve category {id} at this time, please try again later"; + return categoryDataForError; + } + + if (!categoryResponse.IsSuccess || categoryResponse.StatusCode != 200) + { + categoryDataForError.ErrorMessage = $"Unable to retrieve category {id}: {string.Join('\n', categoryResponse.Errors)}"; + return categoryDataForError; + } + + return categoryResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoryAsync)}\n" + + $"There was an API error retrieving category {id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryDataForError.ErrorMessage = $"There was an API error retrieving category {id}"; + return categoryDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoryAsync)}\n" + + $"There was an unexpected error retrieving category {id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryDataForError.ErrorMessage = $"There was an unexpected error retrieving category {id}"; + return categoryDataForError; + } + } + + public async Task GetCategoryForAdminAsync(int id) + { + var categoryAdminDataForError = new CategoryAdminData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetCategoryByIdUrl}{id}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoryResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoryResponse is null) + { + categoryAdminDataForError.ErrorMessage = $"Unable to retrieve category {id} at this time, please try again later"; + return categoryAdminDataForError; + } + + if (!categoryResponse.IsSuccess || categoryResponse.StatusCode != 200) + { + categoryAdminDataForError.ErrorMessage = $"Unable to retrieve category {id}: {string.Join('\n', categoryResponse.Errors)}"; + return categoryAdminDataForError; + } + + return categoryResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoryForAdminAsync)}\n" + + $"There was an API error retrieving category {id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = $"There was an API error retrieving category {id}"; + return categoryAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoryForAdminAsync)}\n" + + $"There was an unexpected error retrieving category {id}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + categoryAdminDataForError.ErrorMessage = $"There was an unexpected error retrieving category {id}"; + return categoryAdminDataForError; + } + } + + + public async Task GetCategoriesAsync(CategoryQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetCategoriesUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoriesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoriesResponse is null) return null; + + return categoriesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoriesAsync)}\n" + + $"There was an API error attempting to retrieve all of the categories: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoriesAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the categories: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + public async Task GetCategoriesForAdminAsync(CategoryQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetCategoriesUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var categoriesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (categoriesResponse is null) return null; + + return categoriesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoriesForAdminAsync)}\n" + + $"There was an API error attempting to retrieve all of the categories: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CategoryService)}\n" + + $"Method: {nameof(GetCategoriesForAdminAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the categories: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + private static string BuildQueryString(CategoryQueryParams queryParams) + { + var query = new StringBuilder(); + query.Append($"?page={queryParams.Page}&pageSize={queryParams.PageSize}"); + + if (!string.IsNullOrEmpty(queryParams.Description)) + { + query.Append($"&description={queryParams.Description}"); + } + + if (!string.IsNullOrEmpty(queryParams.OrderBy)) + { + query.Append($"&orderBy={queryParams.OrderBy}"); + } + + return query.ToString(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CustomerService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CustomerService.cs new file mode 100644 index 00000000..1b48a8be --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/CustomerService.cs @@ -0,0 +1,141 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class CustomerService : ICustomerService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public CustomerService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task GetCustomerProfileAsync() + { + var customerDataForError = new CustomerData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetProfileUrl}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var customerResponse = JsonSerializer.Deserialize(responseContent, options); + + if (customerResponse is null) + { + customerDataForError.ErrorMessage = $"Unable to retrieve customer profile at this time, please try again later"; + return customerDataForError; + } + + if (!customerResponse.IsSuccess || customerResponse.StatusCode != 200) + { + customerDataForError.ErrorMessage = $"Unable to retrieve customer profile: {string.Join('\n', customerResponse.Errors)}"; + return customerDataForError; + } + + return customerResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CustomerService)}\n" + + $"Method: {nameof(GetCustomerProfileAsync)}\n" + + $"There was an API error retrieving the customer profile: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + customerDataForError.ErrorMessage = "There was an API error retrieving the customer profile"; + return customerDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CustomerService)}\n" + + $"Method: {nameof(GetCustomerProfileAsync)}\n" + + $"There was an unexpected error retrieving the customer profile: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + customerDataForError.ErrorMessage = $"There was an unexpected error retrieving the customer profile"; + return customerDataForError; + } + } + + public async Task GetCustomersForAdminAsync(CustomerQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.GetAllCustomersForAdminUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var customersResponse = JsonSerializer.Deserialize(responseContent, options); + + if (customersResponse is null) return null; + + return customersResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(CustomerService)}\n" + + $"Method: {nameof(GetCustomersForAdminAsync)}\n" + + $"There was an API error attempting to retrieve all customers: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(CustomerService)}\n" + + $"Method: {nameof(GetCustomersForAdminAsync)}\n" + + $"There was an unexpected error attempting to retrieve all customers: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + private static string BuildQueryString(CustomerQueryParams queryParams) + { + var query = new StringBuilder(); + query.Append($"?page={queryParams.Page}&pageSize={queryParams.PageSize}"); + + if (queryParams.MinSaleCount.HasValue) + { + query.Append($"&minSaleCount={queryParams.MinSaleCount}"); + } + + if (queryParams.MaxSaleCount.HasValue) + { + query.Append($"&maxSaleCount={queryParams.MaxSaleCount}"); + } + + if (queryParams.MinTotalSpent.HasValue) + { + query.Append($"&minTotalSpent={queryParams.MinTotalSpent}"); + } + + if (queryParams.MaxTotalSpent.HasValue) + { + query.Append($"&maxTotalSpend={queryParams.MaxTotalSpent}"); + } + + return query.ToString(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Handlers/AuthHeaderHandler.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Handlers/AuthHeaderHandler.cs new file mode 100644 index 00000000..47bf5d72 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Handlers/AuthHeaderHandler.cs @@ -0,0 +1,29 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Handlers; + +public class AuthHeaderHandler : DelegatingHandler +{ + private readonly IAuthTokenHolder _tokenHolder; + + public AuthHeaderHandler(IAuthTokenHolder tokenHolder) + { + _tokenHolder = tokenHolder; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(_tokenHolder.Token)) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenHolder.Token); + } + + return await base.SendAsync(request, cancellationToken); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Address/IAddressService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Address/IAddressService.cs new file mode 100644 index 00000000..978293cf --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Address/IAddressService.cs @@ -0,0 +1,17 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; + +public interface IAddressService +{ + Task AddAddressAsync(CreateAddressDto address); + Task UpdateAddressAsync(UpdateAddressDto address); + Task<(bool, string?)> DeleteAddressAsync(int addressId); + Task GetAddressAsync(int addressId); + Task GetCustomerAddressForAdminAsync(int addressId, string? customerId); + Task GetAddressesForCustomerAsync(AddressQueryParams queryParams); + Task GetAllCustomerAddressesForAdminAsync(AddressQueryParams queryParams); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthService.cs new file mode 100644 index 00000000..8cd10830 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthService.cs @@ -0,0 +1,14 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Auth; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; + +public interface IAuthService +{ + string? JwtToken { get; set; } + Task<(bool, string?)> RegisterUserAsync(UserRegistrationDto userDto); + Task<(bool, AuthData?)> LoginUserAsync(UserLoginDto userDto); + Task<(bool, string?)> ResetUserPasswordAsync(UserResetPasswordDto userDto); + Task LogoutUserAsync(); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthTokenHolder.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthTokenHolder.cs new file mode 100644 index 00000000..3ba03e35 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Auth/IAuthTokenHolder.cs @@ -0,0 +1,7 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; + +public interface IAuthTokenHolder +{ + string? Token { get; } + void SetToken(string? token); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/AuthTokenHolder.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/AuthTokenHolder.cs new file mode 100644 index 00000000..248d3768 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/AuthTokenHolder.cs @@ -0,0 +1,9 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces; + +public class AuthTokenHolder : IAuthTokenHolder +{ + public string? Token { get; private set; } + public void SetToken(string? token) => Token = token; +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Category/ICategoryService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Category/ICategoryService.cs new file mode 100644 index 00000000..45f39769 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Category/ICategoryService.cs @@ -0,0 +1,17 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; + +public interface ICategoryService +{ + Task AddCategoryAsync(CreateCategoryDto category); + Task UpdateCategoryAsync(UpdateCategoryDto category); + Task GetCategoryAsync(int id); + Task GetCategoryForAdminAsync(int id); + Task GetCategoriesAsync(CategoryQueryParams queryParams); + Task GetCategoriesForAdminAsync(CategoryQueryParams queryParams); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Customer/ICustomerService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Customer/ICustomerService.cs new file mode 100644 index 00000000..82635466 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Customer/ICustomerService.cs @@ -0,0 +1,11 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; + +public interface ICustomerService +{ + Task GetCustomerProfileAsync(); + Task GetCustomersForAdminAsync(CustomerQueryParams queryParams); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Product/IProductService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Product/IProductService.cs new file mode 100644 index 00000000..49d3c511 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Product/IProductService.cs @@ -0,0 +1,19 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; + +public interface IProductService +{ + Task AddProductAsync(CreateProductDto product); + Task UpdateProductAsync(UpdateProductDto product); + Task<(bool, string?)> DeleteProductAsync(int productId); + Task<(bool, string?)> RestoreProductAsync(int productId); + Task GetProductForAdminAsync(int productId); + Task GetProductAsync(int productId); + Task GetProductsForAdminAsync(ProductQueryParams queryParams); + Task GetProductsAsync(ProductQueryParams queryParams); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/ISaleService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/ISaleService.cs new file mode 100644 index 00000000..a44e4bde --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/ISaleService.cs @@ -0,0 +1,18 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; + +public interface ISaleService +{ + Task CreateOrderAsync(CreateOrderDto sale); + Task GetSaleForCustomerAsync(int saleId); + Task GetSaleForAdminAsync(int saleId); + Task GetSalesForCustomerAsync(SaleQueryParams queryParams); + Task GetSalesForAdminAsync(SaleQueryParams queryParams); + Task<(bool, string?)> AdminUpdateSaleStatusAsync(int saleId, UpdateSaleStatusDto saleStatus); + Task<(bool, string?)> CustomerCancelSaleAsync(int saleId); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/IShoppingCartService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/IShoppingCartService.cs new file mode 100644 index 00000000..f170cb45 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/Interfaces/Sale/IShoppingCartService.cs @@ -0,0 +1,9 @@ +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; + +public interface IShoppingCartService +{ + static List ShoppingCart { get; set; } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ProductService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ProductService.cs new file mode 100644 index 00000000..b49ed2b7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ProductService.cs @@ -0,0 +1,450 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class ProductService : IProductService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public ProductService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task AddProductAsync(CreateProductDto product) + { + var productAdminDataForError = new ProductAdminData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminAddProductUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(product), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productAddedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productAddedResponse is null) + { + productAdminDataForError.ErrorMessage = $"Unable to add new product in category {product.CategoryId} at this time, " + + $"please try again later"; + return productAdminDataForError; + } + + if (!productAddedResponse.IsSuccess || productAddedResponse.StatusCode != 201) + { + productAdminDataForError.ErrorMessage = $"Unable to add new product in category {product.CategoryId}: " + + $"{string.Join('\n', productAddedResponse.Errors)}"; + return productAdminDataForError; + } + + return productAddedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(AddProductAsync)}\n" + + $"There was an API error adding a new product in category {product.CategoryId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an API error adding a new product in category {product.CategoryId}"; + return productAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(AddProductAsync)}\n" + + $"There was an unexpected error adding a new product in category {product.CategoryId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an unexpected error adding a new product in category {product.CategoryId}"; + return productAdminDataForError; + } + } + + public async Task UpdateProductAsync(UpdateProductDto product) + { + var productAdminDataForError = new ProductAdminData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminUpdateProductUrl}{product.Id}"; + + var content = new StringContent(JsonSerializer.Serialize(product), Encoding.UTF8, MediaType); + var response = await httpClient.PutAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productUpdatedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productUpdatedResponse is null) + { + productAdminDataForError.ErrorMessage = $"Unable to update product {product.Id} in category " + + $"{product.CategoryId} at this time, please try again later"; + return productAdminDataForError; + } + + if (!productUpdatedResponse.IsSuccess || productUpdatedResponse.StatusCode != 200) + { + productAdminDataForError.ErrorMessage = $"Unable to update product {product.Id} in category " + + $"{product.CategoryId}: {string.Join('\n', productUpdatedResponse.Errors)}"; + return productAdminDataForError; + } + + return productUpdatedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(UpdateProductAsync)}\n" + + $"There was an API error updating product {product.Id} in category {product.CategoryId}: " + + $"{ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an API error updating product {product.Id} in category {product.CategoryId}"; + return productAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(UpdateProductAsync)}\n" + + $"There was an unexpected error updating product {product.Id} in category {product.CategoryId}: " + + $"{ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an unexpected error updating product {product.Id} in category {product.CategoryId}"; + return productAdminDataForError; + } + } + + public async Task<(bool, string?)> DeleteProductAsync(int productId) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminDeleteProductUrl}{productId}"; + + var response = await httpClient.DeleteAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productDeletedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productDeletedResponse is null) + { + return (false, $"Unable to delete product {productId} at this time, please try again later"); + } + + if (!productDeletedResponse.IsSuccess || productDeletedResponse.StatusCode != 200) + { + return (false, $"Unable to delete product {productId}: {string.Join('\n', productDeletedResponse.Errors)}"); + } + + return (true, productDeletedResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(DeleteProductAsync)}\n" + + $"There was an API error deleting product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an API error deleting product {productId}"); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(DeleteProductAsync)}\n" + + $"There was an unexpected error deleting product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an unexpected error deleting product {productId}"); + } + } + + public async Task<(bool, string?)> RestoreProductAsync(int productId) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminRestoreProductUrl}{productId}"; + + var response = await httpClient.PostAsync(url, null); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productRestoredResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productRestoredResponse is null) + { + return (false, $"Unable to restore product {productId} at this time, please try again later"); + } + + if (!productRestoredResponse.IsSuccess || productRestoredResponse.StatusCode != 200) + { + return (false, $"Unable to restore product {productId}: {string.Join('\n', productRestoredResponse.Errors)}"); + } + + return (true, productRestoredResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(RestoreProductAsync)}\n" + + $"There was an API error restoring product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an API error restoring product {productId}"); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(RestoreProductAsync)}\n" + + $"There was an unexpected error restoring product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an unexpected error deleting product {productId}"); + } + } + + public async Task GetProductForAdminAsync(int productId) + { + var productAdminDataForError = new ProductAdminData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetProductByIdUrl}{productId}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productResponse is null) + { + productAdminDataForError.ErrorMessage = $"Unable to retrieve product {productId} at this time, please try again later"; + return productAdminDataForError; + } + + if (!productResponse.IsSuccess || productResponse.StatusCode != 200) + { + productAdminDataForError.ErrorMessage = $"Unable to retrieve product {productId}: {string.Join('\n', productResponse.Errors)}"; + return productAdminDataForError; + } + + return productResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductForAdminAsync)}\n" + + $"There was an API error retrieving product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an API error retrieving product {productId}"; + return productAdminDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductForAdminAsync)}\n" + + $"There was an unexpected error retrieving product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productAdminDataForError.ErrorMessage = $"There was an unexpected error retrieving product {productId}"; + return productAdminDataForError; + } + } + + public async Task GetProductAsync(int productId) + { + var productDataForError = new ProductData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetProductByIdUrl}{productId}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var productResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productResponse is null) + { + productDataForError.ErrorMessage = $"Unable to retrieve product {productId} at this time, please try again later"; + return productDataForError; + } + + if (!productResponse.IsSuccess || productResponse.StatusCode != 200) + { + productDataForError.ErrorMessage = $"Unable to retrieve product {productId}: {string.Join('\n', productResponse.Errors)}"; + return productDataForError; + } + + return productResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductAsync)}\n" + + $"There was an API error retrieving product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productDataForError.ErrorMessage = $"There was an API error retrieving product {productId}"; + return productDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductAsync)}\n" + + $"There was an unexpected error retrieving product {productId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + productDataForError.ErrorMessage = $"There was an unexpected error retrieving product {productId}"; + return productDataForError; + } + } + + public async Task GetProductsForAdminAsync(ProductQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetProductsUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var productsResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productsResponse is null) return null; + + return productsResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductsForAdminAsync)}\n" + + $"There was an API error attempting to retrieve all of the products: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductsForAdminAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the products: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + public async Task GetProductsAsync(ProductQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetProductsUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var productsResponse = JsonSerializer.Deserialize(responseContent, options); + + if (productsResponse is null) return null; + + return productsResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductsAsync)}\n" + + $"There was an API error attempting to retrieve all of the products: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(ProductService)}\n" + + $"Method: {nameof(GetProductsAsync)}\n" + + $"There was an unexpected error attempting to retrieve all of the products: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + private static string BuildQueryString(ProductQueryParams queryParams) + { + var query = new StringBuilder(); + query.Append($"?page={queryParams.Page}&pageSize={queryParams.PageSize}"); + + if (queryParams.MinUnitPrice.HasValue) + { + query.Append($"&minUnitPrice={queryParams.MinUnitPrice}"); + } + + if (queryParams.MaxUnitPrice.HasValue) + { + query.Append($"&maxUnitPrice={queryParams.MaxUnitPrice}"); + } + + if (queryParams.MinStockQuantity.HasValue) + { + query.Append($"&minStockQuantity={queryParams.MinStockQuantity}"); + } + + if (queryParams.MaxStockQuantity.HasValue) + { + query.Append($"&maxStockQuantity={queryParams.MaxStockQuantity}"); + } + + if (queryParams.MinDiscountPercentage.HasValue) + { + query.Append($"&minDiscountPercentage={queryParams.MinDiscountPercentage}"); + } + + if (queryParams.MaxDiscountPercentage.HasValue) + { + query.Append($"&maxDiscountPercentage={queryParams.MaxDiscountPercentage}"); + } + + if (queryParams.CategoryId.HasValue) + { + query.Append($"&categoryId={queryParams.CategoryId}"); + } + + if (!string.IsNullOrEmpty(queryParams.CategoryName)) + { + query.Append($"&categoryName={queryParams.CategoryName}"); + } + + if (!string.IsNullOrEmpty(queryParams.Description)) + { + query.Append($"&description={queryParams.Description}"); + } + + if (queryParams.InStock.HasValue) + { + query.Append($"&inStock={queryParams.InStock}"); + } + + if (queryParams.IsDeleted.HasValue) + { + query.Append($"&isDeleted={queryParams.IsDeleted}"); + } + + return query.ToString(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/SaleService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/SaleService.cs new file mode 100644 index 00000000..d90b5a69 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/SaleService.cs @@ -0,0 +1,361 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Data; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class SaleService : ISaleService +{ + private readonly IHttpClientFactory _clientFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + private string _errorMessage = string.Empty; + private const string ClientName = "client"; + private const string MediaType = "application/json"; + private const string LogErrorString = "{msg}\n\n"; + + public SaleService(IHttpClientFactory clientFactory, ILogger logger) + { + _clientFactory = clientFactory; + _logger = logger; + } + + public async Task CreateOrderAsync(CreateOrderDto sale) + { + var saleDataForError = new SaleData(); + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerCreateSaleUrl}"; + + var content = new StringContent(JsonSerializer.Serialize(sale), Encoding.UTF8, MediaType); + + var response = await httpClient.PostAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var saleCreatedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (saleCreatedResponse is null) + { + saleDataForError.ErrorMessage = $"Unable to complete sale at this time, please try again later"; + return saleDataForError; + } + + if (!saleCreatedResponse.IsSuccess || saleCreatedResponse.StatusCode != 201) + { + saleDataForError.ErrorMessage = $"Unable to complete sale: {string.Join('\n', saleCreatedResponse.Errors)}"; + return saleDataForError; + } + + return saleCreatedResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(CreateOrderAsync)}\n" + + $"There was an API error attempting to complete the sale: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = "There was an API error attempting to complete the sale"; + return saleDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(CreateOrderAsync)}\n" + + $"There was an unexpected error attempting to comeplete the sale: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = "There was an unexpected error attempting to complete the sale"; + return saleDataForError; + } + } + + public async Task GetSaleForCustomerAsync(int saleId) + { + var saleDataForError = new SaleData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetSaleByIdUrl}{saleId}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var saleResponse = JsonSerializer.Deserialize(responseContent, options); + + if (saleResponse is null) + { + saleDataForError.ErrorMessage = $"Unable to retrieve sale {saleId} at this time, please try again later"; + return saleDataForError; + } + + if (!saleResponse.IsSuccess || saleResponse.StatusCode != 200) + { + saleDataForError.ErrorMessage = $"Unable to retrieve sale {saleId}: {string.Join('\n', saleResponse.Errors)}"; + return saleDataForError; + } + + return saleResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSaleForCustomerAsync)}\n" + + $"There was an API error retrieving sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = $"There was an API error retrieving sale {saleId}"; + return saleDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSaleForCustomerAsync)}\n" + + $"There was an unexpected error retrieving sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = $"There was an unexpected error retrieving sale {saleId}"; + return saleDataForError; + } + } + + public async Task GetSaleForAdminAsync(int saleId) + { + var saleDataForError = new SaleData(); + + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetSaleByIdUrl}{saleId}"; + + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + var saleResponse = JsonSerializer.Deserialize(responseContent, options); + + if (saleResponse is null) + { + saleDataForError.ErrorMessage = $"Unable to retrieve sale {saleId} at this time, please try again later"; + return saleDataForError; + } + + if (!saleResponse.IsSuccess || saleResponse.StatusCode != 200) + { + saleDataForError.ErrorMessage = $"Unable to retrieve sale {saleId}: {string.Join('\n', saleResponse.Errors)}"; + return saleDataForError; + } + + return saleResponse.Data; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSaleForAdminAsync)}\n" + + $"There was an API error retrieving sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = $"There was an API error retrieving sale {saleId}"; + return saleDataForError; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSaleForAdminAsync)}\n" + + $"There was an unexpected error retrieving sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + saleDataForError.ErrorMessage = $"There was an unexpected error retrieving sale {saleId}"; + return saleDataForError; + } + } + + public async Task GetSalesForCustomerAsync(SaleQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerGetSalesUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var salesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (salesResponse is null) return null; + + return salesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSalesForCustomerAsync)}\n" + + $"There was an API error attempting to retrieve all sales: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSalesForCustomerAsync)}\n" + + $"There was an unexpected error attempting to retrieve all sales: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + public async Task GetSalesForAdminAsync(SaleQueryParams queryParams) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminGetAllSalesUrl}{BuildQueryString(queryParams)}"; + + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) return null; + + var responseContent = await response.Content.ReadAsStringAsync(); + var salesResponse = JsonSerializer.Deserialize(responseContent, options); + + if (salesResponse is null) return null; + + return salesResponse; + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSalesForAdminAsync)}\n" + + $"There was an API error attempting to retrieve all sales: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(GetSalesForAdminAsync)}\n" + + $"There was an unexpected error attempting to retrieve all sales: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return null; + } + } + + public async Task<(bool, string?)> AdminUpdateSaleStatusAsync(int saleId, UpdateSaleStatusDto saleStatus) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.AdminUpdateSaleStatusUrl}{saleId}"; + + var content = new StringContent(JsonSerializer.Serialize(saleStatus), Encoding.UTF8, MediaType); + var response = await httpClient.PutAsync(url, content); + + var responseContent = await response.Content.ReadAsStringAsync(); + var saleStatusUpdatedResponse = JsonSerializer.Deserialize(responseContent, options); + + if (saleStatusUpdatedResponse is null) + { + return (false, $"Unable to update the status for sale {saleId} at this time, please try again later"); + } + + if (!saleStatusUpdatedResponse.IsSuccess || saleStatusUpdatedResponse.StatusCode != 200) + { + return (false, $"Unable to update the status for sale {saleId}: {string.Join('\n', saleStatusUpdatedResponse.Errors)}"); + } + + return (true, saleStatusUpdatedResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(AdminUpdateSaleStatusAsync)}\n" + + $"There was an API error attempting to update the status of sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an API error attempting to update the status of sale {saleId}"); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(AdminUpdateSaleStatusAsync)}\n" + + $"There was an unexpected error attempting to update the status of sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an unexpected error attempting to update the status of sale {saleId}"); + } + } + + public async Task<(bool, string?)> CustomerCancelSaleAsync(int saleId) + { + try + { + var httpClient = _clientFactory.CreateClient(ClientName); + var url = $"{Urls.BaseUrl}{Urls.CustomerCancelSaleUrl}{saleId}"; + + var response = await httpClient.PostAsync(url, null); + + var responseContent = await response.Content.ReadAsStringAsync(); + var saleCanceledResponse = JsonSerializer.Deserialize(responseContent, options); + + if (saleCanceledResponse is null) + { + return (false, $"Unable to cancel sale {saleId} at this time, please try again later"); + } + + if (!saleCanceledResponse.IsSuccess || saleCanceledResponse.StatusCode != 200) + { + return (false, $"Unable to cancel sale {saleId}: {string.Join('\n', saleCanceledResponse.Errors)}"); + } + + return (true, saleCanceledResponse.Data); + } + catch (HttpRequestException ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(CustomerCancelSaleAsync)}\n" + + $"There was an API error attempting to cancel sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an API error attempting to cancel sale {saleId}"); + } + catch (Exception ex) + { + _errorMessage = $"\nClass: {nameof(SaleService)}\n" + + $"Method: {nameof(CustomerCancelSaleAsync)}\n" + + $"There was an unexpected error attempting to cancel sale {saleId}: {ex.Message}"; + _logger.LogError(ex, LogErrorString, _errorMessage); + return (false, $"There was an unexpected error attempting to cancel sale {saleId}"); + } + } + + private static string BuildQueryString(SaleQueryParams queryParams) + { + var query = new StringBuilder(); + query.Append($"?page={queryParams.Page}&pageSize={queryParams.PageSize}"); + + if (!string.IsNullOrEmpty(queryParams.CustomerId)) + { + query.Append($"&customerId={queryParams.CustomerId}"); + } + + if (queryParams.MinTotalAmount.HasValue) + { + query.Append($"&minTotalAmount={queryParams.MinTotalAmount}"); + } + + if (queryParams.MaxTotalAmount.HasValue) + { + query.Append($"&maxTotalAmount={queryParams.MaxTotalAmount}"); + } + + if (!string.IsNullOrEmpty(queryParams.Status)) + { + query.Append($"&status={queryParams.Status}"); + } + + return query.ToString(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ShoppingCartService.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ShoppingCartService.cs new file mode 100644 index 00000000..118a4180 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Services/ShoppingCartService.cs @@ -0,0 +1,11 @@ +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using System.Collections.Generic; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.Services; + +public class ShoppingCartService : IShoppingCartService +{ + public static List ShoppingCart { get; set; } = []; +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Styles/ControlStyles.axaml b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Styles/ControlStyles.axaml new file mode 100644 index 00000000..1a51bf66 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Styles/ControlStyles.axaml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewLocator.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewLocator.cs new file mode 100644 index 00000000..30204e14 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewLocator.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace ECommerce.AvaloniaClient.TerrenceLGee; +/// +/// Given a view model, returns the corresponding view if possible. +/// +[RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddAddressViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddAddressViewModel.cs new file mode 100644 index 00000000..da004e91 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddAddressViewModel.cs @@ -0,0 +1,159 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class AddAddressViewModel : ObservableValidator +{ + private readonly IAddressService _addressService; + private readonly IMessenger _messenger; + + public AddAddressViewModel(IAddressService addressService, IMessenger messenger) + { + _addressService = addressService; + _messenger = messenger; + } + + [ObservableProperty] + [Required(ErrorMessage = "Address Line 1 is required.")] + [MaxLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine1Errors))] + private string _addressLine1; + + public string? AddressLine1Errors => GetErrors(nameof(AddressLine1)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine2Errors))] + private string? _addressLine2; + + public string? AddressLine2Errors => GetErrors(nameof(AddressLine2)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "City is required.")] + [MaxLength(50, ErrorMessage = "City cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CityErrors))] + public string _city; + + public string? CityErrors => GetErrors(nameof(City)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "State is required.")] + [MaxLength(50, ErrorMessage = "State cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(StateErrors))] + private string _state; + + public string? StateErrors => GetErrors(nameof(State)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Postal code is required.")] + [RegularExpression(@"\d{4,6}$", ErrorMessage = "Invalid postal code.")] + [NotifyPropertyChangedFor(nameof(PostalCodeErrors))] + private string _postalCode; + + public string? PostalCodeErrors => GetErrors(nameof(PostalCode)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Country is required.")] + [MaxLength(50, ErrorMessage = "Country cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CountryErrors))] + private string _country; + + public string? CountryErrors => GetErrors(nameof(Country)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private bool _isBillingAddress; + + [ObservableProperty] + private bool _isShippingAddress; + + [ObservableProperty] + private string? _successMessage; + + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + private async Task AddAddressAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(AddressLine1, nameof(AddressLine1)); + ValidateProperty(AddressLine2, nameof(AddressLine2)); + ValidateProperty(City, nameof(City)); + ValidateProperty(State, nameof(State)); + ValidateProperty(PostalCode, nameof(PostalCode)); + ValidateProperty(Country, nameof(Country)); + + if (HasErrors) + { + return; + } + + var address = new CreateAddressDto + { + AddressLine1 = AddressLine1, + AddressLine2 = AddressLine2, + City = City, + State = State, + PostalCode = PostalCode, + Country = Country, + IsBillingAddress = IsBillingAddress, + IsShippingAddress = IsShippingAddress + }; + + var data = await _addressService.AddAddressAsync(address); + + if (data is null) + { + ErrorMessage = "Unable to add address at this time"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearAddressAdd(); + SuccessMessage = "Address added successfully"; + _messenger.Send(new AddressAddedMessage(data)); + } + else + { + ErrorMessage = data.ErrorMessage; + } + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + + private void ClearAddressAdd() + { + AddressLine1 = string.Empty; + AddressLine2 = string.Empty; + City = string.Empty; + State = string.Empty; + PostalCode = string.Empty; + Country = string.Empty; + IsBillingAddress = false; + IsShippingAddress = false; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddCategoryViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddCategoryViewModel.cs new file mode 100644 index 00000000..42285c85 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddCategoryViewModel.cs @@ -0,0 +1,102 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class AddCategoryViewModel : ObservableValidator +{ + private readonly ICategoryService _categoryService; + private readonly IMessenger _messenger; + + public AddCategoryViewModel(ICategoryService categoryService, IMessenger messenger) + { + _categoryService = categoryService; + _messenger = messenger; + } + + [ObservableProperty] + [Required(ErrorMessage = "Category name is required.")] + [MaxLength(100, ErrorMessage = "Category name cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(NameErrors))] + private string _name; + + public string? NameErrors => GetErrors(nameof(Name)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(500, ErrorMessage = "Category description cannot exceed 500 characters.")] + [NotifyPropertyChangedFor(nameof(DescriptionErrors))] + private string? _description; + + public string? DescriptionErrors => GetErrors(nameof(Description)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + public string? _successMessage; + + [ObservableProperty] + public string? _errorMessage; + + + [RelayCommand] + private async Task AddCategoryAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Name, nameof(Name)); + ValidateProperty(Description, nameof(Description)); + + if (HasErrors) + { + return; + } + + var category = new CreateCategoryDto + { + Name = Name, + Description = Description + }; + + + var data = await _categoryService.AddCategoryAsync(category); + + if (data is null) + { + ErrorMessage = "Unable to add category at this time"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearCategoryAdd(); + SuccessMessage = "Category added successfully"; + _messenger.Send(new CategoryAddedMessage(data)); + } + else + { + ErrorMessage = data.ErrorMessage; + } + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToCategoryPageMessage()); + } + + private void ClearCategoryAdd() + { + Name = string.Empty; + Description = string.Empty; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddProductViewModel.cs new file mode 100644 index 00000000..d5419572 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AddProductViewModel.cs @@ -0,0 +1,258 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class AddProductViewModel : ObservableValidator +{ + private readonly ICategoryService _categoryService; + private readonly IProductService _productService; + private readonly IMessenger _messenger; + public ObservableCollection Categories { get; } = []; + + public AddProductViewModel(ICategoryService categoryService, IProductService productService, IMessenger messenger) + { + _categoryService = categoryService; + _productService = productService; + _messenger = messenger; + LoadCategoriesCommand.Execute(null); + } + + [ObservableProperty] + private int _categoryId; + + [ObservableProperty] + [Required(ErrorMessage = "Product name is required.")] + [MaxLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(NameErrors))] + private string _name; + + public string? NameErrors => GetErrors(nameof(Name)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(1000, ErrorMessage = "Product description cannot exceed 1000 characters.")] + [NotifyPropertyChangedFor(nameof(DescriptionErrors))] + private string? _description; + + public string? DescriptionErrors => GetErrors(nameof(Description)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Product stock quantity is required.")] + [Range(0, 5000, ErrorMessage = "Product stock quantity must be between 0 and the maximum capacity of our warehouse which is 5000.")] + [NotifyPropertyChangedFor(nameof(StockQuantityErrors))] + private int _stockQuantity; + + public string? StockQuantityErrors => GetErrors(nameof(StockQuantity)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Product unit price is required.")] + [Range(0.01, double.MaxValue, ErrorMessage = "Product unit price must be greater than $0.00.")] + [NotifyPropertyChangedFor(nameof(UnitPriceErrors))] + private decimal _unitPrice; + + public string? UnitPriceErrors => GetErrors(nameof(UnitPrice)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Discount percentage is required.")] + [Range(0, 100, ErrorMessage = "Discount percentage must be between 0% and 100%.")] + [NotifyPropertyChangedFor(nameof(DiscountPercentageErrors))] + private int _discountPercentage; + + public string? DiscountPercentageErrors => GetErrors(nameof(DiscountPercentage)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Url] + [NotifyPropertyChangedFor(nameof(ImageUrlErrors))] + private string? _imageUrl; + + public string? ImageUrlErrors => GetErrors(nameof(ImageUrl)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private bool _isDeleted; + + [ObservableProperty] + private bool _isInStock; + + [ObservableProperty] + private string? _successMessage; + + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + public async Task AddProductAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Name, nameof(Name)); + ValidateProperty(Description, nameof(Description)); + ValidateProperty(StockQuantity, nameof(StockQuantity)); + ValidateProperty(UnitPrice, nameof(UnitPrice)); + ValidateProperty(DiscountPercentage, nameof(DiscountPercentage)); + ValidateProperty(ImageUrl, nameof(ImageUrl)); + + if (HasErrors) + { + return; + } + + var product = new CreateProductDto + { + CategoryId = CategoryId, + Name = Name, + Description = Description, + StockQuantity = StockQuantity, + UnitPrice = UnitPrice, + DiscountPercentage = DiscountPercentage, + IsDeleted = IsDeleted, + IsInStock = IsInStock, + ImageUrl = ImageUrl + }; + + var data = await _productService.AddProductAsync(product); + + if (data is null) + { + ErrorMessage = "Unable to add product at this time"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearProductAdd(); + SuccessMessage = "Product added successfully"; + _messenger.Send(new ProductAddedMessage(data)); + } + else + { + ErrorMessage = data.ErrorMessage; + } + } + + private void ClearProductAdd() + { + Name = string.Empty; + Description = string.Empty; + StockQuantity = 0; + UnitPrice = 0.0m; + DiscountPercentage = 0; + IsDeleted = false; + IsInStock = false; + } + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private CategoryAdminSummaryData? _selectedCategory; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private string? _searchByDescription; + + [RelayCommand] + private async Task LoadCategoriesAsync() + { + Page = 1; + await FetchCategoriesAsync(); + } + + private async Task FetchCategoriesAsync() + { + IsLoading = true; + + var queryParams = new CategoryQueryParams + { + Page = Page, + PageSize = PageSize, + Description = SearchByDescription + }; + + var result = await _categoryService.GetCategoriesForAdminAsync(queryParams); + + if (result is not null) + { + Categories.Clear(); + + foreach (var category in result.Data) + { + Categories.Add(category); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchCategoriesAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchCategoriesAsync(); + } + + [RelayCommand] + private void ClearFilters() + { + SearchByDescription = null; + } + + async partial void OnSearchByDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCategoriesAsync); + + partial void OnSelectedCategoryChanged(CategoryAdminSummaryData? value) + { + if (value is not null) + { + CategoryId = value.Id; + } + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToProductPageMessage()); + } + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseCategoryForUpdateViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseCategoryForUpdateViewModel.cs new file mode 100644 index 00000000..2223769d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseCategoryForUpdateViewModel.cs @@ -0,0 +1,102 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class AdminChooseCategoryForUpdateViewModel : ObservableObject +{ + private readonly ICategoryService _categoryService; + private readonly IMessenger _messenger; + public ObservableCollection Categories { get; } = []; + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private CategoryAdminSummaryData? _selectedCategoryForUpdate; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + + public AdminChooseCategoryForUpdateViewModel(ICategoryService categoryService, IMessenger messenger) + { + _categoryService = categoryService; + _messenger = messenger; + LoadCategoriesCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadCategoriesAsync() + { + IsLoading = true; + + var queryParams = new CategoryQueryParams + { + Page = Page, + PageSize = PageSize + }; + + var result = await _categoryService.GetCategoriesForAdminAsync(queryParams); + + if (result is not null) + { + Categories.Clear(); + + foreach (var category in result.Data) + { + Categories.Add(category); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await LoadCategoriesAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await LoadCategoriesAsync(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToCategoryPageMessage()); + } + + partial void OnSelectedCategoryForUpdateChanged(CategoryAdminSummaryData? value) + { + if (value is not null) + { + _messenger.Send(new CategorySelectedForUpdateMessage(value)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseProductForUpdateViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseProductForUpdateViewModel.cs new file mode 100644 index 00000000..fc3afabb --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/AdminChooseProductForUpdateViewModel.cs @@ -0,0 +1,57 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class AdminChooseProductForUpdateViewModel : ProductsAdminBaseViewModel +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public AdminChooseProductForUpdateViewModel(IProductService productService, IMessenger messenger) + { + _productService = productService; + _messenger = messenger; + LoadProductsCommand.Execute(null); + } + + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + + protected override async Task GetProductsAsync() + { + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + CategoryName = CategoryName, + Description = Description, + InStock = InStock, + IsDeleted = IsDeleted + }; + + return await _productService.GetProductsForAdminAsync(queryParams); + } + + protected override void OnProductSelected(ProductAdminData product) + { + _messenger.Send(new ProductSelectedForUpdateMessage(product)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CategoryOperationsViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CategoryOperationsViewModel.cs new file mode 100644 index 00000000..15e83638 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CategoryOperationsViewModel.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class CategoryOperationsViewModel : ViewModelBase +{ + private readonly IMessenger _messenger; + + public CategoryOperationsViewModel(IMessenger messenger) + { + _messenger = messenger; + } + + [RelayCommand] + private void AddCategory() + { + _messenger.Send(new AddCategoryMessage()); + } + + [RelayCommand] + private void UpdateCategory() + { + _messenger.Send(new UpdateCategoryMessage()); + } + + [RelayCommand] + private void ViewCategories() + { + _messenger.Send(new NavigateBackToAllAdminCategoriesMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CheckoutViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CheckoutViewModel.cs new file mode 100644 index 00000000..aa661d46 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CheckoutViewModel.cs @@ -0,0 +1,177 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class CheckoutViewModel : ObservableObject +{ + private readonly ISaleService _saleService; + private readonly IMessenger _messenger; + + [ObservableProperty] + private static List _shoppingCart; + [ObservableProperty] + private static List _shoppingCartForDisplay; + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + [ObservableProperty] + private CartItemDto? _selectedItem; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private bool _hasPreviousPage; + + [ObservableProperty] + private decimal _subTotal; + + public CheckoutViewModel(ISaleService saleService, List shoppingCart, IMessenger messenger) + { + _saleService = saleService; + _messenger = messenger; + _shoppingCart = shoppingCart; + _subTotal = CalculateSubTotal(_shoppingCart); + _shoppingCartForDisplay = new List(); + LoadCartCommand.Execute(null); + } + + [RelayCommand] + private void LoadCart() + { + var pagedCart = ShoppingCart.Skip((Page - 1) * PageSize) + .Take(PageSize) + .ToList(); + + ShoppingCartForDisplay = pagedCart; + + TotalPages = (int)Math.Ceiling(ShoppingCart.Count / (double)PageSize); + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + [RelayCommand] + private void NextPage() + { + if (!HasNextPage) return; + Page++; + LoadCart(); + } + + [RelayCommand] + private void PreviousPage() + { + if (!HasPreviousPage) return; + Page--; + LoadCart(); + } + + [RelayCommand] + private async Task CheckoutAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + var order = new CreateOrderDto + { + ShoppingCart = ShoppingCart + }; + + var result = await _saleService.CreateOrderAsync(order); + + if (result is null) + { + ErrorMessage = "Unable to complete order at this time"; + return; + } + + if (string.IsNullOrEmpty(result.ErrorMessage)) + { + SuccessMessage = "Order completed successfully"; + ShoppingCart.Clear(); + _messenger.Send(new OrderCompletedMessage(result)); + } + else + { + ErrorMessage = result.ErrorMessage; + } + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + + [RelayCommand] + private void CancelOrder() + { + ShoppingCart.Clear(); + _messenger.Send(new NavigateBackToAllCategoriesOrderCanceledMessage()); + } + + [RelayCommand] + private async Task RemoveItemAsync() + { + if (SelectedItem is not null) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Delete", "Delete this item?", ButtonEnum.YesNo, Icon.Warning, null, WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + ShoppingCart.Remove(SelectedItem); + LoadCart(); + } + } + } + + [RelayCommand] + private async Task ClearCartAsync() + { + var box = MessageBoxManager + .GetMessageBoxStandard("Empty", "Empty Cart?", ButtonEnum.YesNo, Icon.Warning, null, + WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + ShoppingCart.Clear(); + LoadCart(); + } + } + + private decimal CalculateSubTotal(List items) + { + var subtotal = 0.0m; + + foreach (var item in items) + { + subtotal += ((decimal)item.ProductPrice! * item.Quantity); + } + + return subtotal; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerChooseAddressForUpdateViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerChooseAddressForUpdateViewModel.cs new file mode 100644 index 00000000..9b87003f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerChooseAddressForUpdateViewModel.cs @@ -0,0 +1,95 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class CustomerChooseAddressForUpdateViewModel : ObservableObject +{ + private readonly IAddressService _addressService; + private readonly IMessenger _messenger; + public ObservableCollection Addresses { get; } = []; + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private AddressData? _selectedAddress; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + + public CustomerChooseAddressForUpdateViewModel(IAddressService addressService, IMessenger messenger) + { + _addressService = addressService; + _messenger = messenger; + LoadAddressesCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadAddressesAsync() + { + IsLoading = true; + + var queryParams = new AddressQueryParams + { + Page = Page, + PageSize = PageSize + }; + + var result = await _addressService.GetAddressesForCustomerAsync(queryParams); + + if (result is not null) + { + Addresses.Clear(); + + foreach (var address in result.Data) + { + Addresses.Add(address); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages && result.TotalItemsRetrieved >= PageSize; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await LoadAddressesAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await LoadAddressesAsync(); + } + + partial void OnSelectedAddressChanged(AddressData? value) + { + if (value is not null) + { + _messenger.Send(new AddressSelectedForUpdateMessage(value)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerOperationsViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerOperationsViewModel.cs new file mode 100644 index 00000000..79f7b07f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/CustomerOperationsViewModel.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class CustomerOperationsViewModel : ViewModelBase +{ + private readonly IMessenger _messenger; + + public CustomerOperationsViewModel(IMessenger messenger) + { + _messenger = messenger; + } + + [RelayCommand] + private void ViewCustomers() + { + _messenger.Send(new ViewCustomersForAdminMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DeleteProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DeleteProductViewModel.cs new file mode 100644 index 00000000..93de076b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DeleteProductViewModel.cs @@ -0,0 +1,98 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DeleteProductViewModel : ProductsAdminBaseViewModel +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public DeleteProductViewModel(IProductService productService, IMessenger messenger) + { + _productService = productService; + _messenger = messenger; + LoadProductsCommand.Execute(null); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + + + private async Task DeleteProductAsync(ProductAdminData value) + { + if (value is not null) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Delete", $"Delete {value.Name}?", ButtonEnum.YesNo, Icon.Warning, + null, WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + var (success, data) = await _productService.DeleteProductAsync(value.Id); + + if (success) + { + SelectedProduct = null; + box = MessageBoxManager + .GetMessageBoxStandard("Success", $"{data}", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + + result = await box.ShowAsync(); + } + else + { + SelectedProduct = null; + box = MessageBoxManager + .GetMessageBoxStandard("Error", $"{data}", ButtonEnum.Ok, Icon.Error, null, + WindowStartupLocation.CenterOwner); + + result = await box.ShowAsync(); + + } + await LoadProductsAsync(); + } + } + } + + protected override async Task GetProductsAsync() + { + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + CategoryName = CategoryName, + Description = Description, + InStock = InStock, + IsDeleted = IsDeleted + }; + + return await _productService.GetProductsForAdminAsync(queryParams); + } + + protected override void OnProductSelected(ProductAdminData product) + { + Dispatcher.UIThread.InvokeAsync( + () => DeleteProductAsync(product)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedAddressViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedAddressViewModel.cs new file mode 100644 index 00000000..1b1b29ee --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedAddressViewModel.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAddedAddressViewModel : ViewModelBase +{ + private readonly IMessenger _messenger; + public AddressData Address { get; } + + public DisplayAddedAddressViewModel(AddressData address, IMessenger messenger) + { + Address = address; + _messenger = messenger; + } + + [RelayCommand] + private void AddAnotherAddress() + { + _messenger.Send(new AddAddressMessage()); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedCategoryViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedCategoryViewModel.cs new file mode 100644 index 00000000..fc21b962 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedCategoryViewModel.cs @@ -0,0 +1,31 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAddedCategoryViewModel : ViewModelBase +{ + public CategoryAdminData Category { get; } + private readonly IMessenger _messenger; + + + public DisplayAddedCategoryViewModel(CategoryAdminData category, IMessenger messenger) + { + Category = category; + _messenger = messenger; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToAddCategoryMessage()); + } + + [RelayCommand] + private void GoBackToPreviousPage() + { + _messenger.Send(new NavigateBackToCategoryPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedProductViewModel.cs new file mode 100644 index 00000000..ef604f4e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddedProductViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAddedProductViewModel : ViewModelBase +{ + public ProductAdminData Product { get; } + private readonly IMessenger _messenger; + + public DisplayAddedProductViewModel(ProductAdminData product, IMessenger messenger) + { + Product = product; + _messenger = messenger; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToAddProductMessage()); + } + + [RelayCommand] + private void GoBackToProducts() + { + _messenger.Send(new NavigateBackToAllAdminProductsMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddressViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddressViewModel.cs new file mode 100644 index 00000000..2bc932c3 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAddressViewModel.cs @@ -0,0 +1,91 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAddressViewModel : ViewModelBase +{ + private readonly IAddressService _addressService; + private readonly IMessenger _messenger; + private readonly int _addressId; + [ObservableProperty] + private AddressData? _address; + + public DisplayAddressViewModel(IAddressService addressService, int addressId, IMessenger messenger) + { + _addressService = addressService; + _addressId = addressId; + _messenger = messenger; + } + + public async Task GetAddressAsync() + { + Address = await _addressService.GetAddressAsync(_addressId); + + if (Address is null) + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + } + + [RelayCommand] + private async Task GoBack() + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + + [RelayCommand] + private void UpdateAddress() + { + _messenger.Send(new AddressSelectedForUpdateMessage(Address!)); + } + + [RelayCommand] + private async Task DeleteAddress() + { + var box = MessageBoxManager + .GetMessageBoxStandard("Delete", $"Delete Address?", ButtonEnum.YesNo, Icon.Warning, + null, WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + var (success, data) = await _addressService.DeleteAddressAsync(Address!.Id); + + if (success) + { + box = MessageBoxManager + .GetMessageBoxStandard("Success", $"{data}", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + result = await box.ShowAsync(); + if (result == ButtonResult.Ok) + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + } + else + { + box = MessageBoxManager + .GetMessageBoxStandard("Error", $"{data}", ButtonEnum.Ok, Icon.Error, + null, WindowStartupLocation.CenterOwner); + + result = await box.ShowAsync(); + if (result == ButtonResult.Ok) + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + } + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminCategoryDetailViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminCategoryDetailViewModel.cs new file mode 100644 index 00000000..2b9259d8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminCategoryDetailViewModel.cs @@ -0,0 +1,178 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAdminCategoryDetailViewModel : ObservableObject +{ + public int CategoryId { get; } + [ObservableProperty] + public CategoryAdminData? _category; + public ObservableCollection Products { get; } = []; + private readonly ICategoryService _categoryService; + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + [ObservableProperty] + private ProductAdminData? _selectedProduct; + + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private decimal? _minUnitPrice; + [ObservableProperty] + private decimal? _maxUnitPrice; + [ObservableProperty] + private int? _minStockQuantity; + [ObservableProperty] + private int? _maxStockQuantity; + [ObservableProperty] + private int? _minDiscountPercentage; + [ObservableProperty] + private int? _maxDiscountPercentage; + [ObservableProperty] + private string? _description; + + public DisplayAdminCategoryDetailViewModel( + ICategoryService categoryService, + IProductService productService, + int categoryId, + IMessenger messenger) + { + _categoryService = categoryService; + _productService = productService; + CategoryId = categoryId; + _messenger = messenger; + LoadProductsCommand.Execute(null); + } + + public async Task GetCategoryAsync() + { + Category = await _categoryService.GetCategoryForAdminAsync(CategoryId); + if (Category is null) GoBack(); + } + + [RelayCommand] + private async Task LoadProductsAsync() + { + Page = 1; + await FetchProductsAsync(); + } + + + + private async Task FetchProductsAsync() + { + IsLoading = true; + + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + CategoryId = CategoryId, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + Description = Description + }; + + var result = await _productService.GetProductsForAdminAsync(queryParams); + + if (result is not null) + { + Products.Clear(); + + foreach (var product in result.Data) + { + Products.Add(product); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchProductsAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchProductsAsync(); + } + + [RelayCommand] + private async Task ClearFiltersAsync() + { + MinUnitPrice = null; + MaxUnitPrice = null; + MinStockQuantity = null; + MaxStockQuantity = null; + MinDiscountPercentage = null; + MaxDiscountPercentage = null; + Description = null; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToAllAdminCategoriesMessage()); + } + + partial void OnSelectedProductChanged(ProductAdminData? value) + { + if (value is not null) + { + _messenger.Send(new CategoryProductSelectedForAdminMessage(value.Id, CategoryId)); + } + } + + async partial void OnMinUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminProductViewModel.cs new file mode 100644 index 00000000..d0d31839 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayAdminProductViewModel.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayAdminProductViewModel : ObservableObject +{ + private int _productId { get; } + + [ObservableProperty] + private ProductAdminData? _product; + + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public DisplayAdminProductViewModel(IProductService productService, int productId, IMessenger messenger) + { + _productService = productService; + _productId = productId; + _messenger = messenger; + } + + public async Task GetProductAsync() + { + Product = await _productService.GetProductForAdminAsync(_productId); + if (Product is null) GoBack(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerAddressForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerAddressForAdminViewModel.cs new file mode 100644 index 00000000..ea37c01b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerAddressForAdminViewModel.cs @@ -0,0 +1,45 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerAddressForAdminViewModel : ObservableObject +{ + public int AddressId { get; } + public string? CustomerId { get; } + + [ObservableProperty] + public AddressData? _address; + + private readonly IAddressService _addressService; + private readonly IMessenger _messenger; + + public DisplayCustomerAddressForAdminViewModel( + IAddressService addressService, + int addressId, + string? customerId, + IMessenger messenger) + { + _addressService = addressService; + AddressId = addressId; + CustomerId = customerId; + _messenger = messenger; + } + + public async Task GetAddressAsync() + { + Address = await _addressService.GetCustomerAddressForAdminAsync(AddressId, CustomerId); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerDetailsForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerDetailsForAdminViewModel.cs new file mode 100644 index 00000000..eabe8383 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerDetailsForAdminViewModel.cs @@ -0,0 +1,254 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.Enums; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerDetailsForAdminViewModel : ObservableObject +{ + [ObservableProperty] + private CustomerData _customer; + private readonly IAddressService _addressService; + private readonly ISaleService _saleService; + private readonly IMessenger _messenger; + + public ObservableCollection Addresses { get; } = []; + public ObservableCollection Orders { get; } = []; + + [ObservableProperty] + private List _saleStatuses; + + [ObservableProperty] + private AddressData? _selectedAddress; + + [ObservableProperty] + private SaleSummaryData? _selectedOrder; + + [ObservableProperty] + private int _addressPage = 1; + [ObservableProperty] + private int _addressPageSize = 10; + [ObservableProperty] + private int _addressTotalPages; + [ObservableProperty] + private bool _addressHasNextPage; + [ObservableProperty] + private bool _addressHasPreviousPage; + [ObservableProperty] + private bool _addressesIsLoading; + + [ObservableProperty] + private int _orderPage = 1; + [ObservableProperty] + private int _orderPageSize = 10; + [ObservableProperty] + private int _orderTotalPages; + [ObservableProperty] + private bool _orderHasNextPage; + [ObservableProperty] + private bool _orderHasPreviousPage; + [ObservableProperty] + private bool _ordersIsLoading; + + [ObservableProperty] + private decimal? _minTotalAmount; + [ObservableProperty] + private decimal? _maxTotalAmount; + [ObservableProperty] + private string? _status; + [ObservableProperty] + private SaleStatus? _selectedStatus; + + public string CustomerName { get; set; } + public DisplayCustomerDetailsForAdminViewModel( + CustomerData customer, + IAddressService addressService, + ISaleService saleService, + IMessenger messenger) + { + _customer = customer; + _messenger = messenger; + _addressService = addressService; + _saleService = saleService; + SaleStatuses = new List + { + SaleStatus.Pending, + SaleStatus.Processing, + SaleStatus.Shipped, + SaleStatus.Delivered, + SaleStatus.Canceled + }; + CustomerName = $"{_customer.FirstName} {_customer.LastName}"; + LoadAddressesCommand.Execute(null); + LoadOrdersCommand.Execute(null); + } + + + [RelayCommand] + private async Task NextAddressPageAsync() + { + if (!AddressHasNextPage) return; + AddressPage++; + await LoadAddressesAsync(); + } + + [RelayCommand] + private async Task PreviousAddressPage() + { + if (!AddressHasPreviousPage) return; + AddressPage--; + await LoadAddressesAsync(); + } + + + private async Task FetchOrdersAsync() + { + OrdersIsLoading = true; + + var queryParams = new SaleQueryParams + { + Page = OrderPage, + PageSize = OrderPageSize, + CustomerId = Customer.CustomerId, + MinTotalAmount = MinTotalAmount, + MaxTotalAmount = MaxTotalAmount, + Status = (SelectedStatus.HasValue) + ? SelectedStatus.Value.ToString() + : null + }; + + var result = await _saleService.GetSalesForAdminAsync(queryParams); + + if (result is not null) + { + Orders.Clear(); + + foreach (var order in result.Data) + { + Orders.Add(order); + } + + OrderTotalPages = result.TotalPages; + OrderHasNextPage = OrderPage < OrderTotalPages; + OrderHasPreviousPage = OrderPage > 1; + } + + OrdersIsLoading = false; + } + + [RelayCommand] + private async Task NextOrderPageAsync() + { + if (!OrderHasNextPage) return; + OrderPage++; + await FetchOrdersAsync(); + } + + [RelayCommand] + private async Task PreviousOrderPageAsync() + { + if (!OrderHasPreviousPage) return; + OrderPage--; + await FetchOrdersAsync(); + } + + [RelayCommand] + private async Task LoadAddressesAsync() + { + AddressesIsLoading = true; + + var queryParams = new AddressQueryParams + { + Page = AddressPage, + PageSize = AddressPageSize, + CustomerId = Customer.CustomerId, + }; + + var result = await _addressService.GetAddressesForCustomerAsync(queryParams); + + if (result is not null) + { + Addresses.Clear(); + + foreach (var address in result.Data) + { + Addresses.Add(address); + } + + AddressTotalPages = result.TotalPages; + AddressHasNextPage = AddressPage < AddressTotalPages; + AddressHasPreviousPage = AddressPage > 1; + } + + AddressesIsLoading = false; + } + + [RelayCommand] + private async Task LoadOrdersAsync() + { + OrderPage = 1; + await FetchOrdersAsync(); + } + + [RelayCommand] + private void ClearFilters() + { + MinTotalAmount = null; + MaxTotalAmount = null; + SelectedStatus = null; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new ViewCustomersForAdminMessage()); + } + + partial void OnSelectedAddressChanged(AddressData? value) + { + if (value is not null) + { + _messenger.Send(new DisplayCustomerAddressDetailForAdminMessage(value.Id, Customer.CustomerId)); + } + } + + partial void OnSelectedOrderChanged(SaleSummaryData? value) + { + if (value is not null) + { + _messenger.Send(new AdminSelectedCustomerOrderForDetailMessage(value.Id, Customer)); + } + } + + async partial void OnMinTotalAmountChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(OrderPage, FetchOrdersAsync); + async partial void OnMaxTotalAmountChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(OrderPage, FetchOrdersAsync); + + async partial void OnSelectedStatusChanged(SaleStatus? value) + { + OrderPage = 1; + await FetchOrdersAsync(); + } + + private async Task OnFilterChanged() + { + await Task.Delay(500); + OrderPage = 1; + await FetchOrdersAsync(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailForAdminViewModel.cs new file mode 100644 index 00000000..d4b4f3d0 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailForAdminViewModel.cs @@ -0,0 +1,157 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.SaleDTOs; +using ECommerce.Shared.TerrenceLGee.Enums; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerOrderDetailForAdminViewModel : ObservableObject +{ + public int SaleId { get; } + private readonly CustomerData _customer; + [ObservableProperty] + private SaleData? _sale; + + public ObservableCollection OrderItems { get; } = []; + private readonly ISaleService _saleService; + private readonly IMessenger _messenger; + + [ObservableProperty] + private List _orderItemsForDisplay; + + [ObservableProperty] + private SaleProductData? _selectedItem; + + + [ObservableProperty] + private List _statuses; + [ObservableProperty] + private SaleStatus _selectedStatus; + + public DisplayCustomerOrderDetailForAdminViewModel(ISaleService saleService, int saleId, CustomerData customer, IMessenger messenger) + { + _saleService = saleService; + SaleId = saleId; + _customer = customer; + _messenger = messenger; + _orderItemsForDisplay = new List(); + _statuses = new List() + { + SaleStatus.Pending, + SaleStatus.Processing, + SaleStatus.Shipped, + SaleStatus.Delivered, + SaleStatus.Canceled + }; + } + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + + public async Task GetSaleAsync() + { + Sale = await _saleService.GetSaleForAdminAsync(SaleId); + if (Sale is null) return; + LoadOrderItems(Sale.SaleProducts); + FetchOrderItems(); + } + + private void LoadOrderItems(List items) + { + foreach (var item in items) + { + OrderItems.Add(item); + } + + FetchOrderItems(); + } + + [RelayCommand] + private void NextPage() + { + if (!HasNextPage) return; + Page++; + FetchOrderItems(); + } + + [RelayCommand] + private void PreviousPage() + { + if (!HasPreviousPage) return; + Page--; + FetchOrderItems(); + } + + [RelayCommand] + private void FetchOrderItems() + { + var pagedItems = OrderItems.Skip((Page - 1) * PageSize) + .Take(PageSize) + .ToList(); + + OrderItemsForDisplay = pagedItems; + + TotalPages = (int)Math.Ceiling(OrderItems.Count / (double)PageSize); + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToCustomerDetailsMessage(_customer)); + } + + [RelayCommand] + private async Task UpdateOrderAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + var updateSaleStatus = new UpdateSaleStatusDto { Status = SelectedStatus }; + + var (success, data) = await _saleService.AdminUpdateSaleStatusAsync(SaleId, updateSaleStatus); + + if (success) + { + SuccessMessage = data; + Sale = await _saleService.GetSaleForAdminAsync(SaleId); + _messenger.Send(new AdminUpdatedCustomerOrderMessage(_customer)); + } + else + { + ErrorMessage = data; + } + } + + partial void OnSelectedItemChanged(SaleProductData? value) + { + if (value is not null) + { + _messenger.Send(new ViewCustomerSaleProductDetailForAdminMessage(value.ProductId)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailViewModel.cs new file mode 100644 index 00000000..b341ba15 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerOrderDetailViewModel.cs @@ -0,0 +1,145 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerOrderDetailViewModel : ObservableObject +{ + public int SaleId { get; } + [ObservableProperty] + private SaleData? _sale; + + public ObservableCollection OrderItems { get; } = []; + private readonly ISaleService _saleService; + private readonly IMessenger _messenger; + + [ObservableProperty] + private List _orderItemsForDisplay; + + [ObservableProperty] + private SaleProductData? _selectedItem; + + public DisplayCustomerOrderDetailViewModel(ISaleService saleService, int saleId, IMessenger messenger) + { + _saleService = saleService; + SaleId = saleId; + _messenger = messenger; + _orderItemsForDisplay = new List(); + } + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + + public async Task GetSaleAsync() + { + Sale = await _saleService.GetSaleForCustomerAsync(SaleId); + if (Sale is null) return; + LoadOrderItems(Sale.SaleProducts); + } + + private void LoadOrderItems(List items) + { + foreach (var item in items) + { + OrderItems.Add(item); + } + FetchOrderItems(); + } + + [RelayCommand] + private void NextPage() + { + if (!HasNextPage) return; + Page++; + FetchOrderItems(); + } + + [RelayCommand] + private void PreviousPage() + { + if (!HasPreviousPage) return; + Page--; + FetchOrderItems(); + } + + [RelayCommand] + private void FetchOrderItems() + { + var pagedItems = OrderItems.Skip((Page - 1) * PageSize) + .Take(PageSize) + .ToList(); + + OrderItemsForDisplay = pagedItems; + + TotalPages = (int)Math.Ceiling(OrderItems.Count / (double)PageSize); + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new DisplayCustomerProfileMessage()); + } + + [RelayCommand] + private async Task CancelOrderAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + var box = MessageBoxManager + .GetMessageBoxStandard("Cancel", $"Cancel Order?", ButtonEnum.YesNo, Icon.Question, + null, WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + var (success, data) = await _saleService.CustomerCancelSaleAsync(SaleId); + + if (success) + { + SuccessMessage = data; + Sale = await _saleService.GetSaleForCustomerAsync(SaleId); + } + else + { + ErrorMessage = data; + } + } + } + + partial void OnSelectedItemChanged(SaleProductData? value) + { + if (value is not null) + { + _messenger.Send(new SaleProductSelectedForCustomerDetailMessage(value.ProductId)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProductViewModel.cs new file mode 100644 index 00000000..6f8a5097 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProductViewModel.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerProductViewModel : ObservableObject +{ + private readonly int _productId; + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + [ObservableProperty] + private ProductData? _product; + + public DisplayCustomerProductViewModel(IProductService productService, int productId, IMessenger messenger) + { + _productService = productService; + _productId = productId; + _messenger = messenger; + } + + public async Task GetProductAsync() + { + Product = await _productService.GetProductAsync(_productId); + if (Product is null) GoBack(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProfileViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProfileViewModel.cs new file mode 100644 index 00000000..d8265b8d --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayCustomerProfileViewModel.cs @@ -0,0 +1,257 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.Enums; +using ECommerce.Shared.TerrenceLGee.Parameters.AddressParameters; +using ECommerce.Shared.TerrenceLGee.Parameters.SaleParameters; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayCustomerProfileViewModel : ObservableObject +{ + private readonly ICustomerService _customerService; + private readonly IAddressService _addressService; + private readonly ISaleService _saleService; + private readonly IMessenger _messenger; + + public ObservableCollection Addresses { get; } = []; + public ObservableCollection Orders { get; } = []; + + [ObservableProperty] + private List _saleStatuses; + + [ObservableProperty] + private SaleSummaryData? _selectedOrder; + + [ObservableProperty] + private AddressData? _selectedAddress; + + [ObservableProperty] + private CustomerData? _profile; + + [ObservableProperty] + private int _addressPage = 1; + [ObservableProperty] + private int _addressPageSize = 10; + [ObservableProperty] + private int _addressTotalPages; + [ObservableProperty] + private bool _addressHasNextPage; + [ObservableProperty] + private bool _addressHasPreviousPage; + [ObservableProperty] + private bool _addressIsLoading; + + [ObservableProperty] + private int _orderPage = 1; + [ObservableProperty] + private int _orderPageSize = 10; + [ObservableProperty] + private int _orderTotalPages; + [ObservableProperty] + private bool _orderHasNextPage; + [ObservableProperty] + private bool _orderHasPreviousPage; + [ObservableProperty] + private bool _orderIsLoading; + [ObservableProperty] + private decimal? _minTotalAmount; + [ObservableProperty] + private decimal? _maxTotalAmount; + [ObservableProperty] + private string? _status; + [ObservableProperty] + private SaleStatus? _selectedStatus; + + public DisplayCustomerProfileViewModel( + ICustomerService customerService, + IAddressService addressService, + ISaleService saleService, + IMessenger messenger) + { + _customerService = customerService; + _addressService = addressService; + _saleService = saleService; + _messenger = messenger; + SaleStatuses = new List + { + SaleStatus.Pending, + SaleStatus.Processing, + SaleStatus.Shipped, + SaleStatus.Delivered, + SaleStatus.Canceled + }; + } + + + public async Task GetProfileAsync() + { + Profile = await _customerService.GetCustomerProfileAsync(); + + if (Profile is not null) + { + await LoadAddressesAsync(); + await LoadOrdersAsync(); + } + } + + + [RelayCommand] + private async Task NextAddressPageAsync() + { + if (!AddressHasNextPage) return; + AddressPage++; + await LoadAddressesAsync(); + } + + [RelayCommand] + private async Task PreviousAddressPage() + { + if (!AddressHasPreviousPage) return; + AddressPage--; + await LoadAddressesAsync(); + } + + [RelayCommand] + private async Task FetchOrdersAsync() + { + OrderIsLoading = true; + + var queryParams = new SaleQueryParams + { + Page = OrderPage, + PageSize = OrderPageSize, + MinTotalAmount = MinTotalAmount, + MaxTotalAmount = MaxTotalAmount, + Status = (SelectedStatus.HasValue) + ? SelectedStatus.Value.ToString() + : null + }; + + var result = await _saleService.GetSalesForCustomerAsync(queryParams); + + if (result is not null) + { + Orders.Clear(); + + foreach (var order in result.Data) + { + Orders.Add(order); + } + + OrderTotalPages = result.TotalPages; + OrderHasNextPage = OrderPage < OrderTotalPages; + OrderHasPreviousPage = OrderPage > 1; + } + + OrderIsLoading = false; + } + + [RelayCommand] + private async Task NextOrderPageAsync() + { + if (!OrderHasNextPage) return; + OrderPage++; + await FetchOrdersAsync(); + } + + [RelayCommand] + private async Task PreviousOrderPage() + { + if (!OrderHasPreviousPage) return; + OrderPage--; + await FetchOrdersAsync(); + } + + [RelayCommand] + private async Task LoadAddressesAsync() + { + AddressIsLoading = true; + + var queryParams = new AddressQueryParams + { + Page = AddressPage, + PageSize = AddressPageSize + }; + + var result = await _addressService.GetAddressesForCustomerAsync(queryParams); + + if (result is not null) + { + Addresses.Clear(); + + foreach (var address in result.Data) + { + Addresses.Add(address); + } + + AddressTotalPages = result.TotalPages; + AddressHasNextPage = AddressPage < AddressTotalPages; + AddressHasPreviousPage = AddressPage > 1; + } + + AddressIsLoading = false; + } + + + [RelayCommand] + private async Task LoadOrdersAsync() + { + OrderPage = 1; + await FetchOrdersAsync(); + } + + [RelayCommand] + private void ClearFilters() + { + MinTotalAmount = null; + MaxTotalAmount = null; + SelectedStatus = null; + } + + [RelayCommand] + private void AddAddress() + { + _messenger.Send(new AddAddressMessage()); + } + + async partial void OnMinTotalAmountChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(OrderPage, LoadOrdersAsync); + + async partial void OnMaxTotalAmountChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(OrderPage, LoadOrdersAsync); + + async partial void OnSelectedStatusChanged(SaleStatus? value) + { + OrderPage = 1; + await FetchOrdersAsync(); + } + + partial void OnSelectedOrderChanged(SaleSummaryData? value) + { + if (value is not null) + { + _messenger.Send(new SaleSelectedForCustomerDetailMessage(value.Id)); + } + } + + partial void OnSelectedAddressChanged(AddressData? value) + { + if (value is not null) + { + _messenger.Send(new AddressSelectedForDetailMessage(value.Id)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayOrderDetailsViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayOrderDetailsViewModel.cs new file mode 100644 index 00000000..e59da19a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayOrderDetailsViewModel.cs @@ -0,0 +1,87 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Sale; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayOrderDetailsViewModel : ViewModelBase +{ + [ObservableProperty] + private SaleData _sale; + public ObservableCollection OrderItems { get; } = []; + + [ObservableProperty] + private List _orderItemsForDisplay; + + private readonly IMessenger _messenger; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + + public DisplayOrderDetailsViewModel(SaleData sale, IMessenger messenger) + { + _sale = sale; + _messenger = messenger; + _orderItemsForDisplay = new List(); + LoadOrderItems(sale.SaleProducts); + } + + private void LoadOrderItems(List items) + { + foreach (var item in items) + { + OrderItems.Add(item); + } + FetchOrderItems(); + } + + [RelayCommand] + private void FetchOrderItems() + { + var pagedItems = OrderItems.Skip((Page - 1) * PageSize) + .Take(PageSize) + .ToList(); + + OrderItemsForDisplay = pagedItems; + + TotalPages = (int)Math.Ceiling(OrderItems.Count / (double)PageSize); + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + [RelayCommand] + private void NextPage() + { + if (!HasNextPage) return; + Page++; + FetchOrderItems(); + } + + [RelayCommand] + private void PreviousPage() + { + if (!HasPreviousPage) return; + Page--; + FetchOrderItems(); + } + + [RelayCommand] + private void ShopAgain() + { + _messenger.Send(new ShopAgainMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayProductDetailForSaleViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayProductDetailForSaleViewModel.cs new file mode 100644 index 00000000..16cbcf58 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayProductDetailForSaleViewModel.cs @@ -0,0 +1,132 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayProductDetailForSaleViewModel : ObservableObject +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + private readonly int _categoryId; + + [ObservableProperty] + private ProductData _product; + [ObservableProperty] + private static List _shoppingCart; + [ObservableProperty] + private decimal _quantity = 1; + [ObservableProperty] + public decimal _maxQuantity = 5000; + + + + public DisplayProductDetailForSaleViewModel( + IProductService productService, + List shoppingCart, + ProductData product, + int categoryId, + IMessenger messenger) + { + _productService = productService; + _product = product; + _shoppingCart = shoppingCart; + _categoryId = categoryId; + _messenger = messenger; + } + + [RelayCommand] + private async Task AddToCartAsync() + { + var item = ShoppingCart + .FirstOrDefault(ci => ci.ProductId == Product.Id); + + if (item is null) + { + ShoppingCart.Add(new CartItemDto + { + ProductId = + Product.Id, + Quantity = (int)Quantity, + ProductName = Product.Name, + TotalAmount = ((int)Quantity * Product.UnitPrice), + ProductPrice = Product.UnitPrice + }); + } + else + { + ShoppingCart.Remove(item); + var quantity = item.Quantity + (int)Quantity; + var id = item.ProductId; + var name = item.ProductName; + var totalPrice = quantity * Product.UnitPrice; + ShoppingCart.Add(new CartItemDto + { + ProductId = id, + Quantity = quantity, + ProductName = name, + TotalAmount = totalPrice, + ProductPrice = item.ProductPrice + }); + } + + var box = MessageBoxManager + .GetMessageBoxStandard("Added", "Item Added To Cart", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + + await box.ShowAsync(); + } + + [RelayCommand] + private async Task RemoveFromCartAsync() + { + var itemToRemove = ShoppingCart.Where(ci => ci.ProductId == Product.Id && ci.Quantity == (int)Quantity) + .FirstOrDefault(); + + if (itemToRemove is not null) + { + ShoppingCart.Remove(itemToRemove); + var box = MessageBoxManager + .GetMessageBoxStandard("Removed", "Item Removed From Cart", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + + await box.ShowAsync(); + } + else + { + var box = MessageBoxManager + .GetMessageBoxStandard("Error", "Unable To Remove Item From Cart", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + + await box.ShowAsync(); + } + } + + [RelayCommand] + private void GoBackToProducts() + { + _messenger.Send(new NavigateBackToProductsFromSelectedProductMessage(_categoryId, ShoppingCart)); + } + + [RelayCommand] + private void ViewCart() + { + _messenger.Send(new ViewCartMessage(ShoppingCart)); + } + + [RelayCommand] + private void Checkout() + { + _messenger.Send(new CheckoutMessage(ShoppingCart)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplaySelectedProductFromAdminCategoryDetailViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplaySelectedProductFromAdminCategoryDetailViewModel.cs new file mode 100644 index 00000000..00a9493a --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplaySelectedProductFromAdminCategoryDetailViewModel.cs @@ -0,0 +1,44 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplaySelectedProductFromAdminCategoryDetailViewModel : ObservableObject +{ + public int ProductId { get; } + public int CategoryId { get; } + + [ObservableProperty] + public ProductAdminData? _product; + + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public DisplaySelectedProductFromAdminCategoryDetailViewModel( + IProductService productService, + int productId, + int categoryId, + IMessenger messenger) + { + _productService = productService; + ProductId = productId; + CategoryId = categoryId; + _messenger = messenger; + } + + public async Task GetProductAsync() + { + Product = await _productService.GetProductForAdminAsync(ProductId); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToAdminCategoryDetailView(CategoryId)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedAddressViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedAddressViewModel.cs new file mode 100644 index 00000000..efe2155b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedAddressViewModel.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayUpdatedAddressViewModel : ViewModelBase +{ + private readonly IMessenger _messenger; + public AddressData Address { get; } + + public DisplayUpdatedAddressViewModel(AddressData address, IMessenger messenger) + { + Address = address; + _messenger = messenger; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToUpdateAddressMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedCategoryViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedCategoryViewModel.cs new file mode 100644 index 00000000..4919dafd --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedCategoryViewModel.cs @@ -0,0 +1,31 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using System; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayUpdatedCategoryViewModel : ViewModelBase +{ + public CategoryAdminData Category { get; } + private readonly IMessenger _messenger; + + public DisplayUpdatedCategoryViewModel(CategoryAdminData category, IMessenger messenger) + { + Category = category; + _messenger = messenger; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToViewCategoriesForUpdateCategoryMessage()); + } + + [RelayCommand] + private void GoBackToCategoryPage() + { + _messenger.Send(new NavigateBackToCategoryPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedProductViewModel.cs new file mode 100644 index 00000000..aa81811f --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/DisplayUpdatedProductViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class DisplayUpdatedProductViewModel : ViewModelBase +{ + public ProductAdminData Product { get; } + private readonly IMessenger _messenger; + + public DisplayUpdatedProductViewModel(ProductAdminData product, IMessenger messenger) + { + Product = product; + _messenger = messenger; + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToUpdateProductMessage()); + } + + [RelayCommand] + private void GoBackToProductPage() + { + _messenger.Send(new NavigateBackToProductPageMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/HomeScreenViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/HomeScreenViewModel.cs new file mode 100644 index 00000000..44561f29 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/HomeScreenViewModel.cs @@ -0,0 +1,5 @@ +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public class HomeScreenViewModel : ViewModelBase +{ +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/LoginViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/LoginViewModel.cs new file mode 100644 index 00000000..2c0feb57 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/LoginViewModel.cs @@ -0,0 +1,99 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class LoginViewModel : ObservableValidator +{ + private readonly IAuthService _authService; + public event Action? LoginSuccessful; + public event Action? BackRequested; + + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Email address is required.")] + [NotifyPropertyChangedFor(nameof(EmailErrors))] + private string _email; + + public string? EmailErrors => GetErrors(nameof(Email)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Password is required.")] + [NotifyPropertyChangedFor(nameof(PasswordErrors))] + private string _password; + + public string? PasswordErrors => GetErrors(nameof(Password)) + .FirstOrDefault()?.ErrorMessage; + + public LoginViewModel(IAuthService authService) + { + _authService = authService; + } + + [RelayCommand] + public async Task LoginAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Email, nameof(Email)); + ValidateProperty(Password, nameof(Password)); + + if (HasErrors) + { + return; + } + + var login = new UserLoginDto + { + Email = Email, + Password = Password + }; + + var (success, data) = await _authService.LoginUserAsync(login); + + if (success) + { + ClearLogin(); + SuccessMessage = "Login successful"; + + var isAdmin = (data is not null) + ? data.Roles.Contains("admin") + : false; + + LoginSuccessful?.Invoke(isAdmin); + } + else + { + ClearLogin(); + ErrorMessage = (data is not null) + ? data.ErrorMessage + : "Unexpected error occurred while attempting to login"; + } + } + + [RelayCommand] + private void GoBack() + { + BackRequested?.Invoke(); + } + + private void ClearLogin() + { + Email = string.Empty; + Password = string.Empty; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainUserViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainUserViewModel.cs new file mode 100644 index 00000000..4ad39de6 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainUserViewModel.cs @@ -0,0 +1,475 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Enums; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.Enums.Extensions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class MainUserViewModel : ObservableObject +{ + private readonly IServiceProvider _serviceProvider; + private readonly IMessenger _messenger; + private readonly IAuthService _authService; + public ObservableCollection MenuItems { get; } = []; + + [ObservableProperty] + private ObservableObject? _currentSubView; + + [ObservableProperty] + private ObservableObject? _previousSubView; + + [ObservableProperty] + private MenuItemViewModel? _selectedMenuItem; + + public event Action? LogoutRequested; + + public MainUserViewModel(bool IsAdmin, IServiceProvider serviceProvider, IAuthService authService, IMessenger messenger) + { + _serviceProvider = serviceProvider; + _authService = authService; + _messenger = messenger; + + if (IsAdmin) + { + var adminMenuItems = Enum.GetValues() + .Select(e => new MenuItemViewModel(e.GetDisplayName(), e)); + + foreach (var item in adminMenuItems) + { + MenuItems.Add(item); + } + } + else + { + var customerMenuItems = Enum.GetValues() + .Select(e => new MenuItemViewModel(e.GetDisplayName(), e)); + + foreach (var item in customerMenuItems) + { + MenuItems.Add(item); + } + } + + CurrentSubView = _serviceProvider.GetRequiredService(); + + MessageRegistration(); + } + + partial void OnSelectedMenuItemChanged(MenuItemViewModel? value) + { + if (value is null) return; + + switch (value.Value) + { + case AdminMenu.Categories: + CurrentSubView = _serviceProvider.GetRequiredService(); + break; + case AdminMenu.Products: + CurrentSubView = _serviceProvider.GetRequiredService(); + break; + case AdminMenu.Customers: + CurrentSubView = _serviceProvider.GetRequiredService(); + break; + case CustomerMenu.ViewProfile: + _messenger.Send(new DisplayCustomerProfileMessage()); + break; + case CustomerMenu.AddSale: + CurrentSubView = _serviceProvider.GetRequiredService(); + break; + case AdminMenu.Logout: + case CustomerMenu.Logout: + _authService.LogoutUserAsync(); + _messenger.UnregisterAll(this); + LogoutRequested?.Invoke(); + break; + } + } + + private void MessageRegistration() + { + CategoryMessageRegistration(); + ProductMessageRegistration(); + AddressMessageRegistration(); + SaleMessageRegistration(); + CustomerMessageRegistration(); + OtherMessageRegistration(); + } + + private void CategoryMessageRegistration() + { + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var categoryService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAddedCategoryViewModel(m.Data, _messenger); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = new DisplayUpdatedCategoryViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + var categoryService = _serviceProvider.GetRequiredService(); + CurrentSubView = new UpdateCategoryViewModel(categoryService, m.Data, _messenger); + }); + + _messenger.Register(this, async (r, m) => + { + var categoryService = _serviceProvider.GetRequiredService(); + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAdminCategoryDetailViewModel(categoryService, productService, m.CategoryId, _messenger); + await detailVM.GetCategoryAsync(); + CurrentSubView = detailVM; + }); + + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, async (r, m) => + { + var categoryService = _serviceProvider.GetRequiredService(); + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAdminCategoryDetailViewModel(categoryService, productService, m.CategoryId, _messenger); + await detailVM.GetCategoryAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + } + + private void ProductMessageRegistration() + { + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = new DisplayAddedProductViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + var productService = _serviceProvider.GetRequiredService(); + CurrentSubView = new UpdateProductViewModel(productService, m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = new DisplayUpdatedProductViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAdminProductViewModel(productService, m.ProductId, _messenger); + await detailVM.GetProductAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, async (r, m) => + { + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplaySelectedProductFromAdminCategoryDetailViewModel(productService, m.ProductId, m.CategoryId, _messenger); + await detailVM.GetProductAsync(); + CurrentSubView = detailVM; + }); + } + + private void AddressMessageRegistration() + { + _messenger.Register(this, async (r, m) => + { + var addressService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayCustomerAddressForAdminViewModel(addressService, m.AddressId, m.CustomerId, _messenger); + await detailVM.GetAddressAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = new DisplayAddedAddressViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + var addressService = _serviceProvider.GetRequiredService(); + CurrentSubView = new UpdateAddressViewModel(addressService, m.Data, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = new DisplayUpdatedAddressViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var addressService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAddressViewModel(addressService, m.AddressId, _messenger); + await detailVM.GetAddressAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + } + + private void SaleMessageRegistration() + { + _messenger.Register(this, (r, m) => + { + var productService = _serviceProvider.GetRequiredService(); + CurrentSubView = new ViewProductsForSaleViewModel(productService, m.CategoryId, m.ShoppingCart, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + var productService = _serviceProvider.GetRequiredService(); + CurrentSubView = new ViewProductsForSaleViewModel(productService, m.CategoryId, m.ShoppingCart, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + var productService = _serviceProvider.GetRequiredService(); + CurrentSubView = new DisplayProductDetailForSaleViewModel(productService, m.ShoppingCart, m.Data, m.CategoryId, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + var saleService = _serviceProvider.GetRequiredService(); + CurrentSubView = new CheckoutViewModel(saleService, m.ShoppingCart, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = new DisplayOrderDetailsViewModel(m.Data, _messenger); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var saleService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayCustomerOrderDetailViewModel(saleService, m.SaleId, _messenger); + await detailVM.GetSaleAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = new ViewCartViewModel(m.ShoppingCart, _messenger); + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = PreviousSubView; + }); + } + + private void CustomerMessageRegistration() + { + _messenger.Register(this, async (r, m) => + { + var customerService = _serviceProvider.GetRequiredService(); + var addressService = _serviceProvider.GetRequiredService(); + var saleService = _serviceProvider.GetRequiredService(); + var profileVM = new DisplayCustomerProfileViewModel(customerService, addressService, saleService, _messenger); + await profileVM.GetProfileAsync(); + CurrentSubView = profileVM; + }); + + _messenger.Register(this, (r, m) => + { + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + CurrentSubView = _serviceProvider.GetRequiredService(); + }); + + _messenger.Register(this, (r, m) => + { + PreviousSubView = CurrentSubView; + var addressService = _serviceProvider.GetRequiredService(); + var saleService = _serviceProvider.GetRequiredService(); + CurrentSubView = new DisplayCustomerDetailsForAdminViewModel(m.Data, addressService, saleService, _messenger); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var saleService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayCustomerOrderDetailForAdminViewModel(saleService, m.SaleId, m.Data, _messenger); + await detailVM.GetSaleAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayAdminProductViewModel(productService, m.ProductId, _messenger); + await detailVM.GetProductAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, async (r, m) => + { + var addressService = _serviceProvider.GetRequiredService(); + var saleService = _serviceProvider.GetRequiredService(); + CurrentSubView = new DisplayCustomerDetailsForAdminViewModel(m.Data, addressService, saleService, _messenger); + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var addressService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayCustomerAddressForAdminViewModel(addressService, m.AddressId, m.customerId, _messenger); + await detailVM.GetAddressAsync(); + CurrentSubView = detailVM; + }); + + _messenger.Register(this, async (r, m) => + { + PreviousSubView = CurrentSubView; + var productService = _serviceProvider.GetRequiredService(); + var detailVM = new DisplayCustomerProductViewModel(productService, m.ProductId, _messenger); + await detailVM.GetProductAsync(); + CurrentSubView = detailVM; + }); + } + + private void OtherMessageRegistration() + { + _messenger.Register(this, (r, m) => + { + CurrentSubView = PreviousSubView; + }); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainWindowViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..d1542c31 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,9 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class MainWindowViewModel : ObservableObject +{ + [ObservableProperty] + private object? _currentView; +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MenuItemViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MenuItemViewModel.cs new file mode 100644 index 00000000..4142052e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/MenuItemViewModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public class MenuItemViewModel +{ + public string DisplayName { get; set; } = string.Empty; + public Enum Value { get; set; } + + public MenuItemViewModel(string displayName, Enum value) + { + DisplayName = displayName; + Value = value; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/PasswordResetViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/PasswordResetViewModel.cs new file mode 100644 index 00000000..29466cc7 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/PasswordResetViewModel.cs @@ -0,0 +1,129 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class PasswordResetViewModel : ObservableValidator +{ + private readonly IAuthService _authService; + public event Action? BackRequested; + public event Action? LoginRequested; + + public PasswordResetViewModel(IAuthService authService) + { + _authService = authService; + } + + [ObservableProperty] + [Required(ErrorMessage = "Email address is required.")] + [NotifyPropertyChangedFor(nameof(EmailErrors))] + private string _email; + + public string? EmailErrors => GetErrors(nameof(Email)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Old password is required.")] + [NotifyPropertyChangedFor(nameof(OldPasswordErrors))] + private string _oldPassword; + + public string? OldPasswordErrors => GetErrors(nameof(OldPassword)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "New password is required.")] + [NotifyPropertyChangedFor(nameof(NewPasswordErrors))] + private string _newPassword; + + public string? NewPasswordErrors => GetErrors(nameof(NewPassword)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Confirmation of new password is required.")] + [CustomValidation(typeof(PasswordResetViewModel), nameof(ValidatePasswordResetConfirmation))] + [NotifyPropertyChangedFor(nameof(ResetConfirmPasswordErrors))] + private string _resetConfirmPassword; + + public string? ResetConfirmPasswordErrors => GetErrors(nameof(ResetConfirmPassword)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + private async Task ResetPassword() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Email, nameof(Email)); + ValidateProperty(OldPassword, nameof(OldPassword)); + ValidateProperty(NewPassword, nameof(NewPassword)); + ValidateProperty(ResetConfirmPassword, nameof(ResetConfirmPassword)); + + if (HasErrors) + { + return; + } + + var reset = new UserResetPasswordDto + { + Email = Email, + OldPassword = OldPassword, + NewPassword = NewPassword, + ConfirmPassword = ResetConfirmPassword + }; + + var (success, message) = await _authService.ResetUserPasswordAsync(reset); + + if (success) + { + SuccessMessage = message; + ClearPasswordReset(); + LoginRequested?.Invoke(); + } + else + { + ErrorMessage = message; + ClearPasswordReset(); + } + } + + [RelayCommand] + private void GoBack() + { + BackRequested?.Invoke(); + } + + public static ValidationResult? ValidatePasswordResetConfirmation(string confirmPassword, ValidationContext context) + { + var viewModel = (PasswordResetViewModel)context.ObjectInstance; + + var password = viewModel.NewPassword; + + if (!password.Equals(confirmPassword)) + { + return new ValidationResult("Passwords do not match."); + } + + return ValidationResult.Success; + } + + private void ClearPasswordReset() + { + Email = string.Empty; + OldPassword = string.Empty; + NewPassword = string.Empty; + ResetConfirmPassword = string.Empty; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductOperationsViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductOperationsViewModel.cs new file mode 100644 index 00000000..c2a3e025 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductOperationsViewModel.cs @@ -0,0 +1,45 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ProductOperationsViewModel : ViewModelBase +{ + private readonly IMessenger _messenger; + + public ProductOperationsViewModel(IMessenger messenger) + { + _messenger = messenger; + } + + [RelayCommand] + private void AddProduct() + { + _messenger.Send(new AddProductMessage()); + } + + [RelayCommand] + private void UpdateProduct() + { + _messenger.Send(new UpdateProductMessage()); + } + + [RelayCommand] + private void DeleteProduct() + { + _messenger.Send(new DeleteProductMessage()); + } + + [RelayCommand] + private void RestoreProduct() + { + _messenger.Send(new RestoreProductMessage()); + } + + [RelayCommand] + private void ViewProducts() + { + _messenger.Send(new NavigateBackToAllAdminProductsMessage()); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductsAdminBaseViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductsAdminBaseViewModel.cs new file mode 100644 index 00000000..8f284f4e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ProductsAdminBaseViewModel.cs @@ -0,0 +1,148 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public abstract partial class ProductsAdminBaseViewModel : ObservableValidator +{ + public ObservableCollection Products { get; } = []; + + [ObservableProperty] + private ProductAdminData? _selectedProduct; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private decimal? _minUnitPrice; + [ObservableProperty] + private decimal? _maxUnitPrice; + [ObservableProperty] + private int? _minStockQuantity; + [ObservableProperty] + private int? _maxStockQuantity; + [ObservableProperty] + private int? _minDiscountPercentage; + [ObservableProperty] + private int? _maxDiscountPercentage; + [ObservableProperty] + private string? _categoryName; + [ObservableProperty] + private string? _description; + [ObservableProperty] + private bool? _inStock; + [ObservableProperty] + private bool? _isDeleted; + + private async Task FetchProductsAsync() + { + IsLoading = true; + + var result = await GetProductsAsync(); + + if (result is not null) + { + Products.Clear(); + + foreach (var product in result.Data) + { + Products.Add(product); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + protected async Task LoadProductsAsync() + { + Page = 1; + await FetchProductsAsync(); + } + + [RelayCommand] + protected async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchProductsAsync(); + } + + [RelayCommand] + protected async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchProductsAsync(); + } + + [RelayCommand] + protected async Task ClearFiltersAsync() + { + MinUnitPrice = null; + MaxUnitPrice = null; + MinStockQuantity = null; + MaxStockQuantity = null; + MinDiscountPercentage = null; + MaxDiscountPercentage = null; + CategoryName = null; + Description = null; + IsDeleted = false; + } + + protected abstract Task GetProductsAsync(); + protected abstract void OnProductSelected(ProductAdminData product); + + partial void OnSelectedProductChanged(ProductAdminData? value) + { + if (value is not null) + { + OnProductSelected(value); + } + } + async partial void OnMinUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnCategoryNameChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnInStockChanged(bool? value) + { + Page = 1; + await FetchProductsAsync(); + } + + async partial void OnIsDeletedChanged(bool? value) + { + Page = 1; + await FetchProductsAsync(); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RegistrationViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RegistrationViewModel.cs new file mode 100644 index 00000000..a2698abd --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RegistrationViewModel.cs @@ -0,0 +1,256 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Auth; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using ECommerce.Shared.TerrenceLGee.DTOs.AuthDTOs; +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class RegistrationViewModel : ObservableValidator +{ + private readonly IAuthService _authService; + public event Action? BackRequested; + public event Action? RegistrationSuccessful; + + public RegistrationViewModel(IAuthService authService) + { + _authService = authService; + } + + [ObservableProperty] + [Required(ErrorMessage = "First name is required.")] + [MaxLength(25, ErrorMessage = "First name cannot be greater than 25 characters.")] + [NotifyPropertyChangedFor(nameof(FirstNameErrors))] + private string _firstName = string.Empty; + + public string? FirstNameErrors => GetErrors(nameof(FirstName)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Last name is required.")] + [MaxLength(25, ErrorMessage = "Last name cannot be greater than 25 characters.")] + [NotifyPropertyChangedFor(nameof(LastNameErrors))] + private string _lastName = string.Empty; + + public string? LastNameErrors => GetErrors(nameof(LastName)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Email is required.")] + [EmailAddress(ErrorMessage = "Please enter a valid email address.")] + [NotifyPropertyChangedFor(nameof(EmailErrors))] + private string _email = string.Empty; + + public string? EmailErrors => GetErrors(nameof(Email)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Date of birth is required.")] + [NotifyPropertyChangedFor(nameof(SelectedBirthDateErrors))] + private DateTimeOffset? _selectedBirthDate; + + public string? SelectedBirthDateErrors => GetErrors(nameof(SelectedBirthDate)) + .FirstOrDefault()?.ErrorMessage; + + public DateOnly? DateOfBirth => SelectedBirthDate.HasValue + ? DateOnly.FromDateTime(SelectedBirthDate.Value.Date) + : null; + + [ObservableProperty] + [Required(ErrorMessage = "Address Line 1 is required.")] + [MaxLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine1Errors))] + private string _addressLine1; + + public string? AddressLine1Errors => GetErrors(nameof(AddressLine1)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine2Errors))] + private string? _addressLine2; + + public string? AddressLine2Errors => GetErrors(nameof(AddressLine2)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "City is required.")] + [MaxLength(50, ErrorMessage = "City cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CityErrors))] + private string _city; + + public string? CityErrors => GetErrors(nameof(City)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "State is required.")] + [MaxLength(50, ErrorMessage = "State cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(StateErrors))] + private string _state; + + public string? StateErrors => GetErrors(nameof(State)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Postal code is required.")] + [RegularExpression(@"\d{4,6}$", ErrorMessage = "Invalid postal code.")] + [NotifyPropertyChangedFor(nameof(PostalCodeErrors))] + private string _postalCode; + + public string? PostalCodeErrors => GetErrors(nameof(PostalCode)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Country is required.")] + [MaxLength(50, ErrorMessage = "Country cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CountryErrors))] + private string _country; + + public string? CountryErrors => GetErrors(nameof(Country)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private bool _isBillingAddress; + [ObservableProperty] + private bool _isShippingAddress; + + [ObservableProperty] + [Required(ErrorMessage = "Password is required.")] + [MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")] + [NotifyPropertyChangedFor(nameof(PasswordErrors))] + private string _password = string.Empty; + + public string? PasswordErrors => GetErrors(nameof(Password)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "You must confirm your password.")] + [CustomValidation(typeof(RegistrationViewModel), nameof(ValidatePasswordConfirmation))] + [NotifyPropertyChangedFor(nameof(ConfirmPasswordErrors))] + private string _confirmPassword = string.Empty; + + public string? ConfirmPasswordErrors => GetErrors(nameof(ConfirmPassword)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private string? _successMessage; + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + private async Task RegisterAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(FirstName, nameof(FirstName)); + ValidateProperty(LastName, nameof(LastName)); + ValidateProperty(Email, nameof(Email)); + ValidateProperty(AddressLine1, nameof(AddressLine1)); + ValidateProperty(AddressLine2, nameof(AddressLine2)); + ValidateProperty(City, nameof(City)); + ValidateProperty(State, nameof(State)); + ValidateProperty(PostalCode, nameof(PostalCode)); + ValidateProperty(Country, nameof(Country)); + ValidateProperty(Password, nameof(Password)); + ValidateProperty(ConfirmPassword, nameof(ConfirmPassword)); + + if (HasErrors) + { + return; + } + + var customer = new UserRegistrationDto + { + FirstName = FirstName, + LastName = LastName, + Email = Email, + DateOfBirth = DateOfBirth!.Value, + BillingAddress = IsBillingAddress + ? new CreateAddressDto + { + AddressLine1 = AddressLine1, + AddressLine2 = AddressLine2, + City = City, + State = State, + PostalCode = PostalCode, + Country = Country, + IsBillingAddress = IsBillingAddress, + IsShippingAddress = IsShippingAddress + } + : null, + ShippingAddress = IsShippingAddress + ? new CreateAddressDto + { + AddressLine1 = AddressLine1, + AddressLine2 = AddressLine2, + City = City, + State = State, + PostalCode = PostalCode, + Country = Country, + IsBillingAddress = IsBillingAddress, + IsShippingAddress = IsShippingAddress + } + : null, + Password = Password + }; + + var (success, message) = await _authService.RegisterUserAsync(customer); + + if (success) + { + SuccessMessage = message; + ClearRegistration(); + RegistrationSuccessful?.Invoke(); + } + else + { + ErrorMessage = message; + ClearRegistration(); + } + } + + [RelayCommand] + private void GoBack() + { + BackRequested?.Invoke(); + } + + private void ClearRegistration() + { + FirstName = string.Empty; + LastName = string.Empty; + Email = string.Empty; + SelectedBirthDate = null; + AddressLine1 = string.Empty; + AddressLine2 = string.Empty; + City = string.Empty; + State = string.Empty; + PostalCode = string.Empty; + Country = string.Empty; + IsBillingAddress = false; + IsShippingAddress = false; + Password = string.Empty; + ConfirmPassword = string.Empty; + } + + public static ValidationResult? ValidatePasswordConfirmation(string confirmPassword, ValidationContext context) + { + var viewModel = (RegistrationViewModel)context.ObjectInstance; + + var password = viewModel.Password; + + if (!password.Equals(confirmPassword)) + { + return new ValidationResult("Passwords do not match."); + } + + return ValidationResult.Success; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RestoreProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RestoreProductViewModel.cs new file mode 100644 index 00000000..0d7b63af --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/RestoreProductViewModel.cs @@ -0,0 +1,98 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class RestoreProductViewModel : ProductsAdminBaseViewModel +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public RestoreProductViewModel(IProductService productService, IMessenger messenger) + { + _productService = productService; + LoadProductsCommand.Execute(null); + _messenger = messenger; + } + + + [RelayCommand] + private async Task GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + + private async Task RestoreProductAsync(ProductAdminData value) + { + if (value is not null) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Restore", $"Restore {value.Name}?", ButtonEnum.YesNo, Icon.Question, + null, WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + var (success, data) = await _productService.RestoreProductAsync(value.Id); + + if (success) + { + SelectedProduct = null; + box = MessageBoxManager + .GetMessageBoxStandard("Success", $"{data}", ButtonEnum.Ok, Icon.Success, + null, WindowStartupLocation.CenterOwner); + + result = await box.ShowAsync(); + } + else + { + SelectedProduct = null; + box = MessageBoxManager + .GetMessageBoxStandard("Error", $"{data}", ButtonEnum.Ok, Icon.Error, + null, WindowStartupLocation.CenterOwner); + + result = await box.ShowAsync(); + } + await LoadProductsAsync(); + } + } + } + + + protected override async Task GetProductsAsync() + { + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + CategoryName = CategoryName, + Description = Description, + InStock = InStock, + IsDeleted = IsDeleted + }; + + return await _productService.GetProductsForAdminAsync(queryParams); + } + + protected override void OnProductSelected(ProductAdminData product) + { + Dispatcher.UIThread.InvokeAsync( + () => RestoreProductAsync(product)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateAddressViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateAddressViewModel.cs new file mode 100644 index 00000000..5af8a4e8 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateAddressViewModel.cs @@ -0,0 +1,167 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Address; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.AddressMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.OtherMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Address; +using ECommerce.Shared.TerrenceLGee.DTOs.AddressDTOs; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class UpdateAddressViewModel : ObservableValidator +{ + private readonly IAddressService _addressService; + private readonly IMessenger _messenger; + + public AddressData Address; + + public UpdateAddressViewModel(IAddressService addressService, AddressData address, IMessenger messenger) + { + _addressService = addressService; + Address = address; + _messenger = messenger; + _addressLine1 = Address.AddressLine1; + _addressLine2 = Address.AddressLine2; + _city = Address.City; + _state = Address.State; + _postalCode = Address.PostalCode; + _country = Address.Country; + _isBillingAddress = Address.IsBillingAddress; + _isShippingAddress = Address.IsShippingAddress; + } + + [ObservableProperty] + [Required(ErrorMessage = "Address Line 1 is required.")] + [MaxLength(100, ErrorMessage = "Address Line 1 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine1Errors))] + private string _addressLine1; + + public string? AddressLine1Errors => GetErrors(nameof(AddressLine1)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(100, ErrorMessage = "Address Line 2 cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(AddressLine2Errors))] + private string? _addressLine2; + + public string? AddressLine2Errors => GetErrors(nameof(AddressLine2)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "City is required.")] + [MaxLength(50, ErrorMessage = "City cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CityErrors))] + public string _city; + + public string? CityErrors => GetErrors(nameof(City)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "State is required.")] + [MaxLength(50, ErrorMessage = "State cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(StateErrors))] + private string _state; + + public string? StateErrors => GetErrors(nameof(State)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Postal code is required.")] + [RegularExpression(@"\d{4,6}$", ErrorMessage = "Invalid postal code.")] + [NotifyPropertyChangedFor(nameof(PostalCodeErrors))] + private string _postalCode; + + public string? PostalCodeErrors => GetErrors(nameof(PostalCode)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Country is required.")] + [MaxLength(50, ErrorMessage = "Country cannot exceed 50 characters.")] + [NotifyPropertyChangedFor(nameof(CountryErrors))] + private string _country; + + public string? CountryErrors => GetErrors(nameof(Country)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private bool _isBillingAddress; + + [ObservableProperty] + private bool _isShippingAddress; + + [ObservableProperty] + private string? _successMessage; + + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + private async Task UpdateAddressAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(AddressLine1, nameof(AddressLine1)); + ValidateProperty(AddressLine2, nameof(AddressLine2)); + ValidateProperty(City, nameof(City)); + ValidateProperty(PostalCode, nameof(PostalCode)); + ValidateProperty(Country, nameof(Country)); + + if (HasErrors) + { + return; + } + + var address = new UpdateAddressDto + { + Id = Address.Id, + AddressLine1 = AddressLine1, + AddressLine2 = AddressLine2, + City = City, + State = State, + PostalCode = PostalCode, + Country = Country, + IsBillingAddress = IsBillingAddress, + IsShippingAddress = IsShippingAddress + }; + + var data = await _addressService.UpdateAddressAsync(address); + + if (data is null) + { + ErrorMessage = $"Unable to update address {Address.Id} at this time"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearAddressUpdate(); + SuccessMessage = $"Address {Address.Id} updated successfully"; + _messenger.Send(new AddressUpdatedMessage(data)); + } + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToPreviousPageMessage()); + } + + private void ClearAddressUpdate() + { + AddressLine1 = string.Empty; + AddressLine2 = string.Empty; + City = string.Empty; + State = string.Empty; + PostalCode = string.Empty; + Country = string.Empty; + IsBillingAddress = false; + IsShippingAddress = false; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateCategoryViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateCategoryViewModel.cs new file mode 100644 index 00000000..cbbeb19b --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateCategoryViewModel.cs @@ -0,0 +1,110 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.Shared.TerrenceLGee.DTOs.CategoryDTOs; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class UpdateCategoryViewModel : ObservableValidator +{ + private readonly ICategoryService _categoryService; + public CategoryAdminSummaryData Category { get; set; } + private readonly IMessenger _messenger; + + public UpdateCategoryViewModel( + ICategoryService categoryService, + CategoryAdminSummaryData category, + IMessenger messenger) + { + _categoryService = categoryService; + Category = category; + _messenger = messenger; + _name = Category.Name; + _description = Category.Description; + } + + [ObservableProperty] + [Required(ErrorMessage = "Category name is required.")] + [MaxLength(100, ErrorMessage = "Category name cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(NameErrors))] + private string _name; + + public string? NameErrors => GetErrors(nameof(Name)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(500, ErrorMessage = "Category description cannot exceed 500 characters.")] + [NotifyPropertyChangedFor(nameof(DescriptionErrors))] + private string? _description; + + public string? DescriptionErrors => GetErrors(nameof(Description)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + public string? _successMessage; + + [ObservableProperty] + public string? _errorMessage; + + [RelayCommand] + public async Task UpdateCategoryAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Name, nameof(Name)); + ValidateProperty(Description, nameof(Description)); + + if (HasErrors) + { + return; + } + + var category = new UpdateCategoryDto + { + Id = Category.Id, + Name = Name, + Description = Description + }; + + + var data = await _categoryService.UpdateCategoryAsync(category); + + if (data is null) + { + ErrorMessage = $"Unable to update category {Category.Id} at this time"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearCategoryUpdate(); + SuccessMessage = $"Category {Category.Id} updated successfully"; + _messenger.Send(new CategoryUpdatedMessage(data)); + } + else + { + ErrorMessage = data.ErrorMessage; + } + } + + [RelayCommand] + private async Task GoBack() + { + _messenger.Send(new NavigateBackToViewCategoriesForUpdateCategoryMessage()); + } + + private void ClearCategoryUpdate() + { + Name = string.Empty; + Description = string.Empty; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateProductViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateProductViewModel.cs new file mode 100644 index 00000000..5b4ffe20 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/UpdateProductViewModel.cs @@ -0,0 +1,159 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.ProductDTOs; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class UpdateProductViewModel : ObservableValidator +{ + private readonly IProductService _productService; + public ProductAdminData Product { get; set; } + private readonly IMessenger _messenger; + + public UpdateProductViewModel( + IProductService productService, + ProductAdminData product, + IMessenger messenger) + { + _productService = productService; + Product = product; + _messenger = messenger; + _name = Product.Name; + _description = Product.Description; + _stockQuantity = Product.StockQuantity; + _discountPercentage = Product.DiscountPercentage; + _isDeleted = Product.IsDeleted; + _isInStock = Product.IsInStock; + _imageUrl = Product.ImageUrl; + } + + [ObservableProperty] + [Required(ErrorMessage = "Product name is required.")] + [MaxLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")] + [NotifyPropertyChangedFor(nameof(NameErrors))] + private string _name; + + public string? NameErrors => GetErrors(nameof(Name)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [MaxLength(1000, ErrorMessage = "Product description cannot exceed 1000 characters.")] + [NotifyPropertyChangedFor(nameof(DescriptionErrors))] + private string? _description; + + public string? DescriptionErrors => GetErrors(nameof(Description)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Product stock quantity is required.")] + [Range(0, 5000, ErrorMessage = "Product stock quantity must be between 0 and the maximum capacity of our warehouse which is 5000.")] + [NotifyPropertyChangedFor(nameof(StockQuantityErrors))] + public int _stockQuantity; + + public string? StockQuantityErrors => GetErrors(nameof(StockQuantity)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Required(ErrorMessage = "Discount percentage is required.")] + [Range(0, 100, ErrorMessage = "Discount percentage must be between 0% and 100%.")] + [NotifyPropertyChangedFor(nameof(DiscountPercentageErrors))] + public int _discountPercentage; + + public string? DiscountPercentageErrors => GetErrors(nameof(DiscountPercentage)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + [Url] + [NotifyPropertyChangedFor(nameof(ImageUrlErrors))] + private string? _imageUrl; + + public string? ImageUrlErrors => GetErrors(nameof(ImageUrl)) + .FirstOrDefault()?.ErrorMessage; + + [ObservableProperty] + private bool _isDeleted; + + [ObservableProperty] + private bool _isInStock; + + [ObservableProperty] + private string? _successMessage; + + [ObservableProperty] + private string? _errorMessage; + + [RelayCommand] + private async Task UpdateProductAsync() + { + SuccessMessage = null; + ErrorMessage = null; + + ClearErrors(); + + ValidateProperty(Name, nameof(Name)); + ValidateProperty(Description, nameof(Description)); + ValidateProperty(StockQuantity, nameof(StockQuantity)); + ValidateProperty(DiscountPercentage, nameof(DiscountPercentage)); + ValidateProperty(ImageUrl, nameof(ImageUrl)); + + if (HasErrors) + { + return; + } + + var product = new UpdateProductDto + { + Id = Product.Id, + CategoryId = Product.CategoryId, + Name = Name, + Description = Description, + StockQuantity = StockQuantity, + DiscountPercentage = DiscountPercentage, + IsDeleted = IsDeleted, + IsInStock = IsInStock, + ImageUrl = ImageUrl + }; + + var data = await _productService.UpdateProductAsync(product); + + if (data is null) + { + ErrorMessage = $"Unable to update product {Product.Id} in category {Product.CategoryId}"; + return; + } + + if (string.IsNullOrEmpty(data.ErrorMessage)) + { + ClearUpdateProduct(); + SuccessMessage = $"Product {Product.Id} in category {Product.CategoryId} successfully updated"; + _messenger.Send(new ProductUpdatedMessage(data)); + } + else + { + ErrorMessage = data.ErrorMessage; + } + } + + [RelayCommand] + private async Task GoBack() + { + _messenger.Send(new NavigateBackToAllAdminProductsFromUpdateView()); + } + + private void ClearUpdateProduct() + { + Name = string.Empty; + Description = string.Empty; + StockQuantity = 0; + DiscountPercentage = 0; + IsDeleted = false; + IsInStock = false; + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCartViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCartViewModel.cs new file mode 100644 index 00000000..aa6e7046 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCartViewModel.cs @@ -0,0 +1,146 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewCartViewModel : ObservableObject +{ + [ObservableProperty] + private static List _cart; + + [ObservableProperty] + private static List _cartForDisplay; + + private readonly IMessenger _messenger; + + [ObservableProperty] + private CartItemDto? _selectedItem; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private string? _errorMessage; + + public ViewCartViewModel(List cart, IMessenger messenger) + { + _cart = cart; + _messenger = messenger; + _cartForDisplay = new List(); + LoadCartCommand.Execute(null); + } + + [RelayCommand] + private void LoadCart() + { + var pagedCart = Cart.Skip((Page - 1) * PageSize) + .Take(PageSize) + .ToList(); + + CartForDisplay = pagedCart; + + TotalPages = (int)Math.Ceiling(Cart.Count / (double)PageSize); + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + [RelayCommand] + private void NextPage() + { + if (!HasNextPage) return; + Page++; + LoadCart(); + } + + [RelayCommand] + private void PreviousPage() + { + if (!HasPreviousPage) return; + Page--; + LoadCart(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackFromViewCart()); + } + + [RelayCommand] + private void ClearCart() + { + Cart.Clear(); + LoadCart(); + } + + [RelayCommand] + private void Checkout() + { + _messenger.Send(new CheckoutMessage(Cart)); + } + + [RelayCommand] + private void UpdateQuantity() + { + ErrorMessage = null; + + if (SelectedItem is not null) + { + var itemToUpdate = Cart.FirstOrDefault(ci => ci.ProductId == SelectedItem.ProductId); + if (itemToUpdate is not null) + { + itemToUpdate.Quantity = SelectedItem.Quantity; + itemToUpdate.TotalAmount = SelectedItem.Quantity * SelectedItem.ProductPrice; + Cart.Remove(SelectedItem); + Cart.Add(itemToUpdate); + LoadCart(); + } + else + { + ErrorMessage = $"Unable to update quantity for {SelectedItem.ProductName}"; + } + } + } + + [RelayCommand] + private async Task RemoveItemAsync() + { + ErrorMessage = null; + + if (SelectedItem is not null) + { + var box = MessageBoxManager + .GetMessageBoxStandard("Delete", "Delete this item?", ButtonEnum.YesNo, Icon.Warning, null, + WindowStartupLocation.CenterOwner); + + var result = await box.ShowAsync(); + + if (result == ButtonResult.Yes) + { + Cart.Remove(SelectedItem); + LoadCart(); + } + else + { + ErrorMessage = $"Unable to delete {SelectedItem.ProductName}"; + } + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForAdminViewModel.cs new file mode 100644 index 00000000..daea8558 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForAdminViewModel.cs @@ -0,0 +1,121 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.CategoryMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewCategoriesForAdminViewModel : ObservableObject +{ + private readonly ICategoryService _categoryService; + private readonly IMessenger _messenger; + public ObservableCollection Categories { get; } = []; + + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private CategoryAdminSummaryData? _selectedCategory; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private string? _searchByDescription; + + async partial void OnSearchByDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCategoriesAsync); + + public ViewCategoriesForAdminViewModel(ICategoryService categoryService, IMessenger messenger) + { + _categoryService = categoryService; + _messenger = messenger; + LoadCategoriesCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadCategoriesAsync() + { + Page = 1; + await FetchCategoriesAsync(); + } + + private async Task FetchCategoriesAsync() + { + IsLoading = false; + + var queryParams = new CategoryQueryParams + { + Page = Page, + PageSize = PageSize, + Description = SearchByDescription + }; + + var result = await _categoryService.GetCategoriesForAdminAsync(queryParams); + + if (result is not null) + { + Categories.Clear(); + + foreach (var category in result.Data) + { + Categories.Add(category); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchCategoriesAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchCategoriesAsync(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToCategoryPageMessage()); + } + + [RelayCommand] + private void ClearFilters() + { + SearchByDescription = null; + } + + partial void OnSelectedCategoryChanged(CategoryAdminSummaryData? value) + { + if (value is not null) + { + _messenger.Send(new CategorySelectedForAdminMessage(value.Id)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForSaleViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForSaleViewModel.cs new file mode 100644 index 00000000..b963d5c2 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCategoriesForSaleViewModel.cs @@ -0,0 +1,139 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Category; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Sale; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.CategoryParameters; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewCategoriesForSaleViewModel : ObservableObject +{ + private readonly ICategoryService _categoryService; + private readonly IShoppingCartService _shoppingCartService; + private readonly IMessenger _messenger; + public ObservableCollection Categories { get; } = []; + + [ObservableProperty] + private static List _shoppingCart; + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private CategorySummaryData? _selectedCategory; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private string? _searchByDescription; + + public ViewCategoriesForSaleViewModel( + ICategoryService categoryService, + IShoppingCartService shoppingCartService, + IMessenger messenger) + { + _categoryService = categoryService; + _shoppingCartService = shoppingCartService; + _messenger = messenger; + _shoppingCart = ShoppingCartService.ShoppingCart; + LoadCategoriesCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadCategoriesAsync() + { + Page = 1; + + await FetchCategoriesAsync(); + } + + private async Task FetchCategoriesAsync() + { + IsLoading = true; + + var queryParams = new CategoryQueryParams + { + Page = Page, + PageSize = PageSize, + Description = SearchByDescription + }; + + var result = await _categoryService.GetCategoriesAsync(queryParams); + + if (result is not null) + { + Categories.Clear(); + + foreach (var category in result.Data) + { + Categories.Add(category); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchCategoriesAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchCategoriesAsync(); + } + + async partial void OnSearchByDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCategoriesAsync); + + partial void OnSelectedCategoryChanged(CategorySummaryData? value) + { + if (value is not null) + { + _messenger.Send(new CategorySelectedForSaleMessage(value.Id, ShoppingCart)); + } + } + + [RelayCommand] + private void ClearFilters() + { + SearchByDescription = null; + } + + [RelayCommand] + private void Checkout() + { + _messenger.Send(new CheckoutMessage(ShoppingCart)); + } + + [RelayCommand] + private void ViewCart() + { + _messenger.Send(new ViewCartMessage(ShoppingCart)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCustomersForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCustomersForAdminViewModel.cs new file mode 100644 index 00000000..30beb442 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewCustomersForAdminViewModel.cs @@ -0,0 +1,136 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Customer; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.CustomerParameters; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewCustomersForAdminViewModel : ObservableObject +{ + private readonly ICustomerService _customerService; + private readonly IMessenger _messenger; + public ObservableCollection Customers { get; } = []; + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private CustomerData? _selectedCustomer; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + + [ObservableProperty] + private int? _minSaleCount; + [ObservableProperty] + private int? _maxSaleCount; + [ObservableProperty] + private decimal? _minTotalSpent; + [ObservableProperty] + private decimal? _maxTotalSpent; + + async partial void OnMinSaleCountChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCustomersAsync); + async partial void OnMaxSaleCountChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCustomersAsync); + async partial void OnMinTotalSpentChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCustomersAsync); + async partial void OnMaxTotalSpentChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadCustomersAsync); + + public ViewCustomersForAdminViewModel(ICustomerService customerService, IMessenger messenger) + { + _customerService = customerService; + _messenger = messenger; + LoadCustomersCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadCustomersAsync() + { + Page = 1; + await FetchCustomersAsync(); + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchCustomersAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchCustomersAsync(); + } + + private async Task FetchCustomersAsync() + { + IsLoading = true; + + var queryParams = new CustomerQueryParams + { + Page = Page, + PageSize = PageSize, + MinSaleCount = MinSaleCount, + MaxSaleCount = MaxSaleCount, + MinTotalSpent = MinTotalSpent, + MaxTotalSpent = MaxTotalSpent + }; + + var result = await _customerService.GetCustomersForAdminAsync(queryParams); + + if (result is not null) + { + Customers.Clear(); + + foreach (var customer in result.Data) + { + Customers.Add(customer); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages && result.TotalItemsRetrieved >= PageSize; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task ClearFiltersAsync() + { + MinSaleCount = null; + MaxSaleCount = null; + MinTotalSpent = null; + MaxTotalSpent = null; + } + + [RelayCommand] + private async Task GoBack() + { + _messenger.Send(new NavigateBackToCustomerPageMessage()); + } + + partial void OnSelectedCustomerChanged(CustomerData? value) + { + if (value is not null) + { + _messenger.Send(new DisplayCustomerDetailsForAdminMessage(value)); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewModelBase.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewModelBase.cs new file mode 100644 index 00000000..fb665869 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForAdminViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForAdminViewModel.cs new file mode 100644 index 00000000..74febf57 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForAdminViewModel.cs @@ -0,0 +1,54 @@ +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.ProductMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewProductsForAdminViewModel : ProductsAdminBaseViewModel +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + + public ViewProductsForAdminViewModel(IProductService productService, IMessenger messenger) + { + _productService = productService; + _messenger = messenger; + LoadProductsCommand.Execute(null); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToProductPageMessage()); + } + + protected override async Task GetProductsAsync() + { + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + CategoryName = CategoryName, + Description = Description, + InStock = InStock, + IsDeleted = IsDeleted + }; + + return await _productService.GetProductsForAdminAsync(queryParams); + } + + protected override void OnProductSelected(ProductAdminData product) + { + _messenger.Send(new ProductSelectedForAdminMessage(product.Id)); + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForSaleViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForSaleViewModel.cs new file mode 100644 index 00000000..78a0a25e --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/ViewProductsForSaleViewModel.cs @@ -0,0 +1,184 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using ECommerce.AvaloniaClient.TerrenceLGee.Data.Models.Product; +using ECommerce.AvaloniaClient.TerrenceLGee.Helpers; +using ECommerce.AvaloniaClient.TerrenceLGee.Messages.SaleMessages; +using ECommerce.AvaloniaClient.TerrenceLGee.Services.Interfaces.Product; +using ECommerce.Shared.TerrenceLGee.DTOs.OrderDTOs; +using ECommerce.Shared.TerrenceLGee.Parameters.ProductParameters; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class ViewProductsForSaleViewModel : ObservableObject +{ + private readonly IProductService _productService; + private readonly IMessenger _messenger; + private readonly int _categoryId; + + [ObservableProperty] + private static List _shoppingCart; + + public ObservableCollection Products { get; } = []; + + [ObservableProperty] + private bool _isLoading; + [ObservableProperty] + private ProductData? _selectedProduct; + + [ObservableProperty] + private int _page = 1; + [ObservableProperty] + private int _pageSize = 10; + [ObservableProperty] + private int _totalPages; + [ObservableProperty] + private bool _hasPreviousPage; + [ObservableProperty] + private bool _hasNextPage; + [ObservableProperty] + private decimal? _minUnitPrice; + [ObservableProperty] + private decimal? _maxUnitPrice; + [ObservableProperty] + private int? _minStockQuantity; + [ObservableProperty] + private int? _maxStockQuantity; + [ObservableProperty] + private int? _minDiscountPercentage; + [ObservableProperty] + private int? _maxDiscountPercentage; + [ObservableProperty] + private string? _description; + + + public ViewProductsForSaleViewModel( + IProductService productService, + int categoryId, + List shoppingCart, + IMessenger messenger) + { + _productService = productService; + _categoryId = categoryId; + _shoppingCart = shoppingCart; + _messenger = messenger; + LoadProductsCommand.Execute(null); + } + + [RelayCommand] + private async Task LoadProductsAsync() + { + Page = 1; + await FetchProductsAsync(); + } + + private async Task FetchProductsAsync() + { + IsLoading = true; + + var queryParams = new ProductQueryParams + { + Page = Page, + PageSize = PageSize, + CategoryId = _categoryId, + MinUnitPrice = MinUnitPrice, + MaxUnitPrice = MaxUnitPrice, + MinStockQuantity = MinStockQuantity, + MaxStockQuantity = MaxStockQuantity, + MinDiscountPercentage = MinDiscountPercentage, + MaxDiscountPercentage = MaxDiscountPercentage, + Description = Description, + InStock = true + }; + + var result = await _productService.GetProductsAsync(queryParams); + + if (result is not null) + { + Products.Clear(); + + foreach (var product in result.Data) + { + Products.Add(product); + } + + TotalPages = result.TotalPages; + HasNextPage = Page < TotalPages && result.TotalItemsRetrieved >= PageSize; + HasPreviousPage = Page > 1; + } + + IsLoading = false; + } + + [RelayCommand] + private async Task NextPageAsync() + { + if (!HasNextPage) return; + Page++; + await FetchProductsAsync(); + } + + [RelayCommand] + private async Task PreviousPageAsync() + { + if (!HasPreviousPage) return; + Page--; + await FetchProductsAsync(); + } + + [RelayCommand] + private void GoBack() + { + _messenger.Send(new NavigateBackToAllCategoriesForSale()); + } + + [RelayCommand] + private void ClearFilters() + { + MinUnitPrice = null; + MaxUnitPrice = null; + MinStockQuantity = null; + MaxStockQuantity = null; + MaxDiscountPercentage = null; + MinDiscountPercentage = null; + Description = null; + } + + partial void OnSelectedProductChanged(ProductData? value) + { + if (value is not null) + { + _messenger.Send(new ProductSelectedForSaleMessage(value, _categoryId, ShoppingCart)); + } + } + + [RelayCommand] + private void ViewCart() + { + _messenger.Send(new ViewCartMessage(ShoppingCart)); + } + + [RelayCommand] + private void Checkout() + { + _messenger.Send(new CheckoutMessage(ShoppingCart)); + } + + async partial void OnMinUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxUnitPriceChanged(decimal? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxStockQuantityChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMinDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnMaxDiscountPercentageChanged(int? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + + async partial void OnDescriptionChanged(string? value) => await FilterHelper.OnFilterChangedAsync(Page, LoadProductsAsync); + +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/WelcomePageViewModel.cs b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/WelcomePageViewModel.cs new file mode 100644 index 00000000..0801ec75 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/ViewModels/WelcomePageViewModel.cs @@ -0,0 +1,41 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; + +namespace ECommerce.AvaloniaClient.TerrenceLGee.ViewModels; + +public partial class WelcomePageViewModel : ObservableObject +{ + public event Action? LoginRequested; + public event Action? RegistrationRequested; + public event Action? PasswordResetRequested; + + [RelayCommand] + private void NavigateToLogin() + { + LoginRequested?.Invoke(); + } + + [RelayCommand] + private void NavigateToRegistration() + { + RegistrationRequested?.Invoke(); + } + + [RelayCommand] + private void NavigateToPasswordReset() + { + PasswordResetRequested?.Invoke(); + } + + [RelayCommand] + private void Exit() + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.Shutdown(); + } + } +} diff --git a/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Views/AddAddressView.axaml b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Views/AddAddressView.axaml new file mode 100644 index 00000000..05a0ea84 --- /dev/null +++ b/ECommerce.TerrenceLGee/ECommerce.AvaloniaClient.TerrenceLGee/Views/AddAddressView.axaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +