Skip to content

Commit 6d4b279

Browse files
Events (#5)
* events manipulation (create, list, reply, filter) * team dashboard refactoring (remove events) * add bread crumb navigation * refactoring (naming - page suffix, structure) * additional fixes
1 parent 7b26d4d commit 6d4b279

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1341
-106
lines changed
File renamed without changes.
File renamed without changes.

src/TeamUp.Client/Layout/NavMenu.razor

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<FluentDivider />
2525
</Authorized>
2626
</AuthorizeView>
27-
<FluentNavMenu Id="main-menu" Width="250" Collapsible="false" Title="Navigation menu">
27+
<FluentNavMenu Id="main-menu" Width="220" Collapsible="false" Title="Navigation menu">
2828
<FluentNavGroup Expanded="@(teams is not null)" Icon="@(new Icons.Regular.Size20.PeopleTeam())" IconColor="Color.Accent">
2929
<TitleTemplate>
3030
<div class="d-flex align-center" style="margin: 2px">
@@ -58,6 +58,9 @@
5858
<div class="text-center">
5959
<FluentAnchor Href="logout" Appearance="Appearance.Accent" class="w-90">Logout</FluentAnchor>
6060
</div>
61+
<div class="text-center" style="margin-top: 4px;">
62+
<FluentAnchor Href="logout" Appearance="Appearance.Accent" class="w-90">Clear Cache</FluentAnchor>
63+
</div>
6164
</nav>
6265
</div>
6366

File renamed without changes.

src/TeamUp.Client/Pages/Components/TeamEventTypesComponent.razor

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
@rendermode InteractiveAuto
2+
3+
@inherits CancellableComponent
4+
5+
@inject IToastService ToastService
6+
@inject IDialogService DialogService
7+
@inject EventService EventService
8+
9+
<FluentButton Appearance="Appearance.Accent" Disabled="@CanReply()" IconStart="@(new Icons.Filled.Size20.Chat())" OnClick="() => ReplyAsync(EventResponse)" />
10+
11+
@code
12+
{
13+
[Parameter]
14+
[EditorRequired]
15+
public required EventSlimResponse EventResponse { get; set; }
16+
17+
[Parameter]
18+
[EditorRequired]
19+
public required TeamId TeamId { get; set; }
20+
21+
[Parameter]
22+
[EditorRequired]
23+
public required TeamMemberResponse Member { get; set; }
24+
25+
private IDialogReference? dialog;
26+
27+
private UpsertEventReplyInput replyInput = new();
28+
29+
public bool CanReply()
30+
{
31+
var closingTime = (EventResponse.FromUtc - EventResponse.MeetTime - EventResponse.ReplyClosingTimeBeforeMeetTime);
32+
return DateTime.UtcNow >= closingTime;
33+
}
34+
35+
public async Task ReplyAsync(EventSlimResponse eventResponse)
36+
{
37+
if (replyInput.Event != eventResponse)
38+
{
39+
replyInput.ReplyType = eventResponse.InitiatorResponse?.Type;
40+
replyInput.Message = eventResponse.InitiatorResponse?.Message ?? "";
41+
replyInput.Event = eventResponse;
42+
replyInput.Errors = null;
43+
}
44+
45+
dialog = await DialogService.ShowPanelAsync<UpsertEventReplyPanel>(replyInput, new DialogParameters<UpsertEventReplyInput>()
46+
{
47+
Content = replyInput,
48+
Alignment = HorizontalAlignment.Right,
49+
Title = eventResponse.Description,
50+
PrimaryAction = "Reply",
51+
SecondaryAction = "Cancel",
52+
});
53+
54+
var result = await dialog.Result;
55+
56+
if (result.Cancelled)
57+
{
58+
replyInput.Reset();
59+
return;
60+
}
61+
62+
if (replyInput.ReplyType is null)
63+
{
64+
ToastService.ShowError("Response is required.");
65+
replyInput.Reset();
66+
return;
67+
}
68+
69+
var invitationResult = await EventService.UpsertEventReplyAsync(TeamId, eventResponse.Id, Member, new UpsertEventReplyRequest
70+
{
71+
ReplyType = replyInput.ReplyType.Value,
72+
Message = replyInput.Message,
73+
}, CTS.Token);
74+
75+
if (invitationResult.IsFailure)
76+
{
77+
ToastService.ShowError(invitationResult.Error.Message);
78+
79+
if (invitationResult.Error is ApiValidationError error)
80+
{
81+
replyInput.Errors = error.Errors;
82+
}
83+
84+
return;
85+
}
86+
87+
replyInput.Reset();
88+
ToastService.ShowSuccess("Your have replied to the event.");
89+
}
90+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div class="reply-count-summary @($"reply-count-distinct-{ReplyCount.Count + (UndefinedCount > 0 ? 1 : 0)}")">
2+
@if (UndefinedCount > 0)
3+
{
4+
<div class="reply-type reply-type-undefined" tooltip="No Response">
5+
@(UndefinedCount)
6+
</div>
7+
}
8+
9+
@foreach (var replyCountResponse in ReplyCount)
10+
{
11+
<div class="reply-type reply-type-@((int)replyCountResponse.Type)" tooltip="@(replyCountResponse.Type.ToString())">
12+
@(replyCountResponse.Count)
13+
</div>
14+
}
15+
</div>
16+
17+
@code
18+
{
19+
[Parameter]
20+
[EditorRequired]
21+
public required List<ReplyCountResponse> ReplyCount { get; set; }
22+
23+
[Parameter]
24+
[EditorRequired]
25+
public required int TeamCount { get; set; }
26+
27+
public int UndefinedCount { get; set; }
28+
29+
protected override void OnParametersSet()
30+
{
31+
UndefinedCount = TeamCount - ReplyCount.Sum(x => x.Count);
32+
}
33+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@rendermode InteractiveAuto
2+
3+
@inherits CancellableComponent
4+
5+
@inject IToastService ToastService
6+
@inject IDialogService DialogService
7+
@inject IMessenger Messenger
8+
@inject EventService EventService
9+
10+
<div class="d-flex align-center w-100" style="margin-left: 3px">
11+
<h4 class="padding-small no-margin">Upcoming Events</h4>
12+
<FluentButton IconStart="@(new Icons.Regular.Size20.ArrowClockwise())" Appearance="Appearance.Outline" OnClick="() => LoadEventsAsync(true)" />
13+
</div>
14+
@if (events is not null)
15+
{
16+
<FluentDataGrid Items="@(events.AsQueryable())" ResizableColumns="true">
17+
<PropertyColumn Property="@(p => p.EventType)" Title="Event Type" Class="content-center" />
18+
<PropertyColumn Property="@(p => p.Description)" Title="Description" Class="content-center" />
19+
<PropertyColumn Property="@(p => p.FromUtc.ToLocalTime())" Format="dd.MM HH:mm" Sortable="true" IsDefaultSortColumn="true" Title="From" Class="content-center" />
20+
<PropertyColumn Property="@(p => p.ToUtc.ToLocalTime())" Format="HH:mm" Title="To" Class="content-center" />
21+
<PropertyColumn Property="@(p => p.InitiatorResponse == null ? "-" : p.InitiatorResponse.Type.ToString())" Align="Align.Center" Title="Your Response" Class="content-center" />
22+
<TemplateColumn Title="Responses" Align="Align.Center" Style="overflow: unset">
23+
<EventResponsesSummaryComponent ReplyCount="context.ReplyCount" TeamCount="TeamContext?.Team.Members.Count ?? 0" />
24+
</TemplateColumn>
25+
<TemplateColumn Title="Action" Class="content-center" Align="Align.Center">
26+
<FluentAnchor Appearance="Appearance.Outline" IconStart="@(new Icons.Filled.Size20.Search())" Href="@($"/teams/{TeamContext.TeamId.Value}/events/{context.Id.Value}")" />
27+
<EventReplyButton EventResponse="context" TeamId="TeamContext.TeamId" Member="TeamContext.Member" />
28+
</TemplateColumn>
29+
</FluentDataGrid>
30+
}
31+
32+
@code
33+
{
34+
private List<EventSlimResponse>? events;
35+
36+
[CascadingParameter]
37+
public TeamContext TeamContext { get; set; } = null!;
38+
39+
protected override void OnInitialized()
40+
{
41+
Messenger.Register<EventsComponent, EventUpdatedMessage>(this, (self, message) =>
42+
{
43+
var targetEvent = self.events?.FirstOrDefault(e => e.Id == message.Event.Id);
44+
if (targetEvent is not null)
45+
{
46+
targetEvent.InitiatorResponse = message.Event.InitiatorResponse;
47+
targetEvent.ReplyCount = message.Event.ReplyCount;
48+
self.StateHasChanged();
49+
}
50+
});
51+
}
52+
53+
protected override Task OnParametersSetAsync()
54+
{
55+
return LoadEventsAsync();
56+
}
57+
58+
private async Task LoadEventsAsync(bool forceLoad = false)
59+
{
60+
var result = await EventService.GetEventsAsync(TeamContext.TeamId, forceLoad, CTS.Token);
61+
if (result.IsSuccess)
62+
{
63+
events = result.Value;
64+
}
65+
else
66+
{
67+
ToastService.ShowError(result.Error.Message);
68+
}
69+
}
70+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
@page "/teams/{teamGuid:guid}/events/create"
2+
3+
@rendermode InteractiveAuto
4+
5+
@inherits CancellableComponent
6+
7+
@using System.Text.Json
8+
@using BitzArt.Blazor.Cookies
9+
10+
@inject NavigationManager NavigationManager
11+
@inject IToastService ToastService
12+
@inject EventService EventService
13+
@inject TeamService TeamService
14+
15+
<PageTitle>Create Event</PageTitle>
16+
17+
<div Class="body-small h-auto padding-large">
18+
<h1 style="margin-bottom: 16px">Create Event</h1>
19+
20+
<FluentDivider />
21+
22+
<FluentBreadcrumb Class="padding-small">
23+
<FluentBreadcrumbItem Href="@($"/teams/{TeamGuid}")">
24+
Team
25+
</FluentBreadcrumbItem>
26+
<FluentBreadcrumbItem>
27+
Cerate Event
28+
</FluentBreadcrumbItem>
29+
</FluentBreadcrumb>
30+
31+
<FluentDivider />
32+
33+
<EditForm Model="@Input" OnValidSubmit="CreateEventAsync" FormName="CreateEventForm" Style="margin-top: 16px">
34+
<CustomValidation @ref="ValidationComponent" />
35+
36+
<FluentStack Orientation="Orientation.Vertical">
37+
<FluentCombobox TOption="EventTypeResponse" Items="EventTypes" Label="Event Type" Multiple="false" OptionText="x => x.Name"
38+
OptionValue="x => x.Id.Value.ToString()" SelectedOptionChanged="@(e => Input.EventTypeId = e.Id)" Required="true" />
39+
40+
<FluentTextField @bind-value="Input.Description" Immediate="true" Required="true" Placeholder="description" Label="Event Description" Class="w-100" />
41+
<FluentValidationMessage For="() => Input.Description" Class="text-danger" />
42+
43+
<div>
44+
<FluentDatePicker Value="Input.FromUtc.Date" ValueChanged="@(e => Input.FromUtc = e!.Value.AddTicks(Input.FromUtc.TimeOfDay.Ticks))" Label="From" Required="true" />
45+
<FluentTimePicker Value="Input.FromUtc" ValueChanged="@(e => Input.FromUtc = Input.FromUtc.Date.AddTicks(e!.Value.TimeOfDay.Ticks))" Required="true" />
46+
</div>
47+
<FluentValidationMessage For="() => Input.FromUtc" Class="text-danger" />
48+
49+
<FluentTabs Class="w-100" @bind-ActiveTabId="eventDurationMethod">
50+
<FluentTab Label="@($"Event Duration ({(int)(Input.ToUtc - Input.FromUtc).TotalMinutes} minutes)")" Id="tab-timespan" DeferredLoading="true">
51+
<FluentSlider Orientation="Orientation.Horizontal" Min="60" Max="180" Step="15" TValue="double" Style="margin-top: 16px; margin-bottom: 32px"
52+
Value="@((int)(Input.ToUtc - Input.FromUtc).TotalMinutes)" ValueChanged="@(e => Input.ToUtc = Input.FromUtc.AddMinutes(e))">
53+
54+
<FluentSliderLabel Position="60">1 h</FluentSliderLabel>
55+
<FluentSliderLabel Position="90">1.5 h</FluentSliderLabel>
56+
<FluentSliderLabel Position="120">2 h</FluentSliderLabel>
57+
<FluentSliderLabel Position="150">2.5 h</FluentSliderLabel>
58+
<FluentSliderLabel Position="180">3 h</FluentSliderLabel>
59+
</FluentSlider>
60+
</FluentTab>
61+
<FluentTab Label="@($"To ({Input.ToUtc.ToLocalTime()})")" Id="tab-datetime" DeferredLoading="true">
62+
<div>
63+
<FluentDatePicker Value="Input.ToUtc.Date" ValueChanged="@(e => Input.ToUtc = e!.Value.AddTicks(Input.ToUtc.TimeOfDay.Ticks))" />
64+
<FluentTimePicker Value="Input.ToUtc" ValueChanged="@(e => Input.ToUtc = Input.ToUtc.Date.AddTicks(e!.Value.TimeOfDay.Ticks))" />
65+
</div>
66+
<FluentValidationMessage For="() => Input.ToUtc" Class="text-danger" />
67+
</FluentTab>
68+
</FluentTabs>
69+
70+
71+
<FluentSlider Orientation="Orientation.Horizontal" Label="@($"Meet Time: {(Input.FromUtc - Input.MeetTime).TimeOfDay.ToString("hh\\:mm\\:ss")} ({Input.MeetTime.TotalMinutes} minutes before the event)")"
72+
Min="0" Max="180" Step="5" TValue="double" Style="margin-bottom: 32px"
73+
Value="@(Input.MeetTime.TotalMinutes)" ValueChanged="@(e => Input.MeetTime = TimeSpan.FromMinutes(e))">
74+
75+
<FluentSliderLabel Position="0">0 m</FluentSliderLabel>
76+
<FluentSliderLabel Position="15">15 m</FluentSliderLabel>
77+
<FluentSliderLabel Position="30">30 m</FluentSliderLabel>
78+
<FluentSliderLabel Position="60">1 h</FluentSliderLabel>
79+
<FluentSliderLabel Position="120">2 h</FluentSliderLabel>
80+
<FluentSliderLabel Position="180">3 h</FluentSliderLabel>
81+
</FluentSlider>
82+
<FluentValidationMessage For="() => Input.MeetTime" Class="text-danger" />
83+
84+
<FluentSlider Orientation="Orientation.Horizontal" Label="@($"Reply Close Time: {(Input.FromUtc - Input.MeetTime - Input.ReplyClosingTimeBeforeMeetTime).TimeOfDay.ToString("hh\\:mm\\:ss")} ({Input.ReplyClosingTimeBeforeMeetTime.TotalMinutes} minutes before meet time)")"
85+
Min="0" Max="180" Step="5" TValue="double" Style="margin-bottom: 32px"
86+
Value="@(Input.ReplyClosingTimeBeforeMeetTime.TotalMinutes)"
87+
ValueChanged="@(e => Input.ReplyClosingTimeBeforeMeetTime = TimeSpan.FromMinutes(e))">
88+
89+
<FluentSliderLabel Position="0">0 m</FluentSliderLabel>
90+
<FluentSliderLabel Position="15">15 m</FluentSliderLabel>
91+
<FluentSliderLabel Position="30">30 m</FluentSliderLabel>
92+
<FluentSliderLabel Position="60">1 h</FluentSliderLabel>
93+
<FluentSliderLabel Position="120">2 h</FluentSliderLabel>
94+
<FluentSliderLabel Position="180">3 h</FluentSliderLabel>
95+
</FluentSlider>
96+
<FluentValidationMessage For="() => Input.ReplyClosingTimeBeforeMeetTime" Class="text-danger" />
97+
98+
<FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent" Style="margin-top: 8px">Create</FluentButton>
99+
</FluentStack>
100+
</EditForm>
101+
</div>
102+
103+
@code
104+
{
105+
private string eventDurationMethod = "tab-timespan";
106+
107+
[Parameter]
108+
public Guid TeamGuid { get; init; } = default!;
109+
110+
[SupplyParameterFromForm]
111+
private CreateEventRequest Input { get; set; } = new()
112+
{
113+
Description = "",
114+
EventTypeId = null!,
115+
FromUtc = DateTime.Now,
116+
ToUtc = DateTime.Now.AddHours(1),
117+
MeetTime = TimeSpan.FromMinutes(15),
118+
ReplyClosingTimeBeforeMeetTime = TimeSpan.FromMinutes(15),
119+
};
120+
121+
private List<EventTypeResponse>? EventTypes = null;
122+
123+
private CustomValidation ValidationComponent { get; set; } = null!;
124+
125+
protected override async Task OnInitializedAsync()
126+
{
127+
var teamId = TeamId.FromGuid(TeamGuid);
128+
var teamResult = await TeamService.GetEventTypesAsync(teamId, false, CTS.Token);
129+
if (teamResult.IsSuccess)
130+
{
131+
EventTypes = teamResult.Value;
132+
}
133+
}
134+
135+
public async Task CreateEventAsync()
136+
{
137+
var teamId = TeamId.FromGuid(TeamGuid);
138+
139+
var result = await EventService.CreateEventAsync(teamId, Input, CTS.Token);
140+
if (result.IsFailure)
141+
{
142+
if (result.Error is ApiValidationError error)
143+
{
144+
ValidationComponent.DisplayErrors(error.Errors);
145+
}
146+
147+
ToastService.ShowError(result.Error.Message);
148+
return;
149+
}
150+
151+
ToastService.ShowSuccess("Event has been successfully created.");
152+
NavigationManager.NavigateTo($"/teams/{teamId.Value}/events/{result.Value.Value}");
153+
}
154+
}

0 commit comments

Comments
 (0)