Compare commits
2 Commits
27be43fe53
...
005bfebe6d
| Author | SHA1 | Date |
|---|---|---|
|
|
005bfebe6d | |
|
|
343ad931d1 |
|
|
@ -344,7 +344,7 @@ NavMenu.razor:
|
|||
| ✓ | E1 | Days | DayState Fluxor | 4 State-Dateien |
|
||||
| ✓ | E2 | Days | Days List Page | 1 Razor |
|
||||
| ✓ | E3 | Days | Day Details Page | 1 Razor |
|
||||
| ☐ | E4 | Days | PersonExpense Management | Components in DayDetails |
|
||||
| ✓ | E4 | Days | PersonExpense Management | Components in DayDetails |
|
||||
| ☐ | F1 | Dashboard | Dashboard Page | 1 Razor |
|
||||
| ☐ | F2 | Dashboard | Evaluation Components | 2 Shared Components |
|
||||
| ☐ | F3 | Navigation | NavMenu finalisieren | 1 Razor ändern |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Domain.Enums
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
@if (IsInverseMode)
|
||||
{
|
||||
<text>Inverse Strafe hinzufügen</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Strafe hinzufügen</text>
|
||||
}
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_isValid">
|
||||
<!-- Expense Selection -->
|
||||
<MudSelect T="ExpenseDto"
|
||||
Label="Strafe"
|
||||
@bind-Value="_selectedExpense"
|
||||
Required="true"
|
||||
RequiredError="Bitte wählen Sie eine Strafe"
|
||||
ToStringFunc="@(e => e?.Name ?? string.Empty)"
|
||||
Class="mb-4">
|
||||
@foreach (var expense in AvailableExpenses.OrderBy(e => e.Name))
|
||||
{
|
||||
<MudSelectItem T="ExpenseDto" Value="expense">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width: 100%">
|
||||
<MudText>@expense.Name</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@if (expense.IsInverse)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.SwapHoriz" Size="Size.Small" Color="Color.Warning" Title="Inverse" />
|
||||
}
|
||||
@if (expense.IsVariable)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Info" Title="Variable" />
|
||||
}
|
||||
<MudText Color="Color.Secondary">@expense.Price.ToString("C")</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (_selectedExpense?.IsInverse == true)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-4">
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>Inverse Strafe:</strong> Die Strafe wird allen Teilnehmern <em>außer</em> der ausgewählten Person zugewiesen.
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<!-- Person Selection -->
|
||||
<MudSelect T="DayParticipantDto"
|
||||
Label="@(_selectedExpense?.IsInverse == true ? "Person ausschließen" : "Person")"
|
||||
@bind-Value="_selectedParticipant"
|
||||
Required="true"
|
||||
RequiredError="Bitte wählen Sie eine Person"
|
||||
ToStringFunc="@(p => p?.PersonName ?? string.Empty)"
|
||||
Class="mb-4">
|
||||
@foreach (var participant in Participants.OrderBy(p => p.PersonName))
|
||||
{
|
||||
<MudSelectItem T="DayParticipantDto" Value="participant">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Size="Size.Small" Color="@(participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary)">
|
||||
@participant.PersonName[0]
|
||||
</MudAvatar>
|
||||
<MudText>@participant.PersonName</MudText>
|
||||
@if (participant.PersonStatus == PersonStatus.Guest)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<!-- Variable Price -->
|
||||
@if (_selectedExpense?.IsVariable == true)
|
||||
{
|
||||
<MudNumericField T="decimal"
|
||||
Label="Preis"
|
||||
@bind-Value="_customPrice"
|
||||
Min="0"
|
||||
Format="C"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Euro"
|
||||
Required="true"
|
||||
RequiredError="Bitte geben Sie einen Preis ein" />
|
||||
}
|
||||
else if (_selectedExpense is not null)
|
||||
{
|
||||
<MudText Class="mb-4">
|
||||
<strong>Preis:</strong> @_selectedExpense.Price.ToString("C")
|
||||
</MudText>
|
||||
}
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Abbrechen</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
Disabled="@(!_isValid || _selectedExpense is null || _selectedParticipant is null)"
|
||||
OnClick="Submit">
|
||||
Hinzufügen
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance? MudDialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<ExpenseDto> AvailableExpenses { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<DayParticipantDto> Participants { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public Guid DayId { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private ExpenseDto? _selectedExpense;
|
||||
private DayParticipantDto? _selectedParticipant;
|
||||
private decimal _customPrice;
|
||||
|
||||
private bool IsInverseMode => _selectedExpense?.IsInverse == true;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
if (_selectedExpense is not null)
|
||||
{
|
||||
_customPrice = _selectedExpense.Price;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog?.Cancel();
|
||||
|
||||
private void Submit()
|
||||
{
|
||||
if (_selectedExpense is null || _selectedParticipant is null) return;
|
||||
|
||||
if (_selectedExpense.IsInverse)
|
||||
{
|
||||
var dto = new CreateInversePersonExpenseDto
|
||||
{
|
||||
ExcludedPersonId = _selectedParticipant.PersonId,
|
||||
ExpenseId = _selectedExpense.Id,
|
||||
DayId = DayId,
|
||||
GameId = null
|
||||
};
|
||||
MudDialog?.Close(DialogResult.Ok(dto));
|
||||
}
|
||||
else
|
||||
{
|
||||
var dto = new CreatePersonExpenseDto
|
||||
{
|
||||
PersonId = _selectedParticipant.PersonId,
|
||||
ExpenseId = _selectedExpense.Id,
|
||||
DayId = DayId,
|
||||
GameId = null,
|
||||
Price = _selectedExpense.IsVariable ? _customPrice : null
|
||||
};
|
||||
MudDialog?.Close(DialogResult.Ok(dto));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,16 +194,131 @@ else
|
|||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- PersonExpense placeholder for E4 -->
|
||||
<!-- PersonExpense Section -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudText Typo="Typo.h6" Class="mb-4">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AttachMoney" Class="mr-2" />
|
||||
Strafen / Kosten
|
||||
</MudText>
|
||||
<MudAlert Severity="Severity.Info">
|
||||
Die Verwaltung von Strafen und Kosten wird in Phase E4 implementiert.
|
||||
</MudAlert>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AttachMoney" Class="mr-2" />
|
||||
Strafen / Kosten (@Expenses.Count)
|
||||
</MudText>
|
||||
@if (Day.Status != DayStatus.Closed)
|
||||
{
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddExpenseDialog"
|
||||
Size="Size.Small"
|
||||
Disabled="@(Day.Participants.Count == 0)">
|
||||
Strafe hinzufügen
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@if (DayState.Value.IsLoadingExpenses)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Class="mb-4" />
|
||||
}
|
||||
|
||||
@if (Day.Participants.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">
|
||||
Fügen Sie zuerst Teilnehmer hinzu, um Strafen zuweisen zu können.
|
||||
</MudAlert>
|
||||
}
|
||||
else if (Expenses.Count == 0)
|
||||
{
|
||||
<MudText Color="Color.Secondary">Keine Strafen zugewiesen</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Summary Section -->
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="4">
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudStack AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Gesamt</MudText>
|
||||
<MudText Typo="Typo.h5">@TotalExpenseAmount.ToString("C")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudStack AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Offen</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Warning">@OpenExpenseAmount.ToString("C")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudStack AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Bezahlt</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Success">@PaidExpenseAmount.ToString("C")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- Expense Table -->
|
||||
<MudTable Items="@Expenses" Dense="true" Hover="true" Striped="true" Breakpoint="Breakpoint.Sm">
|
||||
<HeaderContent>
|
||||
<MudTh>Person</MudTh>
|
||||
<MudTh>Strafe</MudTh>
|
||||
<MudTh Style="text-align: right">Preis</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh Style="text-align: right">Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Person">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Size="Size.Small" Color="Color.Primary">
|
||||
@context.PersonName[0]
|
||||
</MudAvatar>
|
||||
<MudText>@context.PersonName</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Strafe">@context.Name</MudTd>
|
||||
<MudTd DataLabel="Preis" Style="text-align: right">@context.Price.ToString("C")</MudTd>
|
||||
<MudTd DataLabel="Status">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? Color.Warning : Color.Success)"
|
||||
Icon="@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? Icons.Material.Filled.HourglassEmpty : Icons.Material.Filled.Check)">
|
||||
@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? "Offen" : "Bezahlt")
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Aktionen" Style="text-align: right">
|
||||
<MudStack Row="true" Spacing="1" Justify="Justify.FlexEnd">
|
||||
@if (context.PersonExpenseStatus == PersonExpenseStatus.Open)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Check"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success"
|
||||
OnClick="@(() => MarkAsPaid(context))"
|
||||
Title="Als bezahlt markieren" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Undo"
|
||||
Size="Size.Small"
|
||||
Color="Color.Warning"
|
||||
OnClick="@(() => MarkAsOpen(context))"
|
||||
Title="Als offen markieren" />
|
||||
}
|
||||
@if (Day.Status != DayStatus.Closed)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => ConfirmDeleteExpense(context))"
|
||||
Title="Löschen" />
|
||||
}
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
|
@ -214,12 +329,19 @@ else
|
|||
public Guid DayId { get; set; }
|
||||
|
||||
private DayDto? Day => DayState.Value.SelectedDay;
|
||||
private IReadOnlyList<PersonExpenseDto> Expenses => DayState.Value.SelectedDayExpenses;
|
||||
|
||||
private decimal TotalExpenseAmount => Expenses.Sum(e => e.Price);
|
||||
private decimal OpenExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Open).Sum(e => e.Price);
|
||||
private decimal PaidExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Done).Sum(e => e.Price);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
|
||||
Dispatcher.Dispatch(new LoadAvailablePersonsAction());
|
||||
Dispatcher.Dispatch(new LoadDayExpensesAction(DayId));
|
||||
Dispatcher.Dispatch(new LoadAvailableExpensesAction());
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
|
|
@ -228,6 +350,7 @@ else
|
|||
if (Day?.Id != DayId)
|
||||
{
|
||||
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
|
||||
Dispatcher.Dispatch(new LoadDayExpensesAction(DayId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,4 +527,86 @@ else
|
|||
Snackbar.Add("Teilnehmer wird entfernt...", Severity.Info);
|
||||
}
|
||||
}
|
||||
|
||||
// PersonExpense Methods
|
||||
|
||||
private async Task OpenAddExpenseDialog()
|
||||
{
|
||||
if (Day is null) return;
|
||||
|
||||
var availableExpenses = DayState.Value.AvailableExpenses;
|
||||
|
||||
if (availableExpenses.Count == 0)
|
||||
{
|
||||
Snackbar.Add("Keine Strafen-Vorlagen definiert. Bitte erstellen Sie zuerst Vorlagen unter \"Strafen\".", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "AvailableExpenses", availableExpenses },
|
||||
{ "Participants", Day.Participants },
|
||||
{ "DayId", Day.Id }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AddPersonExpenseDialog>("Strafe hinzufügen", parameters);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result != null && !result.Canceled && result.Data is not null)
|
||||
{
|
||||
if (result.Data is CreateInversePersonExpenseDto inverseDto)
|
||||
{
|
||||
Dispatcher.Dispatch(new CreateInversePersonExpenseAction(inverseDto));
|
||||
Snackbar.Add("Inverse Strafe wird erstellt...", Severity.Info);
|
||||
}
|
||||
else if (result.Data is CreatePersonExpenseDto createDto)
|
||||
{
|
||||
Dispatcher.Dispatch(new CreatePersonExpenseAction(createDto));
|
||||
Snackbar.Add("Strafe wird hinzugefügt...", Severity.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkAsPaid(PersonExpenseDto expense)
|
||||
{
|
||||
var dto = new UpdatePersonExpenseStatusDto
|
||||
{
|
||||
Id = expense.Id,
|
||||
Status = PersonExpenseStatus.Done
|
||||
};
|
||||
Dispatcher.Dispatch(new UpdatePersonExpenseStatusAction(dto));
|
||||
Snackbar.Add("Status wird aktualisiert...", Severity.Info);
|
||||
}
|
||||
|
||||
private void MarkAsOpen(PersonExpenseDto expense)
|
||||
{
|
||||
var dto = new UpdatePersonExpenseStatusDto
|
||||
{
|
||||
Id = expense.Id,
|
||||
Status = PersonExpenseStatus.Open
|
||||
};
|
||||
Dispatcher.Dispatch(new UpdatePersonExpenseStatusAction(dto));
|
||||
Snackbar.Add("Status wird aktualisiert...", Severity.Info);
|
||||
}
|
||||
|
||||
private async Task ConfirmDeleteExpense(PersonExpenseDto expense)
|
||||
{
|
||||
if (Day is null) return;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "ContentText", $"Möchten Sie die Strafe \"{expense.Name}\" für \"{expense.PersonName}\" wirklich löschen?" },
|
||||
{ "ButtonText", "Löschen" },
|
||||
{ "Color", Color.Error }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("Strafe löschen", parameters);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result != null && !result.Canceled)
|
||||
{
|
||||
Dispatcher.Dispatch(new DeletePersonExpenseAction(expense.Id, Day.Id));
|
||||
Snackbar.Add("Strafe wird gelöscht...", Severity.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,3 +169,95 @@ public record ClearDayErrorAction;
|
|||
/// Action to set filter for day list.
|
||||
/// </summary>
|
||||
public record SetDayFilterAction(DayFilterDto? Filter);
|
||||
|
||||
// PersonExpense Actions
|
||||
|
||||
/// <summary>
|
||||
/// Action to load person expenses for a day.
|
||||
/// </summary>
|
||||
public record LoadDayExpensesAction(Guid DayId);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when day expenses are loaded successfully.
|
||||
/// </summary>
|
||||
public record LoadDayExpensesSuccessAction(IReadOnlyList<PersonExpenseDto> Expenses);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when loading day expenses fails.
|
||||
/// </summary>
|
||||
public record LoadDayExpensesFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action to load available expense templates.
|
||||
/// </summary>
|
||||
public record LoadAvailableExpensesAction;
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when available expenses are loaded successfully.
|
||||
/// </summary>
|
||||
public record LoadAvailableExpensesSuccessAction(IReadOnlyList<ExpenseDto> Expenses);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when loading available expenses fails.
|
||||
/// </summary>
|
||||
public record LoadAvailableExpensesFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action to create a new person expense.
|
||||
/// </summary>
|
||||
public record CreatePersonExpenseAction(CreatePersonExpenseDto Dto);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when person expense is created successfully.
|
||||
/// </summary>
|
||||
public record CreatePersonExpenseSuccessAction(PersonExpenseDto Expense);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when creating person expense fails.
|
||||
/// </summary>
|
||||
public record CreatePersonExpenseFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action to create an inverse person expense (charged to all except one).
|
||||
/// </summary>
|
||||
public record CreateInversePersonExpenseAction(CreateInversePersonExpenseDto Dto);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when inverse expenses are created successfully.
|
||||
/// </summary>
|
||||
public record CreateInversePersonExpenseSuccessAction(IReadOnlyList<PersonExpenseDto> Expenses);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when creating inverse expense fails.
|
||||
/// </summary>
|
||||
public record CreateInversePersonExpenseFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action to delete a person expense.
|
||||
/// </summary>
|
||||
public record DeletePersonExpenseAction(Guid Id, Guid DayId);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when person expense is deleted successfully.
|
||||
/// </summary>
|
||||
public record DeletePersonExpenseSuccessAction(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when deleting person expense fails.
|
||||
/// </summary>
|
||||
public record DeletePersonExpenseFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action to update person expense status (Open/Done).
|
||||
/// </summary>
|
||||
public record UpdatePersonExpenseStatusAction(UpdatePersonExpenseStatusDto Dto);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when person expense status is updated successfully.
|
||||
/// </summary>
|
||||
public record UpdatePersonExpenseStatusSuccessAction(PersonExpenseDto Expense);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when updating person expense status fails.
|
||||
/// </summary>
|
||||
public record UpdatePersonExpenseStatusFailureAction(string Error);
|
||||
|
|
|
|||
|
|
@ -11,15 +11,24 @@ public class DayEffects
|
|||
{
|
||||
private readonly IDayService _dayService;
|
||||
private readonly IPersonService _personService;
|
||||
private readonly IExpenseService _expenseService;
|
||||
private readonly IPersonExpenseService _personExpenseService;
|
||||
private readonly ILogger<DayEffects> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DayEffects class.
|
||||
/// </summary>
|
||||
public DayEffects(IDayService dayService, IPersonService personService, ILogger<DayEffects> logger)
|
||||
public DayEffects(
|
||||
IDayService dayService,
|
||||
IPersonService personService,
|
||||
IExpenseService expenseService,
|
||||
IPersonExpenseService personExpenseService,
|
||||
ILogger<DayEffects> logger)
|
||||
{
|
||||
_dayService = dayService;
|
||||
_personService = personService;
|
||||
_expenseService = expenseService;
|
||||
_personExpenseService = personExpenseService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -218,4 +227,135 @@ public class DayEffects
|
|||
dispatcher.Dispatch(new LoadAvailablePersonsFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// PersonExpense Effects
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadDayExpensesAction - loads expenses for a day.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleLoadDayExpenses(LoadDayExpensesAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Loading expenses for day {DayId}", action.DayId);
|
||||
var expenses = await _personExpenseService.GetByDayIdAsync(action.DayId);
|
||||
dispatcher.Dispatch(new LoadDayExpensesSuccessAction(expenses));
|
||||
_logger.LogInformation("Loaded {Count} expenses for day {DayId}", expenses.Count, action.DayId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load expenses for day {DayId}", action.DayId);
|
||||
dispatcher.Dispatch(new LoadDayExpensesFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadAvailableExpensesAction - loads expense templates.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleLoadAvailableExpenses(LoadAvailableExpensesAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Loading available expense templates");
|
||||
var expenses = await _expenseService.GetAllAsync();
|
||||
dispatcher.Dispatch(new LoadAvailableExpensesSuccessAction(expenses));
|
||||
_logger.LogInformation("Loaded {Count} available expense templates", expenses.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load available expense templates");
|
||||
dispatcher.Dispatch(new LoadAvailableExpensesFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreatePersonExpenseAction - creates a person expense.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleCreatePersonExpense(CreatePersonExpenseAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Creating person expense for person {PersonId}, expense {ExpenseId}", action.Dto.PersonId, action.Dto.ExpenseId);
|
||||
var expense = await _personExpenseService.CreateAsync(action.Dto);
|
||||
dispatcher.Dispatch(new CreatePersonExpenseSuccessAction(expense));
|
||||
_logger.LogInformation("Created person expense {Id}", expense.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create person expense");
|
||||
dispatcher.Dispatch(new CreatePersonExpenseFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreateInversePersonExpenseAction - creates inverse expenses.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleCreateInversePersonExpense(CreateInversePersonExpenseAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Creating inverse expense (excluded: {ExcludedPersonId}) for expense {ExpenseId}", action.Dto.ExcludedPersonId, action.Dto.ExpenseId);
|
||||
var expenses = await _personExpenseService.CreateInverseAsync(action.Dto);
|
||||
dispatcher.Dispatch(new CreateInversePersonExpenseSuccessAction(expenses));
|
||||
_logger.LogInformation("Created {Count} inverse expenses", expenses.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create inverse expense");
|
||||
dispatcher.Dispatch(new CreateInversePersonExpenseFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles DeletePersonExpenseAction - deletes a person expense.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleDeletePersonExpense(DeletePersonExpenseAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Deleting person expense {Id}", action.Id);
|
||||
var success = await _personExpenseService.DeleteAsync(action.Id);
|
||||
|
||||
if (success)
|
||||
{
|
||||
dispatcher.Dispatch(new DeletePersonExpenseSuccessAction(action.Id));
|
||||
_logger.LogInformation("Deleted person expense {Id}", action.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcher.Dispatch(new DeletePersonExpenseFailureAction("Strafe konnte nicht gelöscht werden."));
|
||||
_logger.LogWarning("Failed to delete person expense {Id}", action.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete person expense {Id}", action.Id);
|
||||
dispatcher.Dispatch(new DeletePersonExpenseFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles UpdatePersonExpenseStatusAction - updates expense status.
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public async Task HandleUpdatePersonExpenseStatus(UpdatePersonExpenseStatusAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Updating person expense {Id} status to {Status}", action.Dto.Id, action.Dto.Status);
|
||||
var expense = await _personExpenseService.UpdateStatusAsync(action.Dto);
|
||||
dispatcher.Dispatch(new UpdatePersonExpenseStatusSuccessAction(expense));
|
||||
_logger.LogInformation("Updated person expense {Id} status to {Status}", expense.Id, expense.PersonExpenseStatus);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update person expense {Id} status", action.Dto.Id);
|
||||
dispatcher.Dispatch(new UpdatePersonExpenseStatusFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,4 +420,206 @@ public static class DayReducers
|
|||
{
|
||||
Filter = action.Filter
|
||||
};
|
||||
|
||||
// PersonExpense Reducers
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadDayExpensesAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(LoadDayExpensesAction))]
|
||||
public static DayState OnLoadDayExpenses(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadDayExpensesSuccessAction - sets expenses.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnLoadDayExpensesSuccess(DayState state, LoadDayExpensesSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = action.Expenses,
|
||||
IsLoadingExpenses = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadDayExpensesFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnLoadDayExpensesFailure(DayState state, LoadDayExpensesFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = false,
|
||||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadAvailableExpensesAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(LoadAvailableExpensesAction))]
|
||||
public static DayState OnLoadAvailableExpenses(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoading = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadAvailableExpensesSuccessAction - sets available expenses.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnLoadAvailableExpensesSuccess(DayState state, LoadAvailableExpensesSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
AvailableExpenses = action.Expenses,
|
||||
IsLoading = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles LoadAvailableExpensesFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnLoadAvailableExpensesFailure(DayState state, LoadAvailableExpensesFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoading = false,
|
||||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreatePersonExpenseAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(CreatePersonExpenseAction))]
|
||||
public static DayState OnCreatePersonExpense(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreatePersonExpenseSuccessAction - adds expense to list.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnCreatePersonExpenseSuccess(DayState state, CreatePersonExpenseSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = [.. state.SelectedDayExpenses, action.Expense],
|
||||
IsLoadingExpenses = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreatePersonExpenseFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnCreatePersonExpenseFailure(DayState state, CreatePersonExpenseFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = false,
|
||||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreateInversePersonExpenseAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(CreateInversePersonExpenseAction))]
|
||||
public static DayState OnCreateInversePersonExpense(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreateInversePersonExpenseSuccessAction - adds expenses to list.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnCreateInversePersonExpenseSuccess(DayState state, CreateInversePersonExpenseSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = [.. state.SelectedDayExpenses, .. action.Expenses],
|
||||
IsLoadingExpenses = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles CreateInversePersonExpenseFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnCreateInversePersonExpenseFailure(DayState state, CreateInversePersonExpenseFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = false,
|
||||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles DeletePersonExpenseAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(DeletePersonExpenseAction))]
|
||||
public static DayState OnDeletePersonExpense(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles DeletePersonExpenseSuccessAction - removes expense from list.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnDeletePersonExpenseSuccess(DayState state, DeletePersonExpenseSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = state.SelectedDayExpenses.Where(e => e.Id != action.Id).ToList(),
|
||||
IsLoadingExpenses = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles DeletePersonExpenseFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnDeletePersonExpenseFailure(DayState state, DeletePersonExpenseFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = false,
|
||||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles UpdatePersonExpenseStatusAction - sets loading state.
|
||||
/// </summary>
|
||||
[ReducerMethod(typeof(UpdatePersonExpenseStatusAction))]
|
||||
public static DayState OnUpdatePersonExpenseStatus(DayState state)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = true,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles UpdatePersonExpenseStatusSuccessAction - updates expense in list.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnUpdatePersonExpenseStatusSuccess(DayState state, UpdatePersonExpenseStatusSuccessAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = state.SelectedDayExpenses
|
||||
.Select(e => e.Id == action.Expense.Id ? action.Expense : e)
|
||||
.ToList(),
|
||||
IsLoadingExpenses = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles UpdatePersonExpenseStatusFailureAction - sets error state.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnUpdatePersonExpenseStatusFailure(DayState state, UpdatePersonExpenseStatusFailureAction action)
|
||||
=> state with
|
||||
{
|
||||
IsLoadingExpenses = false,
|
||||
Error = action.Error
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ public record DayState
|
|||
/// </summary>
|
||||
public DayDto? SelectedDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PersonExpenses for the currently selected day.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PersonExpenseDto> SelectedDayExpenses { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Available expense templates for the current club.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ExpenseDto> AvailableExpenses { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Available persons for participant selection.
|
||||
/// </summary>
|
||||
|
|
@ -34,6 +44,11 @@ public record DayState
|
|||
/// </summary>
|
||||
public bool IsLoading { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether expenses are loading.
|
||||
/// </summary>
|
||||
public bool IsLoadingExpenses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if operation failed.
|
||||
/// </summary>
|
||||
|
|
@ -51,9 +66,12 @@ public record DayState
|
|||
{
|
||||
Days = [],
|
||||
SelectedDay = null,
|
||||
SelectedDayExpenses = [],
|
||||
AvailableExpenses = [],
|
||||
AvailablePersons = [],
|
||||
Filter = null,
|
||||
IsLoading = false,
|
||||
IsLoadingExpenses = false,
|
||||
Error = null
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue