WIP: quote listing and status update

This commit is contained in:
Frede Hundewadt 2022-12-15 17:05:09 +01:00
parent c54403472e
commit ef50cbc3ff
13 changed files with 297 additions and 145 deletions

View file

@ -0,0 +1,99 @@
@using Wonky.Entity.Views
@using Wonky.Client.Models
<div class="list-group list-group-flush">
<div class="list-group-item bg-dark text-white">
<div class="row g-3">
<div class="col-sm-2">
Reference
</div>
<div class="col-sm-4">
Kunde
</div>
<div class="col-sm-2">
Dato
</div>
<div class="col-sm-1">
Status
</div>
<div class="col-sm-3"></div>
</div>
</div>
@if (Quotes.Any())
{
foreach (var quote in Quotes)
{
<div class="list-group-item list-group-item-action">
<div class="row align-items-center">
<div class="col-sm-2">
<a class="btn btn-outline-dark d-block" style="font-family:monospace;font-size: 14px;"
href="/companies/@quote.Company.CompanyId/quotes/@quote.ActivityId">@quote.ESalesNumber</a>
</div>
<div class="col-sm-4">
@quote.Company.Name
</div>
<di class="col-sm-2">
@quote.OrderDate
</di>
<div class="col-sm-1">
@switch (quote.QuoteStatusEnum)
{
case "None":
<i class="bi-question-circle-fill"></i>
break;
case "Win":
<span>Ordre</span>
break;
case "Lose":
<i class="bi-trash"></i>
break;
case "Note":
<i class="bi-tag-fill"></i>
break;
case "Archive":
<i class="bi-archive-fill"></i>
break;
default:
<i class="bi-question-circle-fill"></i>
break;
}
</div>
<div class="col-sm-3">
<button type="button" class="btn btn-outline-dark me-2" @onclick="() => SetQuote(quote.ESalesNumber, QStatus.Lose)"><i class="bi-trash2-fill"></i></button>
<button type="button" class="btn btn-outline-dark me-2" @onclick="() => SetQuote(quote.ESalesNumber, QStatus.Archive)"><i class="bi-archive-fill"></i> </button>
<button type="button" class="btn btn-outline-dark me-2" @onclick="() => SetQuote(quote.ESalesNumber, QStatus.Note)"><i class="bi-tag-fill"></i> </button>
<button type="button" class="btn btn-success" @onclick="() => SetQuote(quote.ESalesNumber, QStatus.Win)"><i class="bi-cart-fill"></i> </button>
</div>
@if (!string.IsNullOrWhiteSpace(quote.OfficeNote))
{
<div class="col-sm-2"></div>
<div class="col-sm-10">
<i class="bi-pencil"></i> @quote.OfficeNote
</div>
}
</div>
</div>
}
}
else
{
<div class="list-group-item">Ingen data</div>
}
</div>
@code {
[Parameter]
public List<ReportItemView> Quotes { get; set; } = new();
[Parameter] public EventCallback<QuoteCallbackArgs> OnChangedCallback { get; set; }
private async Task SetQuote(string eSalesNumber, QStatus status)
{
var args = new QuoteCallbackArgs()
{
ESalesNumber = eSalesNumber,
Status = status
};
await OnChangedCallback.InvokeAsync(args);
}
}

View file

@ -36,6 +36,12 @@ public static class Utils
return false;
}
}
public static string EnumToString(Enum value)
{
return value.ToString();
}
public static int GetHashFromNow()
{
return DateTime.Now.ToFileTimeUtc().GetHashCode();

View file

@ -31,7 +31,7 @@ public interface ICrmActivityHttpRepository
/// </summary>
/// <param name="activity"></param>
/// <returns></returns>
Task<ApiResponseView> AcceptQuote(ReportItemView activity);
Task<ApiResponseView> UpdateQuoteStatus(ReportItemView activity);
/// <summary>
/// Get activities by date

View file

@ -13,6 +13,7 @@
// along with this program. If not, see [https://www.gnu.org/licenses/agpl-3.0.en.html]
//
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
@ -60,14 +61,13 @@ public class CrmActivityHttpRepository : ICrmActivityHttpRepository
/// </summary>
/// <param name="activity"></param>
/// <returns></returns>
public async Task<ApiResponseView> AcceptQuote(ReportItemView activity)
public async Task<ApiResponseView> UpdateQuoteStatus(ReportItemView activity)
{
var response = await _client.PutAsJsonAsync(
$"{_api.CrmActivities}/{activity.ActivityId}/accept", activity, _options);
$"{_api.CrmActivities}/quote/{activity.ActivityId}", activity, _options);
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ApiResponseView>(content);
return result!;
_logger.LogDebug("UpdateQuote Response Content <= {}", content);
return JsonSerializer.Deserialize<ApiResponseView>(content, _options);
}
/// <summary>
@ -150,7 +150,9 @@ public class CrmActivityHttpRepository : ICrmActivityHttpRepository
/// <returns>ApiResponseView</returns>
public async Task<ApiResponseView> GetExpressState(string activityId)
{
var response = await _client.GetFromJsonAsync<ApiResponseView>($"{_api.CrmActivities}/express/{activityId}?status=Express", _options);
var response =
await _client.GetFromJsonAsync<ApiResponseView>($"{_api.CrmActivities}/express/{activityId}?status=Express",
_options);
return response ?? new ApiResponseView
{
Code = 404,

View file

@ -0,0 +1,11 @@
namespace Wonky.Client.Models;
public enum QStatus
{
None,
Win,
Lose,
Archive,
Note,
All
}

View file

@ -0,0 +1,7 @@
namespace Wonky.Client.Models;
public class QuoteCallbackArgs
{
public string ESalesNumber { get; set; } = "";
public QStatus Status { get; set; }
}

View file

@ -64,7 +64,7 @@ else
<option value="noSale">Ingen salg</option>
@if (!string.IsNullOrEmpty(Activity.VatNumber) && !string.IsNullOrWhiteSpace(Activity.Address1) && Company.HasFolded == 0)
{
@if (DraftStateProvider.Draft.DraftType == "order")
@if (DraftProvider.Draft.DraftType == "order")
{
<option selected value="order">Bestilling</option>
}
@ -73,7 +73,7 @@ else
<option value="order">Bestilling</option>
}
@if(DraftStateProvider.Draft.DraftType == "offer")
@if(DraftProvider.Draft.DraftType == "offer")
{
<option selected value="quote">Tilbud</option>
}
@ -158,10 +158,10 @@ else
<thead>
<tr class="bg-dark text-white">
<th scope="col" colspan="7">
Ordrekladde <span class="mx-2 draft-expires-msg">Global kladde (udløber efter @(DraftStateProvider.Draft.TimeToLiveInSeconds / 60)m inaktivitet)</span>
Ordrekladde <span class="mx-2 draft-expires-msg">Global kladde (udløber efter @(DraftProvider.Draft.TimeToLiveInSeconds / 60)m inaktivitet)</span>
</th>
<th scope="col" class="text-end">
<button type="button" class="btn btn-danger btn-sm" @onclick="@DeleteDraft" disabled="@(DraftStateProvider.Draft.Items.Count == 0)"><i class="bi-trash"></i> Slet kladde</button>
<button type="button" class="btn btn-danger btn-sm" @onclick="@DeleteDraft" disabled="@(DraftProvider.Draft.Items.Count == 0)"><i class="bi-trash"></i> Slet kladde</button>
</th>
</tr>
<tr class="bg-dark opacity-75 text-white">
@ -176,9 +176,9 @@ else
</tr>
</thead>
<tbody>
@if (DraftStateProvider.Draft.Items.Count > 0)
@if (DraftProvider.Draft.Items.Count > 0)
{
@foreach (var cartItem in DraftStateProvider.Draft.Items)
@foreach (var cartItem in DraftProvider.Draft.Items)
{
<tr>
<td class="align-middle">@cartItem.Item.Name</td>
@ -199,7 +199,7 @@ else
<tr>
<td colspan="4"></td>
<td class="align-middle text-black text-end fw-bold">Total</td>
<td class="align-middle text-black text-end fw-bold">@($"{DraftStateProvider.Draft.Total:N2}")</td>
<td class="align-middle text-black text-end fw-bold">@($"{DraftProvider.Draft.Total:N2}")</td>
<td></td>
<td class="align-middle text-end">
<button class="btn btn-primary" type="button" @onclick="CallPriceListModal">

View file

@ -34,7 +34,7 @@ namespace Wonky.Client.Pages;
public partial class CrmActivityNewPage : IDisposable
{
// Parameters
[CascadingParameter] DraftStateProvider DraftStateProvider { get; set; }
[CascadingParameter] DraftStateProvider DraftProvider { get; set; }
[Parameter] public string CompanyId { get; set; }
// Services
[Inject] public ILogger<CrmActivityNewPage> Logger { get; set; }
@ -45,8 +45,8 @@ public partial class CrmActivityNewPage : IDisposable
[Inject] public ILocalStorageService Storage { get; set; }
[Inject] public ICatalogHttpRepository Catalog { get; set; }
[Inject] public ICrmCompanyHttpRepository CompanyRepo { get; set; }
[Inject] public ICrmActivityHttpRepository CrmActivityRepo { get; set; }
[Inject] public ICrmReportHttpRepository CrmReportRepo { get; set; }
[Inject] public ICrmActivityHttpRepository ActivityRepo { get; set; }
[Inject] public ICrmReportHttpRepository ReportRepo { get; set; }
// variables
private readonly JsonSerializerOptions? _options = new() {PropertyNameCaseInsensitive = true};
private SalesItemView SelectedItem { get; set; } = new();
@ -132,7 +132,7 @@ public partial class CrmActivityNewPage : IDisposable
// Initialize date variable
SelectedDate = string.IsNullOrWhiteSpace(UserPrefs.WorkDate) ? DateTime.Now : DateTime.Parse(UserPrefs.WorkDate);
// raise flag if report is closed
ReportClosed = await CrmReportRepo.ReportExist($"{SelectedDate:yyyy-MM-dd}");
ReportClosed = await ReportRepo.ReportExist($"{SelectedDate:yyyy-MM-dd}");
// Ask for confirmation of date
Logger.LogDebug("Preferences.DateConfirmed => {}", UserPrefs.DateConfirmed);
if (!UserPrefs.DateConfirmed)
@ -141,7 +141,7 @@ public partial class CrmActivityNewPage : IDisposable
ConfirmWorkDate.Show();
}
// Lines may already have been added from the company inventory page
if (DraftStateProvider.Draft.DraftType == "order")
if (DraftProvider.Draft.DraftType == "order")
{
// set dropdown selection accordingly
Activity.ActivityTypeEnum = "onSite";
@ -168,7 +168,7 @@ public partial class CrmActivityNewPage : IDisposable
/// <param name="workDate"></param>
private async Task WorkDateComponentCallback(string workDate)
{
ReportClosed = await CrmReportRepo.ReportExist(workDate);
ReportClosed = await ReportRepo.ReportExist(workDate);
SelectedDate = DateTime.Parse(workDate);
Activity.ActivityDate = workDate;
}
@ -236,7 +236,7 @@ public partial class CrmActivityNewPage : IDisposable
switch (Activity.ActivityStatusEnum)
{
// don't accept order with no lines
case "order" when DraftStateProvider.Draft.Items.Count == 0:
case "order" when DraftProvider.Draft.Items.Count == 0:
Toast.ShowError("Ved bestilling skal der være en eller flere linjer i kladden.");
return;
// phone number is required if first time customer
@ -272,9 +272,9 @@ public partial class CrmActivityNewPage : IDisposable
// begin lines
Activity.Lines = new List<ActivityLineDto>();
var ln = 0;
if (DraftStateProvider.Draft.Items.Count != 0)
if (DraftProvider.Draft.Items.Count != 0)
{
var lines = DraftStateProvider.Draft.Items.Select(item => new ActivityLineDto
var lines = DraftProvider.Draft.Items.Select(item => new ActivityLineDto
{
Price = item.Price,
Discount = item.Discount,
@ -293,14 +293,14 @@ public partial class CrmActivityNewPage : IDisposable
// debug logging
Logger.LogDebug("CrmNewActivityPage => \n {}", JsonSerializer.Serialize(Activity));
// post to api
var result = await CrmActivityRepo.CreateActivity(Activity);
var result = await ActivityRepo.CreateActivity(Activity);
// debug logging
Logger.LogDebug("ApiResponseView => \n {}", JsonSerializer.Serialize(result));
// show result message
if (result.IsSuccess)
{
Toast.ShowSuccess($"{result.Message}",
DraftStateProvider.Draft.Items.Count == 0 ? "Besøg er oprettet" : "Bestilling/Tilbud er oprettet");
DraftProvider.Draft.Items.Count == 0 ? "Besøg er oprettet" : "Bestilling/Tilbud er oprettet");
await DeleteDraft();
Navigator.NavigateTo($"/companies");
return;
@ -316,7 +316,7 @@ public partial class CrmActivityNewPage : IDisposable
/// </summary>
private async Task DeleteDraft()
{
await DraftStateProvider.DeleteDraftAsync();
await DraftProvider.DeleteDraftAsync();
Activity.ActivityStatusEnum = "noSale";
}
@ -342,11 +342,11 @@ public partial class CrmActivityNewPage : IDisposable
Price = "0";
Discount = "0";
// add it to the cart
DraftStateProvider.Draft.Items.Add(item);
DraftProvider.Draft.Items.Add(item);
if(Activity.ActivityStatusEnum != "quote")
Activity.ActivityStatusEnum = "order";
// save the item using the CartStateProvider's save method
await DraftStateProvider.SaveChangesAsync();
await DraftProvider.SaveChangesAsync();
}
/// <summary>
@ -356,10 +356,10 @@ public partial class CrmActivityNewPage : IDisposable
private async Task RemoveItem(DraftItem item)
{
// remove item
DraftStateProvider.Draft.Items.Remove(item);
DraftProvider.Draft.Items.Remove(item);
// save the remaining draft
await DraftStateProvider.SaveChangesAsync();
if (!DraftStateProvider.Draft.Items.Any())
await DraftProvider.SaveChangesAsync();
if (!DraftProvider.Draft.Items.Any())
Activity.ActivityStatusEnum = "noSale";
}
@ -370,11 +370,20 @@ public partial class CrmActivityNewPage : IDisposable
/// <param name="e"></param>
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
DraftStateProvider.Draft.DraftType = Activity.ActivityStatusEnum;
Logger.LogDebug("ActivityNewPage => HandleFieldChanged => ActivityStatusEnum <= '{}'", Activity.ActivityStatusEnum);
DraftProvider.Draft.DraftType = Activity.ActivityStatusEnum;
if (Activity.ActivityStatusEnum == "noSale")
{
Logger.LogDebug("ActivityNewPage => ActivityStatusEnum == 'noSale' <= remove items");
DraftProvider.Draft.Items = new List<DraftItem>();
}
// InvalidCanvas = InvalidActivityType;
InvalidActivity = InvalidActivityType
|| PoFormInvalid
|| DraftStateProvider.Draft.Items.Count == 0
|| DraftProvider.Draft.Items.Count == 0
|| (Activity.ActivityStatusEnum == "offer" && string.IsNullOrWhiteSpace(Activity.Email));
if (Activity.YourRef.Length > 35 || Activity.ReferenceNumber.Length > 20 || InvalidActivity)
{
@ -395,6 +404,7 @@ public partial class CrmActivityNewPage : IDisposable
if (string.IsNullOrEmpty(Activity.ActivityTypeEnum) && !ReportClosed)
{
Toast.ShowWarning("Aktivitet type kan ikke være tom");
PoFormInvalid = true;
return;
}
PoFormInvalid = false;

View file

@ -1,69 +0,0 @@
@page "/open-quotes"
@using Wonky.Client.Components
<div class="row g-3">
<div class="col-sm-12">
<h3 class="text-center">Åbne tilbud</h3>
</div>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item bg-dark text-white">
<div class="row g-3">
<div class="col-sm-2">
Reference
</div>
<div class="col-sm-4">
Kunde
</div>
<div class="col-sm-1">
Dato
</div>
<div class="col-sm-2 text-end">
Tilbudssum
</div>
<div class="col-sm-3"></div>
</div>
</div>
@if (Quotes.Any())
{
foreach (var quote in Quotes)
{
<div class="list-group-item list-group-item-action">
<div class="row align-items-center">
<div class="col-sm-2" style="font-family: monospace;">
<a class="btn btn-outline-dark d-block" href="/companies/@quote.Company.CompanyId/quotes/@quote.ActivityId">@quote.ESalesNumber</a>
</div>
<div class="col-sm-4">
@quote.Company.Name
</div>
<di class="col-sm-1">
@quote.OrderDate
</di>
<div class="col-sm-2 text-end">
@($"{quote.OrderAmount:N2}")
</div>
<div class="col-sm-1"><button type="button" class="btn btn-outline-danger" @onclick="() => SetQuote(2)"><i class="bi-hand-thumbs-down-fill"></i> </button></div>
<div class="col-sm-1"><button type="button" class="btn btn-outline-dark" @onclick="() => SetQuote(3)"><i class="bi-trash2-fill"></i> </button></div>
<div class="col-sm-1"><button type="button" class="btn btn-outline-success" @onclick="() => SetQuote(1)"><i class="bi-hand-thumbs-up-fill"></i> </button></div>
<div class="col-sm-1"></div>
@if (!string.IsNullOrWhiteSpace(quote.OfficeNote))
{
<div class="col-sm-2"><i class="bi-pencil"></i> Note</div>
<div class="col-sm-10">
@quote.OfficeNote
</div>
}
</div>
</div>
}
}
else
{
<div class="list-group-item">Ingen data</div>
}
</div>
@if (Working)
{
<WorkingThreeDots/>
}

View file

@ -1,39 +0,0 @@
using Microsoft.AspNetCore.Components;
using Wonky.Client.HttpInterceptors;
using Wonky.Client.HttpInterfaces;
using Wonky.Entity.DTO;
using Wonky.Entity.Views;
namespace Wonky.Client.Pages;
public partial class CrmQuotes : IDisposable
{
[Inject] public ICrmActivityHttpRepository ActivityRepo { get; set; }
[Inject] public HttpInterceptorService Interceptor { get; set; }
private List<ReportItemView> Quotes { get; set; } = new();
private bool Working { get; set; } = true;
protected override async Task OnInitializedAsync()
{
Interceptor.RegisterEvent();
Interceptor.RegisterBeforeSendEvent();
Quotes = await ActivityRepo.GetQuotes();
if (Quotes.Any())
Quotes = Quotes.OrderBy(x => x.CreateTimestamp).ToList();
Working = false;
}
private void SetQuote(int status)
{
// todo - implement update quote from status
// status matches QuoteStatusEnum
// 0 - None
// 1 - Win
// 2 - Lose
// 3 - Draw
}
public void Dispose()
{
Interceptor.DisposeEvent();
}
}

View file

@ -0,0 +1,44 @@
@page "/open-quotes"
@using Wonky.Client.Components
@using Wonky.Client.Models
<div class="row g-3 align-items-center">
<div class="col-sm-9">
<h2>Tilbud</h2>
</div>
<div class="col-sm-3">
<div class="btn-group btn-group" role="group" aria-label="Filter tilbud">
<input type="radio" class="btn-check" name="btn-filter" id="btn-filter1" autocomplete="off" @onchange="() => FilterQuotes(QStatus.Lose)"/>
<label class="btn btn-dark" for="btn-filter1"><i class="bi-trash-fill"></i></label>
<input type="radio" class="btn-check" name="btn-filter" id="btn-filter2" autocomplete="off" @onchange="() => FilterQuotes(QStatus.Archive)"/>
<label class="btn btn-dark" for="btn-filter2"><i class="bi-archive-fill"></i></label>
<input type="radio" class="btn-check" name="btn-filter" id="btn-filter3" autocomplete="off" @onchange="() => FilterQuotes(QStatus.Note)"/>
<label class="btn btn-dark" for="btn-filter3"><i class="bi-tag-fill"></i></label>
<input type="radio" class="btn-check" name="btn-filter" id="btn-filter4" autocomplete="off" @onchange="() => FilterQuotes(QStatus.All)"/>
<label class="btn btn-dark" for="btn-filter4"><i class="bi-list"></i></label>
<input type="radio" class="btn-check" name="btn-filter" id="btn-filter5" autocomplete="off" checked @onchange="() => FilterQuotes(QStatus.None)"/>
<label class="btn btn-dark" for="btn-filter5"><i class="bi-question-circle-fill"></i></label>
</div>
@*
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showAll"
@bind-Value="Filtered" @bind-Value:event="onchange" @onclick="FilterQuotes" disabled="@(!Quotes.Any())"/>
<label for="showAll" class="form-check-label">Vis alle</label>
</div>
*@
</div>
</div>
<QuoteListComponent Quotes="_quotes" OnChangedCallback="UpdateQuoteCallback" />
@if (Working)
{
<WorkingThreeDots/>
}

View file

@ -0,0 +1,81 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Policy;
using System.Text.Json;
using Blazored.LocalStorage;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Wonky.Client.Helpers;
using Wonky.Client.HttpInterceptors;
using Wonky.Client.HttpInterfaces;
using Wonky.Client.Models;
using Wonky.Entity.DTO;
using Wonky.Entity.Views;
namespace Wonky.Client.Pages;
public partial class CrmQuotesListPage : IDisposable
{
[Inject] public ICrmActivityHttpRepository ActivityRepo { get; set; }
[Inject] public HttpInterceptorService Interceptor { get; set; }
[Inject] public ILogger<CrmQuotesListPage> Logger { get; set; }
[Inject] public IToastService Toaster { get; set; }
[Inject] public ILocalStorageService Storage { get; set; }
private List<ReportItemView> Quotes { get; set; } = new();
private List<ReportItemView> _quotes { get; set; } = new();
private bool Working { get; set; } = true;
private QStatus QFilter { get; set; } = QStatus.None;
protected override async Task OnInitializedAsync()
{
Interceptor.RegisterEvent();
Interceptor.RegisterBeforeSendEvent();
Quotes = await ActivityRepo.GetQuotes();
await Storage.SetItemAsync("quotes", Quotes);
if (Quotes.Any())
_quotes = Quotes.Where(x => x.QuoteStatusEnum is "Note" or "None").ToList();
Working = false;
}
private async Task FilterQuotes(QStatus status)
{
QFilter = status;
Quotes = await Storage.GetItemAsync<List<ReportItemView>>("quotes");
_quotes = QFilter switch
{
QStatus.None => Quotes.Where(x => x.QuoteStatusEnum is "None").ToList(),
QStatus.Win => Quotes.Where(x => x.QuoteStatusEnum is "Win").ToList(),
QStatus.Lose => Quotes.Where(x => x.QuoteStatusEnum is "Lose").ToList(),
QStatus.Archive => Quotes.Where(x => x.QuoteStatusEnum is "Archive").ToList(),
QStatus.Note => Quotes.Where(x => x.QuoteStatusEnum is "Note").ToList(),
_ => Quotes.ToList()
};
}
private async Task UpdateQuoteCallback(QuoteCallbackArgs args)
{
var quote = Quotes.First(x => x.ESalesNumber == args.ESalesNumber);
if (args.Status == QStatus.Win)
quote.OrderDate = $"{DateTime.Now:yyyy-MM-dd}";
quote.QuoteStatusEnum = Utils.EnumToString(args.Status);
var response = await ActivityRepo.UpdateQuoteStatus(quote);
Toaster.ShowInfo($"{response.Message}", $"HTTP STATUS {response.Code}");
Quotes = new List<ReportItemView>();
await Storage.RemoveItemAsync("quotes");
Quotes = await ActivityRepo.GetQuotes();
while (!Quotes.Any())
await Task.Delay(1000);
await Storage.SetItemAsync("quotes", Quotes);
await FilterQuotes(QFilter);
StateHasChanged();
}
public void Dispose()
{
Storage.RemoveItemAsync("quotes");
Interceptor.DisposeEvent();
}
}

View file

@ -1,7 +1,7 @@
{
"appInfo": {
"name": "Wonky Client",
"version": "0.85.5",
"version": "0.85.7",
"rc": true,
"sandBox": false,
"image": "grumpy-coder.png"
@ -33,7 +33,7 @@
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Default": "Information",
"System": "Information",
"Microsoft": "Information"
},