Dynamic Portal menu

This commit is contained in:
Andy Howard
2025-01-13 14:52:09 +00:00
parent 4a72043f00
commit ee988b1f76
21 changed files with 218 additions and 229 deletions
@@ -55,23 +55,6 @@ namespace Sarand.BusinessMgr.Web.Portal.App
public SysId ControlOwnerId { get; protected set; }
public SysId ControlRepositoryId { get; protected set; }
}
private class SessionContextData_Core : SessionContextData
{
// Instance configuration for core database instance without Control configuration.
public SessionContextData_Core(SbmSessionContext_Web sessionContext) : base(sessionContext)
{
}
private SbmInstanceConfig instanceConfig = null;
public override SbmInstanceConfig InstanceConfig
{
get
{
if (instanceConfig == null)
instanceConfig = new SbmInstanceConfig_Core(SessionContext);
return instanceConfig;
}
}
}
private class SessionContextData_Control : SessionContextData
{
public SessionContextData_Control(SbmSessionContext_Web sessionContext, SysId controlOwnerId, SysId controlRepositoryId) : base(sessionContext)
@@ -233,26 +216,6 @@ namespace Sarand.BusinessMgr.Web.Portal.App
controlDatabase = SacsDatabaseConsole.OpenOdbcDatabase(odbcConfig);
if (controlDatabase.HasErrorState())
SaLogger.Warn($"{SysUtil.Fn} Control Database initialisation error: " + controlDatabase.ErrorStateMessage);
// Configure BusMgr database connections.
foreach (Net.Core.Web.App.SbmAppConfig.DatabaseConfig dbConfig in AppConfig.BusMgrDatabases)
{
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
config.Location = dbConfig.Location;
config.User = dbConfig.User;
config.Password = dbConfig.Password;
SacsDatabase coreDatabase = SacsDatabaseConsole.OpenOdbcDatabase(config);
if (!coreDatabase.HasErrorState())
{
// Session context.
ushort sessionContextIdx = sessionContextIdx_Next++;
SessionContextData sessionContextData = new SessionContextData_Core(new SbmSessionContext_Web(sessionContextIdx, Sbm.Application.WebPortal));
sessionContextData.BusMgrDatabase = coreDatabase;
sessionContextData.RepositoryContexts.UnionWith(dbConfig.Identifiers);
sessionContexts.Add(sessionContextData.SessionContext.Index, sessionContextData);
}
else SaLogger.Warn($"{SysUtil.Fn} Database: " + coreDatabase.DatabaseName + ". Initialisation error: " + coreDatabase.ErrorStateMessage);
}
}
catch (Exception exc)
{
@@ -10,7 +10,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Consoles
{
//-------------------------------------------------------------------
// Data members.
private SaUpdateLogConsole updateLogConsole;
//private SaUpdateLogConsole updateLogConsole;
//-------------------------------------------------------------------
// Construction.
@@ -33,6 +33,8 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Commodities
//-------------------------------------------------------------------
[HttpGet]
[Route("Commodity")]
[Route("Commodities")]
public IActionResult Index(Model_Commodity model)
{
if (!ValidUserSession)
@@ -234,7 +234,8 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Contacts
ColumnDfn colPhoneMobile = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.PhoneMobile);
ColumnDfn colPhoneDirect = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.PhoneDirect);
ColumnDfn colEmail = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.Email);
ColumnDfn colAssocCount = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.Association_Count);
ColumnDfn colAssocOwnerCount = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.AssociationOwner_Count);
ColumnDfn colAssocTargetCount = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.AssociationTarget_Count);
ColumnDfn colNotes = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.Notes);
ColumnDfn colStatus = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.Status);
ColumnDfn colCreatedOn = AccountMgr.GetAffiliateColumnDfn(SbmAffiliate.Column.CreateOn_Date);
@@ -242,7 +243,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Contacts
List<ColumnDfn> columns = new List<ColumnDfn>()
{
colId, colName, colAffiliateType,
colAddress, colContact, colPhoneMain, colPhoneAlt, colPhoneMobile, colPhoneDirect, colEmail, colAssocCount,
colAddress, colContact, colPhoneMain, colPhoneAlt, colPhoneMobile, colPhoneDirect, colEmail, colAssocOwnerCount, colAssocTargetCount,
colNotes, colStatus, colCreatedOn, colUpdatedOn
};
if (responseData.Config.Column.Hide.Contains($"{Model_Contact.ItemField.Address}"))
@@ -250,7 +251,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Contacts
if (responseData.Config.Column.Hide.Contains($"{Model_Contact.ItemField.Email}"))
columns.Remove(colEmail);
if (responseData.Config.Column.Hide.Contains($"{Model_Contact.ItemField.Associations}"))
columns.Remove(colAssocCount);
columns.Remove(colAssocOwnerCount);
// Sort Columns.
if (!prmAffiliate.SortColumns.Any())
@@ -279,7 +280,9 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Contacts
affiliateData.Field.Value.Add($"{Model_Contact.ItemField.Status}", SacsSystem.GetStatusDescForCode(values.GetString(colStatus.DataName)));
affiliateData.Field.Value.Add($"{Model_Contact.ItemField.CreatedOn}", Localiser.FormatDate(values.GetDate(colCreatedOn.DataName)));
affiliateData.Field.Value.Add($"{Model_Contact.ItemField.UpdatedOn}", Localiser.FormatDate(values.GetDate(colUpdatedOn.DataName)));
long assocCount = values.GetLong(colAssocCount.DataName);
long assocOwnerCount = values.GetLong(colAssocOwnerCount.DataName);
long assocTargetCount = values.GetLong(colAssocTargetCount.DataName);
long assocCount = assocOwnerCount + assocTargetCount;
if (assocCount > 0)
affiliateData.AssociationCount = assocCount;
// Cleanse item values.
@@ -33,6 +33,8 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Contacts
//-------------------------------------------------------------------
[HttpGet]
[Route("Contact")]
[Route("Contacts")]
public IActionResult Index(Model_Contact model)
{
if (!ValidUserSession)
@@ -24,6 +24,10 @@ using static Sarand.BusinessMgr.Net.Standard.Base.App.Sbm.Config.Database;
using Sarand.BusinessMgr.Net.Standard.Base.Bus.Control;
using Sarand.BusinessMgr.Net.Core.Web.Helpers;
using Sarand.BusinessMgr.Web.Portal.Helpers;
using DocumentFormat.OpenXml.EMMA;
using DocumentFormat.OpenXml.Wordprocessing;
using Sarand.BusinessMgr.Net.Core.Web.Models;
using Sarand.BusinessMgr.Web.Portal.Models;
namespace Sarand.BusinessMgr.Web.Portal.Controllers
{
@@ -218,6 +222,26 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers
base.OnActionExecuted(filterContext);
}
//-------------------------------------------------------------------
// Model.
/// <summary>
/// Populate model wioth session data etc.
/// <para>- Invoked from SbmController_Web.OnActionExecuting.</para>
/// </summary>
/// <param name="model"></param>
protected override void RefreshModel(SbmModel_Base model)
{
if (model == null) return;
base.RefreshModel(model);
SbmModel_Portal portalModel = model as SbmModel_Portal;
if (portalModel != null)
{
// Populate menu options.
portalModel.PopulateMenu();
}
}
//-------------------------------------------------------------------
// Keep Logged In.
//-------------------------------------------------------------------
@@ -238,47 +262,41 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers
String logSuffix = keepLoggedInParams.GetValidParams().Concat("", " ", ".");
if (keepLoggedInParams.IsValid())
{
// User may be defined on any one of the BusinessMgr databases - need to find out which.
// Identify session contexts with a BusMgr database that hosts the specified email address.
List<SbmSessionContext> sessionContexts = SbmSessionContextHelper_Portal.GetSessionContextForRepositoryContextUserEmail(keepLoggedInParams.RepositoryContext, keepLoggedInParams.EmailAddress);
if (sessionContexts.Any())
// Instance Repository configured on Control database identified by Context.
SbmSessionContext sessionContext = SbmSessionContextHelper_Portal.GetControlSessionContextByRepositoryContext(keepLoggedInParams.RepositoryContext);
if (sessionContext != null)
{
foreach (SbmSessionContext sessionContext in sessionContexts)
// Consoles and Managers must be associated with the specific context.
SbmUserContext_Web userContext = new SbmUserContext_Web(sessionContext);
using (SaSecurityConsole securityConsoleForUser = App.CreateSecurityConsole(userContext))
using (SbmAccountMgr accountMgrForUser = App.System.CreateAccountMgr(userContext))
{
// Consoles and Managers must be associated with the specific context.
SbmUserContext_Web userContext = new SbmUserContext_Web(sessionContext);
using (SaSecurityConsole securityConsoleForUser = App.CreateSecurityConsole(userContext))
using (SbmAccountMgr accountMgrForUser = App.System.CreateAccountMgr(userContext))
using (SaSecurityItem securityItemUser = securityConsoleForUser.GetUserByEmail(keepLoggedInParams.EmailAddress))
using (SbmAffiliate affiliateUser = accountMgrForUser.GetAffiliateById(keepLoggedInParams.AffiliateIdUser))
using (SbmAssociation associationOrganisation = accountMgrForUser.GetAssociationById(keepLoggedInParams.AssociationIdOrganisation))
using (SbmAffiliate affiliateOrganisation = accountMgrForUser.GetAffiliateById(associationOrganisation?.AffiliateIdOwner ?? null))
{
using (SaSecurityItem securityItemUser = securityConsoleForUser.GetUserByEmail(keepLoggedInParams.EmailAddress))
using (SbmAffiliate affiliateUser = accountMgrForUser.GetAffiliateById(keepLoggedInParams.AffiliateIdUser))
using (SbmAssociation associationOrganisation = accountMgrForUser.GetAssociationById(keepLoggedInParams.AssociationIdOrganisation))
using (SbmAffiliate affiliateOrganisation = accountMgrForUser.GetAffiliateById(associationOrganisation?.AffiliateIdOwner ?? null))
if (securityItemUser != null && affiliateUser != null && associationOrganisation != null && affiliateOrganisation != null)
{
if (securityItemUser != null && affiliateUser != null && associationOrganisation != null && affiliateOrganisation != null)
if (securityItemUser.Id.Equals(keepLoggedInParams.SecurityItemId) && affiliateUser.UserId.Equals(securityItemUser.Id))
{
if (securityItemUser.Id.Equals(keepLoggedInParams.SecurityItemId) && affiliateUser.UserId.Equals(securityItemUser.Id))
if (affiliateUser.IsStatusCurrent)
{
if (affiliateUser.IsStatusCurrent)
List<SysIdString> supervisedOrganisations = accountMgrForUser.GetAffiliatesSupervisedBy(affiliateUser.Id);
String errorMessage = "";
if (SignUserIn(userContext, keepLoggedInParams.RepositoryContext, securityConsoleForUser, accountMgrForUser, securityItemUser, affiliateUser, supervisedOrganisations, associationOrganisation, affiliateOrganisation, true, ref errorMessage))
{
List<SysIdString> supervisedOrganisations = accountMgrForUser.GetAffiliatesSupervisedBy(affiliateUser.Id);
String errorMessage = "";
if (SignUserIn(userContext, keepLoggedInParams.RepositoryContext, securityConsoleForUser, accountMgrForUser, securityItemUser, affiliateUser, supervisedOrganisations, associationOrganisation, affiliateOrganisation, true, ref errorMessage))
{
SaLogger.Info(logPrefix + " 'Keep Logged In' cookie found." + logSuffix);
return true;
}
else SaLogger.Warn(logPrefix + "failed to sign user in: " + errorMessage + "." + logSuffix);
SaLogger.Info(logPrefix + " 'Keep Logged In' cookie found." + logSuffix);
return true;
}
else SaLogger.Warn(logPrefix + "Affiliate is not active." + logSuffix);
else SaLogger.Warn(logPrefix + "failed to sign user in: " + errorMessage + "." + logSuffix);
}
else SaLogger.Warn(logPrefix + "Security Item and Affiliate are inconsistent. " + logSuffix);
else SaLogger.Warn(logPrefix + "Affiliate is not active." + logSuffix);
}
else SaLogger.Warn(logPrefix + "Security Item or Affiliate not found. " + logSuffix);
else SaLogger.Warn(logPrefix + "Security Item and Affiliate are inconsistent. " + logSuffix);
}
else SaLogger.Warn(logPrefix + "Security Item or Affiliate not found. " + logSuffix);
}
if (ValidUserSession)
break;
}
}
else SaLogger.Warn($"{SysUtil.Fn} Keep Logged In cookie found but no matching session contexts found." + logSuffix);
@@ -69,30 +69,12 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
// Instance Repository configured on Control database.
SbmSessionContext sessionContext = SbmSessionContextHelper_Portal.GetControlSessionContextByRepositoryContext(model.RepositoryContext);
String errorMessage = "";
if (!OpenUserSession(model, sessionContext, ref errorMessage))
{
if (!StringUtil.IsBlank(errorMessage))
return Json(responseData.SetControlError(nameof(model.EmailAddress), errorMessage));
}
}
if (!(SessionData_Portal?.ValidUser ?? false))
{
// Session contexts with a BusMgr database that hosts the specified email address.
List<SbmSessionContext> sessionContexts = SbmSessionContextHelper_Portal.GetSessionContextForRepositoryContextUserEmail(model.RepositoryContext, model.EmailAddress);
if (sessionContexts.IsEmpty())
return Json(responseData.SetControlError(nameof(model.EmailAddress), model.Localiser["security.msg.signin.invalid"]));
foreach (SbmSessionContext sessionContext in sessionContexts)
{
String errorMessage = "";
if (OpenUserSession(model, sessionContext, ref errorMessage)) break;
if (!StringUtil.IsBlank(errorMessage))
return Json(responseData.SetControlError(nameof(model.EmailAddress), errorMessage));
}
}
}
if (!(SessionData_Portal?.ValidUser ?? false))
@@ -125,14 +107,14 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
disposables.Add(accountMgrForUser);
// User's security item.
SaSecurityItem securityItemUser = GetSecurityItemForSignin(securityConsoleForUser, model);
SaSecurityItem securityItemUser = GetSecurityItemForSignin(userContext, securityConsoleForUser, model);
disposables.Add(securityItemUser);
if (securityItemUser == null)
{
SaLogger.Warn($"{SysUtil.Fn} Failed to retrieve Security Item. Database: {busMgrDatabase.DatabaseName}. Email: {model.EmailAddress}.");
return false;
}
if (!securityConsoleForUser.PasswordMatches(securityItemUser, model.Password))
if (!securityConsoleForUser.PasswordMatches(securityItemUser, model.Password, userContext.DbUpdateInfo))
{
SaLogger.Warn($"{SysUtil.Fn} Supplied password doesn't match Security Item. Database: {busMgrDatabase.DatabaseName}. Email: {model.EmailAddress}.");
return false;
@@ -182,7 +164,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
}
//-------------------------------------------------------------------
private SaSecurityItem GetSecurityItemForSignin(SaSecurityConsole securityConsoleForUser, Model_Security model)
private SaSecurityItem GetSecurityItemForSignin(SbmUserContext_Web userContext, SaSecurityConsole securityConsoleForUser, Model_Security model)
{
// Retrieve security item by email address.
SaSecurityItem securityItemUser = securityConsoleForUser.GetUserByEmail(model.EmailAddress);
@@ -198,7 +180,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
// Record invalid login attempt.
securityItemUser.InvalidLoginCount++;
securityItemUser.LastInvalidLoginAt = DateTime.Now;
if (!securityConsoleForUser.UpdateSecurityItem(securityItemUser, DbUpdateInfo))
if (!securityConsoleForUser.UpdateSecurityItem(securityItemUser, userContext.DbUpdateInfo))
SaLogger.Warn($"{SysUtil.Fn} Failed to store invalid sign-in attempt against User: {model.EmailAddress}. {AccountMgr.ErrorStateMessage}", true);
SaLogger.Warn($"{SysUtil.Fn} Attempt to sign-in to User {model.EmailAddress} with invalid password. Attempt: {securityItemUser.InvalidLoginCount}.", true);
securityItemUser.Dispose(); securityItemUser = null;
@@ -410,15 +392,14 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
RequestPasswordParams storePasswordParams = ExtractRequestPasswordLinkParams(model.rpe);
if (storePasswordParams.IsValid())
{
// User may be defined on any one of the BusinessMgr databases - need to find out which.
// Identify session contexts for databases with specified Id that hosts the specified email address.
List<SbmSessionContext> sessionContexts = SbmSessionContextHelper_Portal.GetSessionContextForRepositoryContextUserEmail(storePasswordParams.RepositoryContext, storePasswordParams.EmailAddress);
foreach (SbmSessionContext sessionContext in sessionContexts)
// Instance Repository configured on Control database identified by Context.
SbmSessionContext sessionContext = SbmSessionContextHelper_Portal.GetControlSessionContextByRepositoryContext(storePasswordParams.RepositoryContext);
if (sessionContext != null)
{
// Consoles and Managers must be associated with the specific context.
SbmUserContext_Web userContext = new SbmUserContext_Web(sessionContext);
using (SaSecurityConsole securityConsoleForUser = App.CreateSecurityConsole(userContextForUser))
using (SbmAccountMgr accountMgrForUser = App.System.CreateAccountMgr(userContextForUser))
using (SaSecurityConsole securityConsoleForUser = App.CreateSecurityConsole(userContext))
using (SbmAccountMgr accountMgrForUser = App.System.CreateAccountMgr(userContext))
{
// Retrieve user details and validate email address.
securityItemUser = securityConsoleForUser.GetSecurityItemById(SaSecurity.ItemType.User, storePasswordParams.SecurityItemId);
@@ -429,7 +410,6 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
userContextForUser = userContext;
validRequestPasswordParams = true;
validSessionRequired = false;
break;
}
else
{
@@ -471,7 +451,8 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
String oldPasswordHashed = securityItemUser.PasswordHash;
// Store password.
securityItemUser.PasswordHash = SbmPasswordHelper.GetPasswordHash(securityItemUser, model.Password);
// NB Do not use Microsoft.AspNetCore.Identity.PasswordHasher.HashPassword() while legacy (C++) desktop application still in use.
securityConsole.SetPassword(securityItemUser, model.Password);
securityItemUser.PasswordChangedAt = DateTime.Now;
// Remove new password from history if present.
for (int passwordHistoryIdx = 0; passwordHistoryIdx < securityItemUser.PasswordHistory.Count; passwordHistoryIdx++)
@@ -489,7 +470,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
securityItemUser.PasswordHistory.Add(oldPasswordHashed);
while (securityItemUser.PasswordHistory.Count > 10)
securityItemUser.PasswordHistory.RemoveAt(securityItemUser.PasswordHistory.Count - 1);
if (!securityConsole.UpdateSecurityItem(securityItemUser, DbUpdateInfo))
if (!securityConsole.UpdateSecurityItem(securityItemUser, userContextForUser?.DbUpdateInfo ?? DbUpdateInfo))
{
SaLogger.Warn($"{SysUtil.Fn} Failed to change password for {securityItemUser.EmailAddress}: {securityConsole.ErrorStateMessage}");
return Json(responseData.SetControlError(nameof(model.Password), Localiser["security.msg.change.password.failed"]));
@@ -537,9 +518,9 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
model.ValidateRequestPasswordData(responseData);
if (responseData.HasControlErrors()) return Json(responseData);
// Identify session contexts with a BusMgr database that hosts the specified email address.
List<SbmSessionContext> sessionContexts = SbmSessionContextHelper_Portal.GetSessionContextForRepositoryContextUserEmail(model.RepositoryContext, model.EmailAddress);
foreach (SbmSessionContext sessionContext in sessionContexts)
// Instance Repository configured on Control database identified by Context.
SbmSessionContext sessionContext = SbmSessionContextHelper_Portal.GetControlSessionContextByRepositoryContext(model.RepositoryContext);
if (sessionContext != null)
{
SbmUserContext_Web userContext = new SbmUserContext_Web(sessionContext);
@@ -76,10 +76,9 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
{
String logPrefix = $"{SysUtil.Fn} 'Request Password' parameters supplied but ";
String logSuffix = changePasswordParams.GetValidParams().Concat("", " ", ".");
// User may be defined on any one of the BusinessMgr databases - need to find out which.
// Identify session contexts with a BusMgr database that hosts the specified email address.
List<SbmSessionContext> sessionContexts = SbmSessionContextHelper_Portal.GetSessionContextForRepositoryContextUserEmail(changePasswordParams.RepositoryContext, changePasswordParams.EmailAddress);
foreach (SbmSessionContext sessionContext in sessionContexts)
// Instance Repository configured on Control database identified by Context.
SbmSessionContext sessionContext = SbmSessionContextHelper_Portal.GetControlSessionContextByRepositoryContext(changePasswordParams.RepositoryContext);
if (sessionContext != null)
{
// Consoles and Managers must be associated with the specific context.
SbmUserContext_Web userContext = new SbmUserContext_Web(sessionContext);
@@ -99,7 +98,6 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
{
model.rpe = encryptedParams;
validSessionRequired = false;
break;
}
else SaLogger.Warn(logPrefix + "Security Item and Affiliate are incompatible with parameters. " + logSuffix);
}
@@ -63,6 +63,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Helpers
//-------------------------------------------------------------------
// Session Contexts for Repository Context and User Email Address.
//-------------------------------------------------------------------
[Obsolete("Deprecated - BusMgr databases only referenced via Control database rather than in application config.")]
/// <summary>
/// Identify the session context(s) that corresponds with the BusinessMgr
/// repository by context for the specified user by email address.
@@ -40,6 +40,60 @@ namespace Sarand.BusinessMgr.Web.Portal.Models
public bool UserCanCreateItem(Sbm.ItemType itemType) { return SessionData_Portal.SecurityProfile_Core.UserCanCreateItemType(itemType); }
public bool UserCanUpdateItem(Sbm.ItemType itemType) { return SessionData_Portal.SecurityProfile_Core.UserCanUpdateItemType(itemType); }
//-------------------------------------------------------------------
// Page menu options.
public class MenuOptionData
{
public String Caption { get; set; }
public String Page { get; set; }
}
public class MenuItemData
{
public String Caption { get; set; }
public List<MenuOptionData> Options { get; } = new List<MenuOptionData>();
}
public class MenuData
{
public List<MenuItemData> Items { get; } = new List<MenuItemData>();
}
public MenuData Menu { get; set; }
/// <summary>
/// Populate available menu options for the page.
/// <para>- Invoked from SbmController_Portal.OnActionExecuting.</para>
/// </summary>
public void PopulateMenu()
{
Menu = new MenuData();
// Contacts.
MenuItemData contactsItem = new MenuItemData() { Caption = @Localiser["label.contacts"] };
contactsItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.contacts"], Page = "Contacts" });
contactsItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.customers"], Page = "Customers" });
contactsItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.suppliers"], Page = "Suppliers" });
contactsItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.associations"], Page = "Associations" });
if (contactsItem.Options.Any())
Menu.Items.Add(contactsItem);
// Commodities.
MenuItemData commoditiesItem = new MenuItemData() { Caption = @Localiser["label.commodities"] };
commoditiesItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.commodities"], Page = "Commodity" });
commoditiesItem.Options.Add(new MenuOptionData() { Caption = @Localiser["caption.asset.categories"], Page = "Assets/AssetCategory" });
commoditiesItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.assets"], Page = "Assets/Asset" });
commoditiesItem.Options.Add(new MenuOptionData() { Caption = @Localiser["caption.vehicle.classes"], Page = "Assets/VehicleClass" });
commoditiesItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.vehicles"], Page = "Assets/Vehicle" });
if (commoditiesItem.Options.Any())
Menu.Items.Add(commoditiesItem);
// Reference.
MenuItemData referenceItem = new MenuItemData() { Caption = @Localiser["label.reference"] };
referenceItem.Options.Add(new MenuOptionData() { Caption = @Localiser["caption.code.types"], Page = "Codes/CodeType" });
referenceItem.Options.Add(new MenuOptionData() { Caption = @Localiser["label.codes"], Page = "Codes/Code" });
if (referenceItem.Options.Any())
Menu.Items.Add(referenceItem);
}
//-------------------------------------------------------------------
// View reference data.