feat(usability): confirm saves and guard booking submissions against duplicates (U07)
This commit is contained in:
@@ -5,6 +5,8 @@ namespace Duempelkas.App.Components.Accounts;
|
||||
|
||||
public partial class EntryRow
|
||||
{
|
||||
[Parameter] public bool Highlighted { get; set; }
|
||||
|
||||
#region Parameters
|
||||
|
||||
[Parameter]
|
||||
@@ -26,6 +28,7 @@ public partial class EntryRow
|
||||
private string GetRowClass()
|
||||
{
|
||||
var classes = new List<string>();
|
||||
if (Highlighted) classes.Add("entry-recent");
|
||||
if (Entry.IsTransfer)
|
||||
{
|
||||
classes.Add("entry-row-transfer");
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<tbody>
|
||||
@foreach (var entry in Entries)
|
||||
{
|
||||
<EntryRow Entry="entry" OnDelete="OnDeleteEntry" OnRestore="OnRestoreEntry" OnEdit="OnEditEntry" />
|
||||
<EntryRow @key="entry.Id" Highlighted="@(entry.Id == HighlightedEntryId)" Entry="entry" OnDelete="OnDeleteEntry" OnRestore="OnRestoreEntry" OnEdit="OnEditEntry" />
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Duempelkas.App.Components.Accounts;
|
||||
|
||||
public partial class EntryTable
|
||||
{
|
||||
[Parameter] public int? HighlightedEntryId { get; set; }
|
||||
[Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden.";
|
||||
#region Parameters
|
||||
|
||||
|
||||
@@ -4,54 +4,65 @@
|
||||
<div class="dialog-content booking-dialog" role="dialog" aria-modal="true" aria-labelledby="entry-dialog-title">
|
||||
<h5 id="entry-dialog-title">@(EditEntry != null ? "Buchung bearbeiten" : "Einnahme oder Ausgabe erfassen") – @AccountName</h5>
|
||||
|
||||
<div class="form-container">
|
||||
<label class="form-label" for="entry-type">Art</label>
|
||||
@if (EditEntry == null)
|
||||
<form @onsubmit="Save" @onsubmit:preventDefault="true" aria-busy="@(isSaving ? "true" : "false")">
|
||||
<fieldset disabled="@(isSaving || committedResult != null || completed)">
|
||||
<div class="form-container">
|
||||
<label class="form-label" for="entry-type">Art</label>
|
||||
@if (EditEntry == null)
|
||||
{
|
||||
<select id="entry-type" @bind="entryType">
|
||||
<option value="@EntryType.Income">Einnahme</option>
|
||||
<option value="@EntryType.Expense">Ausgabe</option>
|
||||
</select>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div><strong>@(entryType == EntryType.Income ? "Einnahme" : "Ausgabe")</strong>
|
||||
<p class="text-muted">Falsche Art? Diese Buchung abbrechen, in der Kontoliste löschen und mit der richtigen Art neu erfassen.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<label class="form-label" for="entry-date">Datum</label>
|
||||
<div>
|
||||
<input id="entry-date" type="date" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" aria-describedby="entry-date-help" aria-invalid="@(date == default ? "true" : "false")" />
|
||||
@if (date == default) { <p id="entry-date-help" class="text-danger">Bitte ein gültiges Datum eingeben.</p> }
|
||||
</div>
|
||||
|
||||
<label class="form-label" for="entry-title">Beschreibung</label>
|
||||
<div>
|
||||
<input id="entry-title" type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="z. B. Mitgliedsbeitrag oder Porto" aria-describedby="entry-title-help" aria-invalid="@(string.IsNullOrWhiteSpace(title) ? "true" : "false")" />
|
||||
@if (string.IsNullOrWhiteSpace(title)) { <p id="entry-title-help" class="text-danger">Bitte beschreiben, wofür das Geld eingenommen oder ausgegeben wurde.</p> }
|
||||
</div>
|
||||
|
||||
<label class="form-label" for="entry-amount">Betrag (€)</label>
|
||||
<div>
|
||||
<input id="entry-amount" type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" placeholder="12,50" aria-describedby="entry-amount-help" aria-invalid="@(!ValidAmount ? "true" : "false")" />
|
||||
<p id="entry-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0. Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen, auch beim Einfügen. Es wird nicht gerundet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear && date != default)
|
||||
{
|
||||
<div class="alert alert-warning" role="status">
|
||||
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
|
||||
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
</fieldset>
|
||||
@if (isSaving) { <p role="status" aria-live="polite">Wird gespeichert …</p> }
|
||||
@if (saveError != null)
|
||||
{
|
||||
<select id="entry-type" @bind="entryType">
|
||||
<option value="@EntryType.Income">Einnahme</option>
|
||||
<option value="@EntryType.Expense">Ausgabe</option>
|
||||
</select>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div><strong>@(entryType == EntryType.Income ? "Einnahme" : "Ausgabe")</strong>
|
||||
<p class="text-muted">Falsche Art? Diese Buchung abbrechen, in der Kontoliste löschen und mit der richtigen Art neu erfassen.</p>
|
||||
<div class="alert alert-danger" role="alert">@saveError
|
||||
<details><summary>Technische Details</summary>@saveDetails</details>
|
||||
</div>
|
||||
}
|
||||
|
||||
<label class="form-label" for="entry-date">Datum</label>
|
||||
<div>
|
||||
<input id="entry-date" type="date" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" aria-describedby="entry-date-help" aria-invalid="@(date == default ? "true" : "false")" />
|
||||
@if (date == default) { <p id="entry-date-help" class="text-danger">Bitte ein gültiges Datum eingeben.</p> }
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-success" disabled="@(isSaving || completed || (committedResult == null && !CanSave))">
|
||||
@(isSaving ? "Wird gespeichert …" : committedResult != null ? "Ansicht aktualisieren" : "Speichern")
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled="@isSaving" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<label class="form-label" for="entry-title">Beschreibung</label>
|
||||
<div>
|
||||
<input id="entry-title" type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="z. B. Mitgliedsbeitrag oder Porto" aria-describedby="entry-title-help" aria-invalid="@(string.IsNullOrWhiteSpace(title) ? "true" : "false")" />
|
||||
@if (string.IsNullOrWhiteSpace(title)) { <p id="entry-title-help" class="text-danger">Bitte beschreiben, wofür das Geld eingenommen oder ausgegeben wurde.</p> }
|
||||
</div>
|
||||
|
||||
<label class="form-label" for="entry-amount">Betrag (€)</label>
|
||||
<div>
|
||||
<input id="entry-amount" type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" placeholder="12,50" aria-describedby="entry-amount-help" aria-invalid="@(!ValidAmount ? "true" : "false")" />
|
||||
<p id="entry-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0. Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen, auch beim Einfügen. Es wird nicht gerundet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear && date != default)
|
||||
{
|
||||
<div class="alert alert-warning" role="status">
|
||||
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
|
||||
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-success" @onclick="Save" disabled="@(!CanSave)">
|
||||
<i class="bi bi-check-lg"></i> Speichern
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ public partial class AddEntryDialog
|
||||
public EntryDto? EditEntry { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<DateTime> OnSave { get; set; }
|
||||
public EventCallback<BookingSaved> OnSave { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
@@ -30,6 +30,12 @@ public partial class AddEntryDialog
|
||||
[Parameter] public string AccountName { get; set; } = string.Empty;
|
||||
private bool CanSave => !string.IsNullOrWhiteSpace(title) && ValidAmount && date != default && (!OutsideYear || (confirmDifferentYear && confirmedYear == date.Year));
|
||||
|
||||
private bool isSaving;
|
||||
private bool completed;
|
||||
private BookingSaved? committedResult;
|
||||
private string? saveError;
|
||||
private string? saveDetails;
|
||||
|
||||
#region Fields
|
||||
|
||||
private EntryType entryType = EntryType.Income;
|
||||
@@ -66,20 +72,40 @@ public partial class AddEntryDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (!CanSave) return;
|
||||
if (EditEntry != null)
|
||||
if (isSaving || completed || (committedResult == null && !CanSave)) return;
|
||||
isSaving = true;
|
||||
saveError = null;
|
||||
saveDetails = null;
|
||||
try
|
||||
{
|
||||
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
|
||||
if (committedResult == null)
|
||||
{
|
||||
var id = EditEntry?.Id ?? 0;
|
||||
if (EditEntry != null)
|
||||
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
|
||||
else
|
||||
id = (await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount)).Id;
|
||||
committedResult = new(id, date, entryType == EntryType.Income ? "Einnahme" : "Ausgabe", amount, EditEntry != null);
|
||||
}
|
||||
// If refreshing the page fails after commit, retry only notification,
|
||||
// never create a second booking.
|
||||
await OnSave.InvokeAsync(committedResult);
|
||||
completed = true;
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount);
|
||||
saveError = committedResult == null
|
||||
? "Speichern fehlgeschlagen. Die Eingaben bleiben erhalten. Bitte erneut versuchen."
|
||||
: "Die Buchung ist gespeichert. Die Ansicht konnte nicht aktualisiert werden. Bitte Ansicht aktualisieren; es wird keine weitere Buchung erzeugt.";
|
||||
saveDetails = ex.Message;
|
||||
}
|
||||
|
||||
await OnSave.InvokeAsync(date);
|
||||
finally { isSaving = false; }
|
||||
}
|
||||
|
||||
private async Task Cancel() => await OnCancel.InvokeAsync();
|
||||
private async Task Cancel()
|
||||
{
|
||||
if (!isSaving) await OnCancel.InvokeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -7,40 +7,52 @@
|
||||
|
||||
<p class="text-muted">Erfasst nur die Buchung. Führt keine Banküberweisung aus.</p>
|
||||
|
||||
<div class="form-container">
|
||||
<label class="form-label">Zielkonto</label>
|
||||
<select class="form-select" @bind="targetAccountId">
|
||||
<option value="0">— Auswählen —</option>
|
||||
@foreach (var acc in accounts.Where(a => a.Id != SourceAccountId))
|
||||
<form @onsubmit="Save" @onsubmit:preventDefault="true" aria-busy="@(isSaving ? "true" : "false")">
|
||||
<fieldset disabled="@(isSaving || committedResult != null || completed)">
|
||||
<div class="form-container">
|
||||
<label class="form-label">Zielkonto</label>
|
||||
<select id="transfer-account" class="form-select" @bind="targetAccountId">
|
||||
<option value="0">— Auswählen —</option>
|
||||
@foreach (var acc in accounts.Where(a => a.Id != SourceAccountId))
|
||||
{
|
||||
<option value="@acc.Id">@acc.Name</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<label class="form-label">Datum</label>
|
||||
<input id="transfer-date" type="date" class="form-control" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" />
|
||||
|
||||
<label class="form-label">Bezeichnung</label>
|
||||
<input id="transfer-title" type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="Beschreibung der Umbuchung" />
|
||||
|
||||
<label class="form-label">Betrag (€)</label>
|
||||
<div><input id="transfer-amount" type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" aria-describedby="transfer-amount-help" />
|
||||
<p id="transfer-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0; Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen; keine Rundung.</p></div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear)
|
||||
{
|
||||
<option value="@acc.Id">@acc.Name</option>
|
||||
<div class="alert alert-warning" role="status">
|
||||
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
|
||||
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
|
||||
</div>
|
||||
}
|
||||
</select>
|
||||
|
||||
<label class="form-label">Datum</label>
|
||||
<input type="date" class="form-control" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" />
|
||||
|
||||
<label class="form-label">Bezeichnung</label>
|
||||
<input type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="Beschreibung der Umbuchung" />
|
||||
|
||||
<label class="form-label">Betrag (€)</label>
|
||||
<div><input type="text" inputmode="decimal" class="form-control" @bind="amountText" @bind:event="oninput" aria-describedby="transfer-amount-help" />
|
||||
<p id="transfer-amount-help" class="@(!ValidAmount ? "text-danger" : "text-muted")">Betrag ohne Minuszeichen, größer als 0; Komma oder Punkt, höchstens zwei Nachkommastellen. Keine Tausendertrenner oder Währungszeichen; keine Rundung.</p></div>
|
||||
</div>
|
||||
|
||||
@if (OutsideYear)
|
||||
{
|
||||
<div class="alert alert-warning" role="status">
|
||||
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
|
||||
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
|
||||
</fieldset>
|
||||
@if (isSaving) { <p role="status" aria-live="polite">Wird gespeichert …</p> }
|
||||
@if (saveError != null)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@saveError
|
||||
<details><summary>Technische Details</summary>@saveDetails</details>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-success" disabled="@(isSaving || completed || (committedResult == null && !CanSave))">
|
||||
@(isSaving ? "Wird gespeichert …" : committedResult != null ? "Ansicht aktualisieren" : "Speichern")
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" disabled="@isSaving" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-success" @onclick="Save"
|
||||
disabled="@(!CanSave || (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)))"><i class="bi bi-arrow-left-right"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class AddTransferDialog
|
||||
public EntryDto? EditEntry { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<DateTime> OnSave { get; set; }
|
||||
public EventCallback<BookingSaved> OnSave { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnCancel { get; set; }
|
||||
@@ -26,6 +26,12 @@ public partial class AddTransferDialog
|
||||
private int? confirmedYear;
|
||||
private bool OutsideYear => ContextYear.HasValue && date.Year != ContextYear.Value;
|
||||
|
||||
private bool isSaving;
|
||||
private bool completed;
|
||||
private BookingSaved? committedResult;
|
||||
private string? saveError;
|
||||
private string? saveDetails;
|
||||
|
||||
#region Fields
|
||||
|
||||
private List<AccountSummaryDto> accounts = new();
|
||||
@@ -74,20 +80,40 @@ public partial class AddTransferDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (!CanSave) return;
|
||||
if (EditEntry != null)
|
||||
if (isSaving || completed || (committedResult == null && !CanSave)) return;
|
||||
isSaving = true;
|
||||
saveError = null;
|
||||
saveDetails = null;
|
||||
try
|
||||
{
|
||||
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
|
||||
if (committedResult == null)
|
||||
{
|
||||
var id = EditEntry?.Id ?? 0;
|
||||
if (EditEntry != null)
|
||||
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
|
||||
else
|
||||
id = await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
|
||||
committedResult = new(id, date, "Umbuchung", amount, EditEntry != null);
|
||||
}
|
||||
// If refreshing the page fails after commit, retry only notification,
|
||||
// never create a second booking.
|
||||
await OnSave.InvokeAsync(committedResult);
|
||||
completed = true;
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
|
||||
saveError = committedResult == null
|
||||
? "Speichern fehlgeschlagen. Die Eingaben bleiben erhalten. Bitte erneut versuchen."
|
||||
: "Die Buchung ist gespeichert. Die Ansicht konnte nicht aktualisiert werden. Bitte Ansicht aktualisieren; es wird keine weitere Buchung erzeugt.";
|
||||
saveDetails = ex.Message;
|
||||
}
|
||||
|
||||
await OnSave.InvokeAsync(date);
|
||||
finally { isSaving = false; }
|
||||
}
|
||||
|
||||
private async Task Cancel() => await OnCancel.InvokeAsync();
|
||||
private async Task Cancel()
|
||||
{
|
||||
if (!isSaving) await OnCancel.InvokeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
<OperationNotice Result="fileResult" />
|
||||
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
|
||||
|
||||
@if (lastSaved != null)
|
||||
{
|
||||
<div class="alert alert-success" role="status" aria-live="polite">
|
||||
@lastSaved.Kind über @FormatAmount(lastSaved.Amount) @(lastSaved.Edited ? "aktualisiert" : "gespeichert").
|
||||
</div>
|
||||
}
|
||||
@if (savedOutsideYear.HasValue)
|
||||
{
|
||||
<div class="alert alert-info" role="status">
|
||||
@@ -39,7 +45,7 @@
|
||||
<button class="btn btn-secondary btn-nav" @onclick="NavigateBack">
|
||||
<i class="bi bi-arrow-left"></i> Zurück
|
||||
</button>
|
||||
<button class="btn btn-success btn-nav" @onclick="() => showAddEntry = true">
|
||||
<button class="btn btn-success btn-nav" @onclick="() => showAddEntry = true">
|
||||
<i class="bi bi-plus-lg"></i> Einnahme oder Ausgabe erfassen
|
||||
</button>
|
||||
<button class="btn btn-info btn-nav" @onclick="() => showAddTransfer = true">
|
||||
@@ -48,7 +54,7 @@
|
||||
<button class="btn btn-dark btn-nav" @onclick="HandleExport" disabled="@fileBusy">
|
||||
<i class="bi bi-file-earmark-pdf"></i> Als PDF speichern
|
||||
</button>
|
||||
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
||||
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
||||
<i class="bi bi-funnel"></i> @GetFilterLabel()
|
||||
</button>
|
||||
</div>
|
||||
@@ -103,7 +109,7 @@
|
||||
@if (entries != null)
|
||||
{
|
||||
<div class="entries-scroll-wrapper">
|
||||
<EntryTable Entries="entries" EmptyMessage="emptyEntriesMessage" OnDeleteEntry="RequestDeleteEntry" OnRestoreEntry="RequestRestoreEntry" OnEditEntry="RequestEditEntry" />
|
||||
<EntryTable HighlightedEntryId="lastSaved?.EntryId" Entries="entries" EmptyMessage="emptyEntriesMessage" OnDeleteEntry="RequestDeleteEntry" OnRestoreEntry="RequestRestoreEntry" OnEditEntry="RequestEditEntry" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public partial class AccountDetail
|
||||
private bool fileBusy;
|
||||
|
||||
private int? savedOutsideYear;
|
||||
private BookingSaved? lastSaved;
|
||||
private string emptyEntriesMessage = "Noch keine Buchungen vorhanden.";
|
||||
private EntryDto? editingEntry;
|
||||
private EntryDto? editingTransferEntry;
|
||||
@@ -105,18 +106,19 @@ public partial class AccountDetail
|
||||
await LoadAll();
|
||||
}
|
||||
|
||||
private async Task HandleEntryCreated(DateTime savedDate)
|
||||
{
|
||||
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||
showAddEntry = false;
|
||||
await LoadAll();
|
||||
}
|
||||
private Task HandleEntryCreated(BookingSaved saved) => HandleBookingSaved(saved);
|
||||
|
||||
private async Task HandleTransferCreated(DateTime savedDate)
|
||||
private Task HandleTransferCreated(BookingSaved saved) => HandleBookingSaved(saved);
|
||||
|
||||
private async Task HandleBookingSaved(BookingSaved saved)
|
||||
{
|
||||
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||
showAddTransfer = false;
|
||||
await LoadAll();
|
||||
lastSaved = saved;
|
||||
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != saved.Date.Year ? saved.Date.Year : null;
|
||||
showAddEntry = false;
|
||||
showAddTransfer = false;
|
||||
editingEntry = null;
|
||||
editingTransferEntry = null;
|
||||
}
|
||||
|
||||
private Task ShowSavedYear() => SelectFilterYear(savedOutsideYear);
|
||||
@@ -180,13 +182,7 @@ public partial class AccountDetail
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleEntryEdited(DateTime savedDate)
|
||||
{
|
||||
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||
editingEntry = null;
|
||||
editingTransferEntry = null;
|
||||
await LoadAll();
|
||||
}
|
||||
private Task HandleEntryEdited(BookingSaved saved) => HandleBookingSaved(saved);
|
||||
|
||||
private async Task HandleExport()
|
||||
{
|
||||
@@ -234,7 +230,7 @@ public partial class AccountDetail
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static string FormatAmount(decimal amount) => $"{amount:N2} €";
|
||||
private static string FormatAmount(decimal amount) => amount.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("de-DE")) + " €";
|
||||
|
||||
private string GetFilterLabel() => Period.Label;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public interface IEntryService
|
||||
Task<List<int>> GetEntryYearsAsync(int accountId);
|
||||
Task<List<int>> GetAllEntryYearsAsync();
|
||||
Task<EntryDto> CreateEntryAsync(int accountId, EntryType type, DateTime date, string title, decimal amount);
|
||||
Task CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount);
|
||||
Task<int> CreateTransferAsync(int sourceAccountId, int targetAccountId, DateTime date, string title, decimal amount);
|
||||
Task DeleteEntryAsync(int entryId);
|
||||
Task RestoreEntryAsync(int entryId);
|
||||
Task UpdateEntryAsync(int entryId, DateTime date, string title, decimal amount);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Duempelkas.App.Services.Models;
|
||||
|
||||
public sealed record BookingSaved(int EntryId, DateTime Date, string Kind, decimal Amount, bool Edited);
|
||||
Reference in New Issue
Block a user