Skip to content

Commit f51e894

Browse files
this commit introduces blazor web assembly solution for establishment management, including dashboard, products, orders, kitchen, and settings. Integrates custom JWT authentication, local storage, external API consumption (Identity, Store, Profiles), and MudBlazor-based interface. Includes UI components, validated forms, error pages, infrastructure extensions, configuration files, and static resources. Project ready for development and visual customization.
1 parent df7ed1f commit f51e894

59 files changed

Lines changed: 2874 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.3.11312.210
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{B8EFCA5F-814F-285C-A8CB-F00F14650265}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comanda.Merchants.WebUI", "Source\Comanda.Merchants.WebUI.csproj", "{C835E518-C45B-4575-93EC-78103FD20F9B}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Debug|x64 = Debug|x64
14+
Debug|x86 = Debug|x86
15+
Release|Any CPU = Release|Any CPU
16+
Release|x64 = Release|x64
17+
Release|x86 = Release|x86
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.ActiveCfg = Debug|Any CPU
23+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.Build.0 = Debug|Any CPU
24+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.ActiveCfg = Debug|Any CPU
25+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.Build.0 = Debug|Any CPU
26+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.Build.0 = Release|Any CPU
28+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.ActiveCfg = Release|Any CPU
29+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.Build.0 = Release|Any CPU
30+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.ActiveCfg = Release|Any CPU
31+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.Build.0 = Release|Any CPU
32+
EndGlobalSection
33+
GlobalSection(SolutionProperties) = preSolution
34+
HideSolutionNode = FALSE
35+
EndGlobalSection
36+
GlobalSection(NestedProjects) = preSolution
37+
{C835E518-C45B-4575-93EC-78103FD20F9B} = {B8EFCA5F-814F-285C-A8CB-F00F14650265}
38+
EndGlobalSection
39+
GlobalSection(ExtensibilityGlobals) = postSolution
40+
SolutionGuid = {B38CFDDE-933A-4367-B105-C867F91B05BD}
41+
EndGlobalSection
42+
EndGlobal
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@inject IProfilesClient profilesClient
2+
@inject IPrincipalProvider principalProvider
3+
@inject IStoreClient storeClient
4+
@inject ISessionManager sessionManager
5+
@inject IIdentityClient identityClient
6+
@inject ILocalStorageGateway localStorage
7+
@inject AuthenticationStateProvider authenticationStateProvider
8+
@inject NavigationManager navigationManager
9+
10+
<CascadingAuthenticationState>
11+
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
12+
<Found Context="routeData">
13+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
14+
<NotAuthorized>
15+
<NotAuthorized />
16+
</NotAuthorized>
17+
</AuthorizeRouteView>
18+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
19+
</Found>
20+
</Router>
21+
</CascadingAuthenticationState>
22+
23+
@code {
24+
protected override async Task OnInitializedAsync()
25+
{
26+
await base.OnInitializedAsync();
27+
await SetPrincipalAsync();
28+
29+
var session = await authenticationStateProvider.GetAuthenticationStateAsync();
30+
var principal = session.User;
31+
32+
if (principal?.Identity?.IsAuthenticated is true)
33+
{
34+
var identifier = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
35+
var owners = await profilesClient.GetOwnersAsync(new() { UserId = identifier! });
36+
37+
var owner = owners?.Data?.Items.FirstOrDefault();
38+
if (owner is null)
39+
return;
40+
41+
var establishments = await storeClient.GetEstablishmentsAsync(new() { OwnerId = owner.Identifier });
42+
var establishment = establishments?.Data?.Items.FirstOrDefault();
43+
44+
if (establishment is null)
45+
{
46+
navigationManager.NavigateTo("/onboarding");
47+
return;
48+
}
49+
50+
await localStorage.SetAsync<EstablishmentScheme>(Storage.Establishment, establishment);
51+
await localStorage.SetAsync<OwnerScheme>(Storage.Merchant, owner);
52+
}
53+
54+
}
55+
56+
private async Task SetPrincipalAsync()
57+
{
58+
var principal = await principalProvider.GetPrincipalAsync();
59+
if (principal.IsFailure || principal.Data is null)
60+
return;
61+
62+
await localStorage.SetAsync<PrincipalScheme>(Storage.Principal, principal.Data);
63+
}
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class JwtAuthenticationStateProvider(ILocalStorageGateway localStorage) :
4+
AuthenticationStateProvider
5+
{
6+
private readonly JwtSecurityTokenHandler tokenHandler = new();
7+
private readonly ClaimsPrincipal anonymous =
8+
new(new ClaimsIdentity());
9+
10+
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
11+
{
12+
var tokenString = await localStorage.GetAsStringAsync(Storage.SecurityToken);
13+
if (string.IsNullOrWhiteSpace(tokenString))
14+
{
15+
return new AuthenticationState(anonymous);
16+
}
17+
18+
var token = tokenHandler.ReadJwtToken(tokenString);
19+
if (token.ValidTo < DateTime.UtcNow)
20+
{
21+
return new AuthenticationState(anonymous);
22+
}
23+
24+
var identity = new ClaimsIdentity(token.Claims, "https://www.rfc-editor.org/rfc/rfc7519", nameType: "preferred_username", roleType: "role");
25+
var principal = new ClaimsPrincipal(identity);
26+
27+
return new AuthenticationState(principal);
28+
}
29+
30+
public void NotifyAuthenticationStateChanged()
31+
{
32+
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
33+
}
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class PrincipalProvider(HttpClient httpClient) : IPrincipalProvider
4+
{
5+
private readonly JsonSerializerOptions serializerOptions = new()
6+
{
7+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
8+
PropertyNameCaseInsensitive = true
9+
};
10+
11+
public async Task<Result<PrincipalScheme>> GetPrincipalAsync(CancellationToken cancellation = default)
12+
{
13+
var response = await httpClient.GetAsync("/api/v1/identity/principal", cancellation);
14+
var content = await response.Content.ReadAsStringAsync(cancellation);
15+
16+
// we prefer explicit boolean comparisons for readability
17+
// https://rules.sonarsource.com/csharp/RSPEC-1125/
18+
19+
#pragma warning disable S1125
20+
if (response.IsSuccessStatusCode is false)
21+
{
22+
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
23+
}
24+
25+
var result = JsonSerializer.Deserialize<PrincipalScheme>(content, serializerOptions);
26+
if (result is null)
27+
{
28+
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
29+
}
30+
31+
return Result<PrincipalScheme>.Success(result);
32+
}
33+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class SessionManager(ILocalStorageGateway localStorage, IIdentityClient identityClient, AuthenticationStateProvider provider) : ISessionManager
4+
{
5+
public async Task SignInAsync(string token, string refreshToken)
6+
{
7+
await localStorage.SetAsStringAsync(Storage.SecurityToken, token);
8+
await localStorage.SetAsStringAsync(Storage.RefreshToken, refreshToken);
9+
10+
// inform provider of signin because blazor does not automatically detect localstorage updates
11+
if (provider is JwtAuthenticationStateProvider authenticationState)
12+
authenticationState.NotifyAuthenticationStateChanged();
13+
}
14+
15+
public async Task SignOutAsync()
16+
{
17+
var refreshToken = await localStorage.GetAsStringAsync(Storage.RefreshToken);
18+
if (string.IsNullOrWhiteSpace(refreshToken))
19+
return;
20+
21+
await localStorage.RemoveAsync(Storage.SecurityToken);
22+
await identityClient.InvalidateSessionAsync(new() { RefreshToken = refreshToken });
23+
24+
// notify provider of logout because blazor does not detect localstorage changes automatically
25+
if (provider is JwtAuthenticationStateProvider authenticationState)
26+
authenticationState.NotifyAuthenticationStateChanged();
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
12+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
13+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all" />
14+
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
15+
16+
<PackageReference Include="MudBlazor" Version="8.15.0" />
17+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
18+
<PackageReference Include="Comanda.Internal.Contracts" Version="1.0.1" />
19+
<PackageReference Include="HttpsRichardy.Federation.Sdk.Contracts" Version="1.0.2" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
2+
<MudGrid Spacing="3">
3+
<MudItem xs="12" sm="6" md="3">
4+
<MudPaper Elevation="0" Class="pa-4"
5+
Style="height: 100%; border-radius: 12px; border-left: 4px solid var(--mud-palette-primary);">
6+
<div class="d-flex align-center mb-3">
7+
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Large"
8+
Style="background: rgba(var(--mud-palette-primary-rgb), 0.1);">
9+
<MudIcon Icon="@Icons.Material.Filled.AttachMoney" Color="Color.Primary" Size="Size.Medium" />
10+
</MudAvatar>
11+
</div>
12+
<MudText Typo="Typo.body2"
13+
Style="opacity: 0.7; text-transform: uppercase; font-weight: 600; font-size: 0.75rem; letter-spacing: 0.5px;">
14+
Vendas Hoje
15+
</MudText>
16+
<MudText Typo="Typo.h4" Class="mt-2" Style="font-weight: 800; font-size: 2rem;">
17+
R$ 2.847,50
18+
</MudText>
19+
</MudPaper>
20+
</MudItem>
21+
22+
<MudItem xs="12" sm="6" md="3">
23+
<MudPaper Elevation="0" Class="pa-4"
24+
Style="height: 100%; border-radius: 12px; border-left: 4px solid var(--mud-palette-primary);">
25+
<div class="d-flex align-center mb-3">
26+
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Large"
27+
Style="background: rgba(var(--mud-palette-primary-rgb), 0.1);">
28+
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Color="Color.Primary" Size="Size.Medium" />
29+
</MudAvatar>
30+
</div>
31+
<MudText Typo="Typo.body2"
32+
Style="opacity: 0.7; text-transform: uppercase; font-weight: 600; font-size: 0.75rem; letter-spacing: 0.5px;">
33+
Pedidos Hoje
34+
</MudText>
35+
<MudText Typo="Typo.h4" Class="mt-2" Style="font-weight: 800; font-size: 2rem;">
36+
42
37+
</MudText>
38+
</MudPaper>
39+
</MudItem>
40+
41+
<MudItem xs="12" sm="6" md="3">
42+
<MudPaper Elevation="0" Class="pa-4"
43+
Style="height: 100%; border-radius: 12px; border-left: 4px solid var(--mud-palette-primary);">
44+
<div class="d-flex align-center mb-3">
45+
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Large"
46+
Style="background: rgba(var(--mud-palette-primary-rgb), 0.1);">
47+
<MudIcon Icon="@Icons.Material.Filled.PendingActions" Color="Color.Primary"
48+
Size="Size.Medium" />
49+
</MudAvatar>
50+
</div>
51+
<MudText Typo="Typo.body2"
52+
Style="opacity: 0.7; text-transform: uppercase; font-weight: 600; font-size: 0.75rem; letter-spacing: 0.5px;">
53+
Pedidos Pendentes
54+
</MudText>
55+
<MudText Typo="Typo.h4" Class="mt-2" Style="font-weight: 800; font-size: 2rem;">
56+
7
57+
</MudText>
58+
<div class="d-flex align-center mt-2">
59+
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" Color="Color.Inherit" />
60+
<MudText Typo="Typo.caption" Color="Color.Inherit" Style="margin-left: 4px; font-weight: 600;">
61+
Aguardando
62+
</MudText>
63+
</div>
64+
</MudPaper>
65+
</MudItem>
66+
67+
<MudItem xs="12" sm="6" md="3">
68+
<MudPaper Elevation="0" Class="pa-4"
69+
Style="height: 100%; border-radius: 12px; border-left: 4px solid var(--mud-palette-primary);">
70+
<div class="d-flex align-center mb-3">
71+
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Large"
72+
Style="background: rgba(var(--mud-palette-primary-rgb), 0.1);">
73+
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Primary" Size="Size.Medium" />
74+
</MudAvatar>
75+
</div>
76+
<MudText Typo="Typo.body2"
77+
Style="opacity: 0.7; text-transform: uppercase; font-weight: 600; font-size: 0.75rem; letter-spacing: 0.5px;">
78+
Ticket Médio
79+
</MudText>
80+
<MudText Typo="Typo.h4" Class="mt-2" Style="font-weight: 800; font-size: 2rem;">
81+
R$ 67,80
82+
</MudText>
83+
</MudPaper>
84+
</MudItem>
85+
</MudGrid>
86+
87+
<MudPaper Elevation="0" Class="pa-6 mt-5" Style="border-radius: 12px;">
88+
<div class="d-flex justify-space-between align-center mb-4">
89+
<div>
90+
<MudText Typo="Typo.h5" Style="font-weight: 700;">
91+
Resumo dos Últimos 7 Dias
92+
</MudText>
93+
<MudText Typo="Typo.body2" Style="opacity: 0.6; margin-top: 4px;">
94+
Atualizado @DateTime.Now.ToString("HH:mm")
95+
</MudText>
96+
</div>
97+
<MudButton Variant="Variant.Text" Color="Color.Inherit" StartIcon="@Icons.Material.Filled.Refresh"
98+
Size="Size.Small" Style="border-radius: 8px;">
99+
Atualizar
100+
</MudButton>
101+
</div>
102+
103+
<MudGrid Spacing="4">
104+
<MudItem xs="12" sm="6" md="3">
105+
<div class="d-flex align-center">
106+
<MudIcon Icon="@Icons.Material.Outlined.Receipt" Color="Color.Primary" Size="Size.Large"
107+
Class="mr-3" />
108+
<div>
109+
<MudText Typo="Typo.caption"
110+
Style="opacity: 0.6; text-transform: uppercase; font-size: 0.7rem; font-weight: 600;">
111+
Total Pedidos
112+
</MudText>
113+
<MudText Typo="Typo.h5" Style="font-weight: 800; margin-top: 4px;">
114+
287
115+
</MudText>
116+
</div>
117+
</div>
118+
</MudItem>
119+
120+
<MudItem xs="12" sm="6" md="3">
121+
<div class="d-flex align-center">
122+
<MudIcon Icon="@Icons.Material.Outlined.Paid" Color="Color.Primary" Size="Size.Large" Class="mr-3" />
123+
<div>
124+
<MudText Typo="Typo.caption"
125+
Style="opacity: 0.6; text-transform: uppercase; font-size: 0.7rem; font-weight: 600;">
126+
Receita Total
127+
</MudText>
128+
<MudText Typo="Typo.h5" Style="font-weight: 800; margin-top: 4px;">
129+
R$ 19.458,90
130+
</MudText>
131+
</div>
132+
</div>
133+
</MudItem>
134+
135+
<MudItem xs="12" sm="6" md="3">
136+
<div class="d-flex align-center">
137+
<MudIcon Icon="@Icons.Material.Outlined.CalendarToday" Color="Color.Primary" Size="Size.Large"
138+
Class="mr-3" />
139+
<div>
140+
<MudText Typo="Typo.caption"
141+
Style="opacity: 0.6; text-transform: uppercase; font-size: 0.7rem; font-weight: 600;">
142+
Média Diária
143+
</MudText>
144+
<MudText Typo="Typo.h5" Style="font-weight: 800; margin-top: 4px;">
145+
R$ 2.779,84
146+
</MudText>
147+
</div>
148+
</div>
149+
</MudItem>
150+
151+
<MudItem xs="12" sm="6" md="3">
152+
<div class="d-flex align-center">
153+
<MudIcon Icon="@Icons.Material.Outlined.ShowChart" Color="Color.Primary" Size="Size.Large"
154+
Class="mr-3" />
155+
<div>
156+
<MudText Typo="Typo.caption"
157+
Style="opacity: 0.6; text-transform: uppercase; font-size: 0.7rem; font-weight: 600;">
158+
Crescimento
159+
</MudText>
160+
<MudText Typo="Typo.h5" Color="Color.Inherit" Style="font-weight: 800; margin-top: 4px;">
161+
+15.8%
162+
</MudText>
163+
</div>
164+
</div>
165+
</MudItem>
166+
</MudGrid>
167+
</MudPaper>
168+
</MudContainer>

0 commit comments

Comments
 (0)