FEAT: KANVAS customer

This commit is contained in:
Frede Hundewadt 2023-04-06 13:22:02 +02:00
parent 0a96c9c640
commit 7572127425
7 changed files with 477 additions and 350 deletions

View file

@ -37,7 +37,7 @@
}
<div class="row mb-2 bg-dark text-white rounded-3 p-3">
<div class="col">
<h3>@Activity.Name - @Activity.Account</h3>
<span class="h3">@Activity.Name</span> <span>(@Activity.Account)</span>
</div>
</div>
@ -58,9 +58,16 @@ else
<label for="activityType" class="col-sm-2 col-form-label-sm">Ordre Type</label>
<div class="col-sm-4">
<InputSelect id="activityType" class="form-select bg-primary text-bg-primary" @bind-Value="@Activity.ActivityTypeEnum">
<option value="">&rarr; TAG MIG &larr;</option>
<option value="onSite">Besøg</option>
<option value="phone">Telefon</option>
@if (Kanvas)
{
<option value="canvas" selected>Kanvas</option>
}
else
{
<option value="">&rarr; TAG MIG &larr;</option>
<option value="onSite">Besøg</option>
<option value="phone">Telefon</option>
}
</InputSelect>
<ValidationMessage For="@(() => Activity.ActivityTypeEnum)"></ValidationMessage>
</div>
@ -68,25 +75,32 @@ else
<label for="statusType" class="col-sm-2 col-form-label-sm">Status</label>
<div class="col-sm-4">
<InputSelect id="statusType" class="form-select bg-primary text-bg-primary" @bind-Value="@Activity.ActivityStatusEnum">
<option value="noSale">Ingen salg</option>
@if (!string.IsNullOrEmpty(Activity.VatNumber) && !string.IsNullOrWhiteSpace(Activity.Address1) && Company.HasFolded == 0)
@if (Kanvas)
{
@if (DraftProvider.Draft.DraftType == "order")
<option value="canvas" selected>Kanvas</option>
}
else
{
<option value="noSale">Ingen salg</option>
@if (!string.IsNullOrEmpty(Activity.VatNumber) && !string.IsNullOrWhiteSpace(Activity.Address1) && Company.HasFolded == 0)
{
<option selected value="order">Bestilling</option>
}
else
{
<option value="order">Bestilling</option>
}
@if (DraftProvider.Draft.DraftType == "order")
{
<option selected value="order">Bestilling</option>
}
else
{
<option value="order">Bestilling</option>
}
@if (DraftProvider.Draft.DraftType == "offer")
{
<option selected value="quote">Tilbud</option>
}
else
{
<option value="quote">Tilbud</option>
@if (DraftProvider.Draft.DraftType == "offer")
{
<option selected value="quote">Tilbud</option>
}
else
{
<option value="quote">Tilbud</option>
}
}
}
</InputSelect>
@ -156,6 +170,8 @@ else
</div>
</div>
@if (!Kanvas)
{
<div class="row g-2 mb-3">
<div class="col-sm-3 d-grid mx-auto">
@*
@ -348,7 +364,9 @@ else
</div>
</div>
</div>
}
</EditForm>
<div class="row mt-5 mb-2">
<div class="col-sm-6">
<a class="btn btn-warning" href="/advisor/customers/@Company.CompanyId">Kundekort <i class="bi-arrow-left"></i></a>

View file

@ -96,6 +96,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
private List<ProductInventoryView> CheckList { get; set; } = new();
private InvoiceListView CompanyInvoices { get; set; } = new();
private List<ReportItemView> Activities { get; set; } = new();
private bool Kanvas { get; set; }
/// <summary>
/// Page initialization
@ -114,23 +115,35 @@ public partial class AdvisorActivityCreatePage : IDisposable
SalesRep = await UserInfo.GetUserInfo();
// Fetch Customer from http
Company = await CompanyRepo.GetCompanyById(CompanyId);
if (Company.HasFolded == 1)
// Company has shut down
Activity.OrderMessage = "BEMÆRK: CVR nummer er ophørt.";
// variable to validate if customer needs phone number update
OldPhone = Company.Phone;
if (string.IsNullOrWhiteSpace(Company.Phone)
&& !string.IsNullOrWhiteSpace(Company.Account)
&& Company.Account != "NY" && Company.Account.Length > 7)
if (Company.Account.StartsWith("KANVAS"))
{
Company.Phone = Company.Account[..8];
Kanvas = true;
Activity.ActivityStatusEnum = "canvas";
Activity.ActivityTypeEnum = "canvas";
Activity.ActivityVisitEnum = "new";
PoFormInvalid = false;
}
else
{
if (Company.HasFolded == 1)
// Company has shut down
Activity.OrderMessage = "BEMÆRK: CVR nummer er ophørt.";
// variable to validate if customer needs phone number update
OldPhone = Company.Phone;
if (string.IsNullOrWhiteSpace(Company.Phone)
&& !string.IsNullOrWhiteSpace(Company.Account)
&& Company.Account != "NY" && Company.Account.Length > 7)
{
Company.Phone = Company.Account[..8];
}
Activity.ActivityStatusEnum = "noSale";
Activity.ActivityVisitEnum = Company.Account is "" or "NY" ? "new" : "recall";
}
// Populate base activity information
Activity.BcId = Company.BcId;
Activity.ActivityStatusEnum = "noSale";
Activity.VisitTypeEnum = Company.Account is "" or "NY" ? "new" : "recall";
Activity.CompanyId = Company.CompanyId;
Activity.SalesRepId = SalesRep.UserId;
Activity.SalesRep = SalesRep.SalesRep;
@ -164,12 +177,14 @@ public partial class AdvisorActivityCreatePage : IDisposable
PromptDateConfirm = $"Aktiviteter oprettes med dato {SelectedDate.ToShortDateString()}. Er dette OK?";
ConfirmWorkDate.Show();
}
// Lines may already have been added from the company inventory page
if (DraftProvider.Draft.DraftType == "order")
{
// set dropdown selection accordingly
Activity.ActivityTypeEnum = "onSite";
if(Activity.ActivityTypeEnum != "phone")
Activity.ActivityTypeEnum = "onSite";
Activity.ActivityStatusEnum = "order";
PoFormInvalid = false;
}
@ -177,91 +192,50 @@ public partial class AdvisorActivityCreatePage : IDisposable
Working = false;
}
// #############################################################
// overlays
private async Task ShowVisitOverlay()
{
Logger.LogDebug("ShowInventoryOverlay - wait for visits");
ActivityListOverlay.Show();
Activities = await ActivityRepo.GetCustomerActivities(CompanyId);
await Task.Delay(500);
}
private async Task ShowInventoryOverlay()
{
Logger.LogDebug("ShowInventoryOverlay - wait for inventory");
InventoryListOverlay.Show();
Inventory = await HistoryRepo.FetchInventory(CompanyId);
Inventory = Inventory.OrderBy(x => x.Description).ToList();
await Task.Delay(500);
}
private async Task OnInventoryCallback(DraftItem item)
{
Activity.ActivityStatusEnum = "order";
DraftProvider.Draft.DraftType = "order";
DraftProvider.Draft.Items.Add(item);
StateHasChanged();
}
private async Task ShowInvoiceOverlay()
{
Logger.LogDebug("ShowInvoiceOverlay - wait for invoices");
InvoiceListOverlay.Show();
CompanyInvoices = await FetchCompanyInvoices();
await Task.Delay(500);
}
private async Task<InvoiceListView> FetchCompanyInvoices()
private void ShowPriceListOverlay()
{
// fetch from storage
var storage = await Storage.GetItemAsStringAsync($"{CompanyId}-invoices");
await Task.Delay(500);
var iDate = await Storage.GetItemAsStringAsync($"{CompanyId}-iDate");
// if we have a list and iDate was today return the list
if (!string.IsNullOrWhiteSpace(storage) && (!string.IsNullOrWhiteSpace(iDate) &&
DateTime.Parse(iDate.Replace("\"", "")) >= DateTime.Now))
{
Logger.LogDebug("fetching invoices from storage");
Logger.LogDebug("storage contains <= {}", storage);
return JsonSerializer.Deserialize<InvoiceListView>(storage);
}
Logger.LogDebug("pulling invoices from backend");
// pull invoices
var companyInvoices = await HistoryRepo.FetchInvoiceList(CompanyId);
// send invoices to storage
await Storage.SetItemAsync($"{CompanyId}-invoices", companyInvoices);
await Storage.SetItemAsync($"{CompanyId}-iDate", $"{DateTime.Now:yyyy-MM-dd}");
Logger.LogDebug(" --> return invoices from backend");
Working = false;
Logger.LogDebug("backend contains <= {}", JsonSerializer.Serialize(companyInvoices));
return companyInvoices;
}
private void ShowOrgWarning()
{
if (OrgWarning)
return;
OrgWarning = true;
if (Company.CountryCode.ToLower() == "se" && Utils.StringToDigits(Activity.VatNumber).Length < 10 &&
Activity.ActivityStatusEnum == "order")
{
Toaster.ShowWarning("Org nummer er ufuldstændig. Skal opdateres før bestilling kan sendes. ", "ADVARSEL");
}
CatalogOverlay.Show();
}
private async Task CallConfirmCheckOverlay()
{
// check if new account
if (string.IsNullOrWhiteSpace(Company.Account)
|| Company.Account.ToLower() == "ny"
|| Activity.ActivityStatusEnum.ToLower() == "quote")
|| Activity.ActivityStatusEnum.ToLower() == "quote"
|| Activity.ActivityStatusEnum.ToLower() == "canvas")
{
// proceed to create activity - as there is no product check to be done
await CreateActivity();
@ -316,38 +290,16 @@ public partial class AdvisorActivityCreatePage : IDisposable
ConfirmationCheckOverlay.Show();
}
private async Task ConfirmProductCheckCallback()
{
ConfirmationCheckOverlay.Hide();
await CreateActivity();
foreach (var item in CheckList)
{
item.Check = false;
}
await Storage.SetItemAsync($"{CompanyId}-products", CheckList);
}
private async Task WorkDateConfirmCallback()
{
await PreferenceService.SetDateConfirmed(true);
Activity.ActivityDate = $"{SelectedDate:yyyy-MM-dd}";
ConfirmWorkDate.Hide();
StateHasChanged();
}
private async Task WorkDateComponentCallback(string workDate)
{
ReportClosed = await ReportRepo.ReportExist(workDate);
SelectedDate = DateTime.Parse(workDate);
Activity.ActivityDate = workDate;
}
private void ShowPriceListOverlay()
{
CatalogOverlay.Show();
private void ShowPriceHistoryOverlay()
{
if (ShowItem)
PriceOverlay.Show();
}
// #############################################################
// callbacks
private async Task PriceListCallback(SelectedSku sku)
{
// get selected item
@ -360,12 +312,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
StateHasChanged();
}
private void ShowPriceHistoryOverlay()
{
if (ShowItem)
PriceOverlay.Show();
}
private void PriceHistoryCallback(decimal price)
{
if (price == 0)
@ -374,26 +321,99 @@ public partial class AdvisorActivityCreatePage : IDisposable
StateHasChanged();
}
private void OnInventoryCallback(DraftItem item)
{
Activity.ActivityStatusEnum = "order";
DraftProvider.Draft.DraftType = "order";
DraftProvider.Draft.Items.Add(item);
StateHasChanged();
}
private async Task ConfirmProductCheckCallback()
{
ConfirmationCheckOverlay.Hide();
await CreateActivity();
foreach (var item in CheckList)
{
item.Check = false;
}
await Storage.SetItemAsync($"{CompanyId}-products", CheckList);
}
private async Task WorkDateConfirmCallback()
{
await PreferenceService.SetDateConfirmed(true);
Activity.ActivityDate = $"{SelectedDate:yyyy-MM-dd}";
ConfirmWorkDate.Hide();
StateHasChanged();
}
private async Task WorkDateComponentCallback(string workDate)
{
ReportClosed = await ReportRepo.ReportExist(workDate);
SelectedDate = DateTime.Parse(workDate);
Activity.ActivityDate = workDate;
}
// ################################################################################################
// fetch invoices for customer
private async Task<InvoiceListView> FetchCompanyInvoices()
{
// no need to do for kanvas entry
if (Kanvas) return new InvoiceListView();
// fetch from storage
var storage = await Storage.GetItemAsStringAsync($"{CompanyId}-invoices");
await Task.Delay(500);
var iDate = await Storage.GetItemAsStringAsync($"{CompanyId}-iDate");
// if we have a list and iDate was today return the list
if (!string.IsNullOrWhiteSpace(storage) && (!string.IsNullOrWhiteSpace(iDate) &&
DateTime.Parse(iDate.Replace("\"", "")) >= DateTime.Now))
{
Logger.LogDebug("fetching invoices from storage");
Logger.LogDebug("storage contains <= {}", storage);
return JsonSerializer.Deserialize<InvoiceListView>(storage);
}
Logger.LogDebug("pulling invoices from backend");
// pull invoices
var companyInvoices = await HistoryRepo.FetchInvoiceList(CompanyId);
// send invoices to storage
await Storage.SetItemAsync($"{CompanyId}-invoices", companyInvoices);
await Storage.SetItemAsync($"{CompanyId}-iDate", $"{DateTime.Now:yyyy-MM-dd}");
Logger.LogDebug(" --> return invoices from backend");
Working = false;
Logger.LogDebug("backend contains <= {}", JsonSerializer.Serialize(companyInvoices));
return companyInvoices;
}
// ##################################################################################################
// create activity
private async Task CreateActivity()
{
// avoid duplication
if (Working)
return;
// validate customer address1
// - this is a required input
if (string.IsNullOrWhiteSpace(Activity.Address1))
Logger.LogDebug("view kanvas activity => {}", JsonSerializer.Serialize(Activity));
switch (Kanvas)
{
Toaster.ShowError("Kunde adresse er ufuldstændig.");
return;
}
// validate org number
// - this is a required input
// - must validate according to country rules.
if (!VatUtils.ValidateFormat(Company.CountryCode, Activity.VatNumber))
{
Toaster.ShowError("Firma registreringsnummer er ikke korrekt.");
return;
// validate customer address1
// - this is a required input
case false when string.IsNullOrWhiteSpace(Activity.Address1):
Toaster.ShowError("Kunde adresse er ufuldstændig.");
return;
// validate org number
// - this is a required input
// - must validate according to country rules.
case false when !VatUtils.ValidateFormat(Company.CountryCode, Activity.VatNumber):
Toaster.ShowError("Firma registreringsnummer er ikke korrekt.");
return;
}
// validate input according to status
@ -419,7 +439,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
// reset selected item
SelectedItem = new SalesItemView();
// check if phone number need to be updated
if (OldPhone != Activity.Phone)
if (!Kanvas && OldPhone != Activity.Phone)
{
Company.Phone = Activity.Phone;
Activity.OrderMessage = $"BEMÆRK: {Activity.Phone}\n{Activity.OrderMessage}";
@ -482,12 +502,29 @@ public partial class AdvisorActivityCreatePage : IDisposable
Toaster.ShowError(result.Message, "ORDRE FEJL");
}
private void ShowOrgWarning()
{
if (Kanvas) return;
if (OrgWarning) return;
OrgWarning = true;
if (Company.CountryCode.ToLower() == "se" && Utils.StringToDigits(Activity.VatNumber).Length < 10 &&
Activity.ActivityStatusEnum == "order")
{
Toaster.ShowWarning("Org nummer er ufuldstændig. Skal opdateres før bestilling kan sendes. ", "ADVARSEL");
}
}
// ###############################################################################
// draft functions
private async Task DeleteDraft()
{
await DraftProvider.DeleteDraftAsync();
Activity.ActivityStatusEnum = "noSale";
}
private async Task AddItem(SalesItemView salesItem)
{
ShowItem = false;
@ -513,6 +550,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
await DraftProvider.SaveChangesAsync();
}
private async Task RemoveItem(DraftItem item)
{
// remove item
@ -523,6 +561,9 @@ public partial class AdvisorActivityCreatePage : IDisposable
Activity.ActivityStatusEnum = "noSale";
}
// ##################################################################################################
// form validation
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
Logger.LogDebug("ActivityNewPage => HandleFieldChanged => ActivityStatusEnum <= '{}'",
@ -556,6 +597,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
StateHasChanged();
}
private void ValidationChanged(object sender, ValidationStateChangedEventArgs e)
{
if (string.IsNullOrEmpty(Activity.ActivityTypeEnum) && !ReportClosed)
@ -582,6 +624,9 @@ public partial class AdvisorActivityCreatePage : IDisposable
ActivityContext.OnValidationStateChanged += ValidationChanged;
}
// #########################################################################################################
// dispose
public void Dispose()
{
Interceptor.DisposeEvent();

View file

@ -29,7 +29,9 @@
</div>
}
<div class="row pt-2 mb-2 rounded rounded-2 bg-dark text-white">
<h3>@Company.Name @(string.IsNullOrWhiteSpace(Company.Account) ? "" : "- ")@Company.Account</h3>
<div class="col-sm-12">
<span class="h3">@Company.Name</span> <span>(@Company.Account)</span>
</div>
</div>
// erp context
<EditForm EditContext="ErpContext">
@ -89,65 +91,81 @@
<InputText id="email" class="form-control" @bind-Value="Company.Email" readonly="@(ErpEditDisabled)"/>
<ValidationMessage For="@(() => Company.Email)"></ValidationMessage>
</div>
<div class="col-sm-4">@* ---- placeholder --- *@</div>
@* Force enable visit *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-primary d-block" disabled="@(Company.HasFolded == 0 || Company.Name == "ERROR")" @onclick="ForceActivity">Aktiver besøg</button>
</div>
@if (!Kanvas)
{
<div class="col-sm-4">@* ---- placeholder --- *@</div>
@* Enable edit/save *@
<div class="col-sm-2 d-grid mx-auto">
<button type="button" class="btn btn-edit" @onclick="ToggleErpEdit"><i class="bi-pencil"></i> STAM data</button>
</div>
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-danger d-block" onclick="@UpdateErpData" disabled="@(Working || Company.Name == "ERROR" || ErpEditDisabled)"><i class="bi-cloud-arrow-up"></i> STAM data </button>
</div>
@* vat number*@
<label for="vatNumber" class="col-sm-1 col-form-label-sm">CVR/Org nr.</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-text">
<DisplayStateComponent StateClass="@VatState"/>
</span>
<InputText id="vatNumber" class="form-control" @bind-Value="Company.VatNumber" readonly="@(VatEditDisabled)"/>
<ValidationMessage For="@(() => Company.VatNumber)"></ValidationMessage>
<div class="col-sm-2 d-grid mx-auto">
<button type="button" class="btn btn-edit" @onclick="ToggleErpEdit"><i class="bi-pencil"></i> STAM data</button>
</div>
@* Force enable visit *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-primary d-block" @onclick="ForceActivity">Aktiver besøg</button>
</div>
@* Save erp data *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-danger d-block" onclick="@UpdateErpData" disabled="@(Working || Company.Name == "ERROR" || ErpEditDisabled)"><i class="bi-cloud-arrow-up"></i> STAM data </button>
</div>
@* vat number*@
<label for="vatNumber" class="col-sm-1 col-form-label-sm">CVR/Org nr.</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-text">
<DisplayStateComponent StateClass="@VatState"/>
</span>
<InputText id="vatNumber" class="form-control" @bind-Value="Company.VatNumber" readonly="@(VatEditDisabled)"/>
<ValidationMessage For="@(() => Company.VatNumber)"></ValidationMessage>
</div>
</div>
@* Enable edit/save vatnumber *@
<div class="col-sm-2 d-grid mx-auto">
<button type="button" class="btn btn-edit" @onclick="ToggleVatEdit"><i class="bi-pencil"></i> Moms/Org Nr.</button>
</div>
</div>
@* vat lookup *@
<div class="col-sm-3 d-grid mx-auto">
@switch (CountryCode)
{
case "dk":
<button type="button" class="btn btn-info" @onclick="OpenVatLookupModal"><i class="bi-search"></i> CVR</button>
break;
case "no":
<a class="btn btn-info" href="https://brreg.no/" target="_blank"><i class="bi-search"></i> brreg.no</a>
break;
case "se":
<a class="btn btn-info" href="https://www.allabolag.se/what/@Company.Name" target="_blank"><i class="bi-search"></i> allabolag.se</a>
break;
}
</div>
@* Enable edit/save *@
<div class="col-sm-2 d-grid mx-auto">
<button type="button" class="btn btn-edit" @onclick="ToggleVatEdit"><i class="bi-pencil"></i> Moms/Org Nr.</button>
</div>
<div class="col-sm-3 d-grid mx-auto">
@switch (CountryCode)
{
case "dk":
<button type="button" class="btn btn-info" @onclick="OpenVatLookupModal" disabled="@(VatEditDisabled)"><i class="bi-search"></i> CVR</button>
break;
case "no":
<a class="btn btn-info" href="https://brreg.no/" target="_blank"><i class="bi-search"></i> brreg.no</a>
break;
case "se":
<a class="btn btn-info" href="https://www.allabolag.se/what/@Company.Name" target="_blank"><i class="bi-search"></i> allabolag.se</a>
break;
}
</div>
@* save vat number *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-warning d-block" @onclick="UpdateVatNumber" disabled="@(VatEditDisabled)"><i class="bi-cloud-arrow-up"></i> Moms/Org Nr.</button>
</div>
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-warning d-block" @onclick="UpdateVatNumber" disabled="@(VatEditDisabled)"><i class="bi-cloud-arrow-up"></i> Moms/Org Nr.</button>
</div>
}
</div>
<hr class="mb-3"/>
@* activity buttons *@
<div class="row mt-3 mb-3">
<div class="col-sm-3">
<a class="btn btn-danger d-block" href="/advisor/customers/@Company.CompanyId/invoices">Faktura</a>
@if (!Kanvas)
{
<a class="btn btn-danger d-block" href="/advisor/customers/@Company.CompanyId/invoices">Faktura</a>
}
</div>
<div class="col-sm-3">
<a class="btn btn-warning d-block" href="/advisor/customers/@Company.CompanyId/activities">Tidl. Besøg</a>
@if (!Kanvas)
{
<a class="btn btn-warning d-block" href="/advisor/customers/@Company.CompanyId/activities">Tidl. Besøg</a>
}
</div>
<div class="col-sm-3">
<a class="btn btn-success d-block" href="/advisor/customers/@Company.CompanyId/h/i">Produkter</a>
@if (!Kanvas)
{
<a class="btn btn-success d-block" href="/advisor/customers/@Company.CompanyId/h/i">Produkter</a>
}
</div>
<div class="col-sm-3">
<ActivityButton ActionLink="@ActionLink"
@ -158,104 +176,105 @@
</div>
</div>
<hr class="mb-3"/>
@if (!Kanvas)
{
<hr class="mb-3"/>
@* crm context - OBS note *@
<div class="row mb-2">
<label for="note" class="col-sm-1 col-form-label-sm">OBS</label>
<div class="col-sm-8">
@if (string.IsNullOrWhiteSpace(Company.Note))
{
<InputText name="note" id="note" class="form-control" @bind-Value="Company.Note"/>
}
else
{
<InputText name="note" id="note" class="form-control bg-warning text-black" @bind-Value="Company.Note"/>
}
<ValidationMessage For="@(() => Company.Note)"></ValidationMessage>
</div>
@* Save CRM data button *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-warning" disabled="@(Company.Name == "ERROR")" @onclick="UpdateCrmData"><i class="bi-cloud-arrow-up"></i> CRM data</button>
</div>
</div>
@* crm context - contacts *@
<div class="row mb-4">
<label for="contacts" class="col-sm-1 col-form-label-sm">Kontakt</label>
<div id="contacts" class="col-sm-11">
<div class="list-group">
<div class="list-group-item list-group-item-action bg-dark text-white" @onclick="() => OpenContactPopup(DefaultContact)">
<div class="row">
<div class="col-sm-4">Stilling</div>
<div class="col-sm-4">Navn</div>
<div class="col-sm-3">Direkte</div>
<div class="col-sm-1 text-end">
<i class="bi-plus-circle"></i>
</div>
</div>
</div>
@if (Contacts.Any())
<div class="row mb-2">
<label for="note" class="col-sm-1 col-form-label-sm">OBS</label>
<div class="col-sm-8">
@if (string.IsNullOrWhiteSpace(Company.Note))
{
@foreach (var contact in Contacts)
{
<div class="list-group-item list-group-item-action" @onclick="() => OpenContactPopup(contact)">
<div class="row g-2">
<div class="col-sm-4">@contact.JobTitle</div>
<div class="col-sm-4">@contact.FirstName @contact.LastName</div>
<div class="col-sm-3">
@contact.PhoneDirect
</div>
<div class="col-sm-1 text-end">
<i class="bi-pencil"></i>
</div>
<InputText name="note" id="note" class="form-control" @bind-Value="Company.Note"/>
}
else
{
<InputText name="note" id="note" class="form-control bg-warning text-black" @bind-Value="Company.Note"/>
}
<ValidationMessage For="@(() => Company.Note)"></ValidationMessage>
</div>
@* Save CRM data button *@
<div class="col-sm-3 d-grid mx-auto">
<button type="button" class="btn btn-warning" disabled="@(Company.Name == "ERROR")" @onclick="UpdateCrmData"><i class="bi-cloud-arrow-up"></i> CRM data</button>
</div>
</div>
@* crm context - contacts *@
<div class="row mb-4">
<label for="contacts" class="col-sm-1 col-form-label-sm">Kontakt</label>
<div id="contacts" class="col-sm-11">
<div class="list-group">
<div class="list-group-item list-group-item-action bg-dark text-white" @onclick="() => OpenContactPopup(DefaultContact)">
<div class="row">
<div class="col-sm-4">Stilling</div>
<div class="col-sm-4">Navn</div>
<div class="col-sm-3">Direkte</div>
<div class="col-sm-1 text-end">
<i class="bi-plus-circle"></i>
</div>
</div>
</div>
@if (Contacts.Any())
{
@foreach (var contact in Contacts)
{
<div class="list-group-item list-group-item-action" @onclick="() => OpenContactPopup(contact)">
<div class="row g-2">
<div class="col-sm-4">@contact.JobTitle</div>
<div class="col-sm-4">@contact.FirstName @contact.LastName</div>
<div class="col-sm-3">
@contact.PhoneDirect
</div>
<div class="col-sm-1 text-end">
<i class="bi-pencil"></i>
</div>
</div>
</div>
}
}
}
</div>
</div>
</div>
</div>
@* crm context - dates and interval *@
<div class="row mb-2">
<label for="nextVisit" class="col-sm-1 col-form-label-sm">Næste besøg</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-text">
<DisplayStateComponent StateClass="@VisitState"/>
</span>
<InputDate id="nextVisit" class="form-control" @bind-Value="@(NextVisit)"/>
<div class="row mb-2">
<label for="nextVisit" class="col-sm-1 col-form-label-sm">Næste besøg</label>
<div class="col-sm-3">
<div class="input-group">
<span class="input-group-text">
<DisplayStateComponent StateClass="@VisitState"/>
</span>
<InputDate id="nextVisit" class="form-control" @bind-Value="@(NextVisit)"/>
</div>
</div>
<label for="lastVisit" class="col-sm-1 col-form-label-sm">Sidse besøg</label>
<div class="col-sm-3">
<InputDate id="lastVisit" class="form-control" @bind-Value="@LastVisit"/>
</div>
<label for="interval" class="col-sm-2 col-form-label-sm">Uge Interval</label>
<div class="col-sm-2">
<InputNumber id="interval" class="form-control" @bind-Value="Company.Interval"/>
<ValidationMessage For="@(() => Company.Interval)"></ValidationMessage>
</div>
</div>
<label for="lastVisit" class="col-sm-1 col-form-label-sm">Sidse besøg</label>
<div class="col-sm-3">
<InputDate id="lastVisit" class="form-control" @bind-Value="@LastVisit"/>
<div class="row mb-2">
<label for="crmNotes" class="col-sm-1 col-form-label-sm">Noter</label>
<div class="col-sm-11">
<InputTextArea id="crmNotes" class="form-control" @bind-Value="Company.CrmNotes"/>
</div>
</div>
<label for="interval" class="col-sm-2 col-form-label-sm">Uge Interval</label>
<div class="col-sm-2">
<InputNumber id="interval" class="form-control" @bind-Value="Company.Interval"/>
<ValidationMessage For="@(() => Company.Interval)"></ValidationMessage>
<div class="row pt-3">
<div class="col-sm-3 d-grid">
<button type="button" class="btn btn-danger" @onclick="ToggleVisibility">@ToggleButtonText</button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="crmNotes" class="col-sm-1 col-form-label-sm">Noter</label>
<div class="col-sm-11">
<InputTextArea id="crmNotes" class="form-control" @bind-Value="Company.CrmNotes"/>
@*
<div class="row mt-5">
<div class="col-sm-3">
<a class="btn btn-info" href="@($"/advisor/customers/{CompanyId}/workplaces")">Arbejdssteder</a>
</div>
</div>
</div>
<div class="row pt-3">
<div class="col-sm-3 d-grid">
<button type="button" class="btn btn-danger" @onclick="ToggleVisibility">@ToggleButtonText</button>
</div>
</div>
*@
}
</EditForm>
<div class="row mt-5">
<div class="col-sm-3">
<a class="btn btn-info" href="@($"/advisor/customers/{CompanyId}/workplaces")">Arbejdssteder</a>
</div>
</div>
}
@if (Working)

View file

@ -71,6 +71,7 @@ public partial class AdvisorCustomerViewEditPage : IDisposable
private ContactModal ContactPopup { get; set; } = new();
private UserManagerEditView UserInfo { get; set; } = new();
private string ToggleButtonText { get; set; } = "";
private bool Kanvas { get; set; }
protected override async Task OnInitializedAsync()
{
@ -96,60 +97,67 @@ public partial class AdvisorCustomerViewEditPage : IDisposable
Company = await CustomerRepo.GetCompanyById(CompanyId);
Logger.LogDebug("company => {}", JsonSerializer.Serialize(Company));
// toggle view button text
ToggleButtonText = Company.IsHidden == 0 ? "Udelad kunde i oversigt" : "Brug Normal Visning";
CurrentVat = Company.VatNumber;
Company.CountryCode = UserInfo.CountryCode.ToLower();
if (Company.Account.StartsWith("KANVAS"))
Kanvas = true;
// internal flag
EnableActivity = Company.ValidVat;
// override if canvas which has account property as empty string or "NY"
if (Company.Account == "NY" || string.IsNullOrWhiteSpace(Company.Account))
if (Company.Account == "NY" || Company.Account.StartsWith("KANVAS") || string.IsNullOrWhiteSpace(Company.Account))
EnableActivity = 1;
// visit interval init
if (Company.Interval == 0)
Company.Interval = 8;
// visit date init
LastVisit = DateTime.Parse(Company.LastVisit);
NextVisit = DateTime.Parse(Company.NextVisit);
// if no previous visit is registered - force last visit date to 2020
if (LastVisit.Year < 2020)
LastVisit = DateTime.Parse("2020-01-01");
// set next visit according to last visit and interval
if (!Company.ValidDateSpan())
NextVisit = LastVisit.AddDays(Company.Interval * 7);
// display urgency of next visit
VisitState = Utils.GetVisitState($"{NextVisit:yyyy-MM-dd}");
// action link passed to activity button component
ActionLink = $"/advisor/customers/{CompanyId}/activities/new"; // used when drawing visit button
// handle company out of business case
if (Company.HasFolded == 1)
// only execute if the company a 'real' customer
if (!Kanvas)
{
// this is only used if user has selected to show closed companies
HasFolded = true;
VatState = "the-dead";
VisitState = "the-dead";
Logger.LogDebug("company => {}", JsonSerializer.Serialize(Company));
// toggle view button text
ToggleButtonText = Company.IsHidden == 0 ? "Udelad kunde i oversigt" : "Brug Normal Visning";
CurrentVat = Company.VatNumber;
Company.CountryCode = UserInfo.CountryCode.ToLower();
// visit interval init
if (Company.Interval == 0)
Company.Interval = 8;
// visit date init
LastVisit = DateTime.Parse(Company.LastVisit);
NextVisit = DateTime.Parse(Company.NextVisit);
// if no previous visit is registered - force last visit date to 2020
if (LastVisit.Year < 2020)
LastVisit = DateTime.Parse("2020-01-01");
// set next visit according to last visit and interval
if (!Company.ValidDateSpan())
NextVisit = LastVisit.AddDays(Company.Interval * 7);
// display urgency of next visit
VisitState = Utils.GetVisitState($"{NextVisit:yyyy-MM-dd}");
// handle company out of business case
if (Company.HasFolded == 1)
{
// this is only used if user has selected to show closed companies
HasFolded = true;
VatState = "the-dead";
VisitState = "the-dead";
}
else
{
// valid vat enum
Company.ValidVat = VatUtils.ValidateFormat(Company.CountryCode, Company.VatNumber) ? 1 : 0;
// valid vat flag
ValidVat = Company.ValidVat == 1; // true/false flag set if company has a valid vatNumber
// vat state css class
VatState = Company.ValidVat == 1 ? "the-good" : "no-vat"; // assign css class
}
// create search address from address
if (CountryIsDk)
CompanyVatAddress = PrepareVatAddress(Company);
await FetchContacts(CompanyId);
await Task.Delay(100);
await RequestErpUpdate();
}
else
{
// valid vat enum
Company.ValidVat = VatUtils.ValidateFormat(Company.CountryCode, Company.VatNumber) ? 1 : 0;
// valid vat flag
ValidVat = Company.ValidVat == 1; // true/false flag set if company has a valid vatNumber
// vat state css class
VatState = Company.ValidVat == 1 ? "the-good" : "no-vat"; // assign css class
}
// create search address from address
if (CountryIsDk)
CompanyVatAddress = PrepareVatAddress(Company);
await FetchContacts(CompanyId);
// remove loading image
Working = false;
await Task.Delay(100);
await RequestErpUpdate();
}
private void ToggleErpEdit()
@ -414,6 +422,7 @@ public partial class AdvisorCustomerViewEditPage : IDisposable
/// <param name="e"></param>
private void HandleFieldChanged(object? sender, FieldChangedEventArgs? e)
{
NextVisit = LastVisit.AddDays(Company.Interval * 7);
// avoid nesting if by assuming ValidVat is false
ValidVat = false;

View file

@ -153,7 +153,7 @@ public partial class OfficeOrderCreatePage : IDisposable
// setting up activity properties
Activity.ActivityStatusEnum = "noSale";
Activity.VisitTypeEnum = "recall";
Activity.ActivityVisitEnum = "recall";
Activity.ActivityTypeEnum = "phone";
Activity.ActivityStatusEnum = "order";
Activity.OurRef = $"T:{UserInfo.FirstName}";

View file

@ -1,15 +1,15 @@
{
"appInfo": {
"name": "Wonky Online",
"version": "0.131.0",
"version": "0.132.1",
"rc": true,
"sandBox": false,
"image": "grumpy-coder.png"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Debug",
"Default": "Information",
"System": "Information",
"Microsoft": "Information"
},
"Debug": {
@ -19,7 +19,7 @@
}
},
"apiConfig": {
"baseUrl": "https://dev.innotec.dk",
"baseUrl": "https://zeta.innotec.dk",
"catalog": "api/v2/catalog/country",
"crmCustomers": "api/v2/crm/companies",
"crmInventoryExt": "history/inventory",

View file

@ -24,164 +24,200 @@ public class ActivityDto
/// Activity entity id
/// </summary>
public string SalesHeadId { get; set; } = "";
/// <summary>
/// Sales representative identification
/// </summary>
[Required] public string SalesRep { get; set; } = "";
[Required]
public string SalesRep { get; set; } = "";
/// <summary>
/// Sales representative entity Id
/// </summary>
[Required] public string SalesRepId { get; set; } = "";
[Required]
public string SalesRepId { get; set; } = "";
/// <summary>
/// Company countryCode (ensure correct code when office create ordre)
/// </summary>
[Required] public string CountryCode { get; set; } = "";
[Required]
public string CountryCode { get; set; } = "";
/// <summary>
/// Company entity Id
/// </summary>
[Required] public string CompanyId { get; set; } = "";
[Required]
public string CompanyId { get; set; } = "";
/// <summary>
/// Business Central entity Id
/// </summary>
public string BcId { get; set; } = "";
/// <summary>
/// Customer account
/// </summary>
public string Account { get; set; } = "";
/// <summary>
/// VAT number
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string VatNumber { get; set; } = "";
/// <summary>
/// Customer name
/// </summary>
[Required(ErrorMessage = "Navn skal udfyldes")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
public string Name { get; set; } = "";
/// <summary>
/// Customer address city name
/// </summary>
[Required(ErrorMessage = "Byanvn skal udfyldes")]
[MaxLength(30, ErrorMessage = "Du kan højst bruge 30 tegn")]
public string City { get; set; }= "";
[MaxLength(30, ErrorMessage = "Du kan højst bruge 30 tegn")]
public string City { get; set; } = "";
/// <summary>
/// Customer address postal code
/// </summary>
[Required(ErrorMessage = "Postnr. skal udfyldes")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string ZipCode { get; set; } = "";
/// <summary>
/// Customer address line 1
/// </summary>
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
public string Address1 { get; set; } = "";
/// <summary>
/// Customer address line 2
/// </summary>
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
public string Address2 { get; set; } = "";
/// <summary>
/// Customer office phone
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string Phone { get; set; } = "";
/// <summary>
/// Customer mobile phone
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string Mobile { get; set; } = "";
/// <summary>
/// Customer office email
/// </summary>
[MaxLength(80, ErrorMessage = "Du kan højst bruge 80 tegn")]
[MaxLength(80, ErrorMessage = "Du kan højst bruge 80 tegn")]
public string Email { get; set; } = "";
/// <summary>
/// Customer attention description
/// </summary>
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
public string Attention { get; set; } = "";
// Form entries
/// <summary>
/// Activity type enum as string
/// </summary>
[Required(ErrorMessage = "Vælg aktivitetstype")]
public string ActivityTypeEnum { get; set; } = "";
/// <summary>
/// Flag express order
/// </summary>
public bool Express { get; set; }
// Form entries
/// <summary>
/// Activity status enum as string
/// </summary>
[Required(ErrorMessage = "Vælg status for besøg ")]
[Required(ErrorMessage = "Vælg status for besøg ")]
public string ActivityStatusEnum { get; set; } = "";
/// <summary>
/// Activity type enum as string
/// </summary>
[Required(ErrorMessage = "Vælg aktivitetstype")]
public string ActivityTypeEnum { get; set; } = "";
/// <summary>
/// Visit type enum as string
/// </summary>
public string VisitTypeEnum { get; set; } = "recall";
public string ActivityVisitEnum { get; set; } = "recall";
/// <summary>
/// Activity date
/// </summary>
[Required(ErrorMessage = "Dato skal angives")]
[Required(ErrorMessage = "Dato skal angives")]
public string ActivityDate { get; set; } = "";
/// <summary>
/// Product demonstration
/// </summary>
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
public string Demo { get; set; } = "";
/// <summary>
/// Our reference - system generated
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string OurRef { get; set; } = "";
/// <summary>
/// Customer reference number
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string ReferenceNumber { get; set; } = "";
/// <summary>
/// Customer reference description
/// </summary>
[MaxLength(35, ErrorMessage = "Du kan højst bruge 35 tegn")]
[MaxLength(35, ErrorMessage = "Du kan højst bruge 35 tegn")]
public string YourRef { get; set; } = "";
/// <summary>
/// Processing note to office
/// </summary>
[MaxLength(255, ErrorMessage = "Du kan højst bruge 255 tegn")]
[MaxLength(255, ErrorMessage = "Du kan højst bruge 255 tegn")]
public string OrderMessage { get; set; } = "";
/// <summary>
/// CRM note for future reference
/// </summary>
[MaxLength(255, ErrorMessage = "Du kan højst bruge 255 tegn")]
[MaxLength(255, ErrorMessage = "Du kan højst bruge 255 tegn")]
public string CrmNote { get; set; } = "";
// Delivery address form entries
/// <summary>
/// Customer delivery name
/// </summary>
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
public string DlvName { get; set; } = "";
/// <summary>
/// Customer delivery address line 1
/// </summary>
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
[MaxLength(100, ErrorMessage = "Du kan højst bruge 100 tegn")]
public string DlvAddress1 { get; set; } = "";
/// <summary>
/// Customer delivery address line 2
/// </summary>
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
[MaxLength(50, ErrorMessage = "Du kan højst bruge 50 tegn")]
public string DlvAddress2 { get; set; } = "";
/// <summary>
/// Customer delivery postal code
/// </summary>
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
[MaxLength(20, ErrorMessage = "Du kan højst bruge 20 tegn")]
public string DlvZipCode { get; set; } = "";
/// <summary>
/// Customer delivery city name
/// </summary>
[MaxLength(30, ErrorMessage = "Du kan højst bruge 30 tegn")]
[MaxLength(30, ErrorMessage = "Du kan højst bruge 30 tegn")]
public string DlvCity { get; set; } = "";
// Lines
/// <summary>
/// Order lines