1118 lines
44 KiB
C#
1118 lines
44 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using System.Security.Cryptography;
|
|
using biz = GZTW.AyaNova.BLL;
|
|
|
|
|
|
|
|
namespace AyaNova.PlugIn.V8
|
|
{
|
|
class util
|
|
{
|
|
public static string RELEASE_VERSION_STRING { get; set; }
|
|
|
|
public static GZTW.AyaNova.BLL.LocalizedTextTable LocaleText = null;
|
|
public const string TEST_ROUTE = "notify/hello";
|
|
public const string API_BASE_ROUTE = "api/v8/";
|
|
private const int MAX_TRIES = 3;//max times to retry an api call before giving up
|
|
private const int API_RETRY_DELAY = 3000;//pause in ms before retrying api call
|
|
public static int HTTPCLIENT_TIMEOUT_SECONDS = 100;//changed by the setting in ops anyway, just a in-case sensible default here
|
|
public static HttpClient client = null;
|
|
//url once known to be good
|
|
internal static string ApiBaseUrl { get; set; }
|
|
//auth processes url for api and this is the best guess as to the client url to use for notification / help links etc
|
|
internal static string GuessClientUrl { get; set; }
|
|
|
|
internal static string JWT { get; set; }
|
|
|
|
public static Guid WBIIntegrationID { get { return new Guid("{6946040C-1B50-4eab-BE08-A0E93DB7449F}"); } }
|
|
public static Guid RIIntegrationID { get { return new Guid("{F3E11847-B148-4CDC-88A7-BDB144EE5743}"); } }
|
|
|
|
|
|
|
|
#region INIT / AUTH
|
|
public static void InitClient()
|
|
{
|
|
if (client != null)
|
|
{
|
|
client.Dispose();
|
|
client = null;
|
|
}
|
|
client = new HttpClient();
|
|
client.Timeout = TimeSpan.FromSeconds(HTTPCLIENT_TIMEOUT_SECONDS);
|
|
|
|
//client.Timeout = new TimeSpan(0, 0, 45);
|
|
//client.BaseAddress = new Uri(ApiBaseUrl);
|
|
// client.DefaultRequestHeaders.Accept.Clear();
|
|
// client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
//
|
|
}
|
|
|
|
/// <summary>
|
|
/// Only a return value of "OK" is ok
|
|
/// </summary>
|
|
/// <param name="serverUrl"></param>
|
|
/// <returns></returns>
|
|
public static async Task<string> InitAndConfirmAddressAsync(string serverUrl)
|
|
{
|
|
ApiBaseUrl = serverUrl;
|
|
InitClient();
|
|
try
|
|
{
|
|
// TimeSpan tsDefault = client.Timeout;
|
|
// client.Timeout = new TimeSpan(0, 0, 20);
|
|
HttpResponseMessage response = await client.GetAsync(serverUrl + TEST_ROUTE);
|
|
// client.Timeout = tsDefault;
|
|
if (response.IsSuccessStatusCode)
|
|
return "OK";
|
|
else
|
|
return "Failed: " + response.StatusCode.ToString();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
while (ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
return "Failed exception: \r\n" + ex.Message;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
public async static Task<bool> AuthenticateAsync(string login, string password = null)
|
|
{
|
|
InitClient();
|
|
|
|
if (password == null)
|
|
password = login;
|
|
|
|
dynamic creds = new JObject();
|
|
creds.login = login;
|
|
creds.password = password;
|
|
|
|
ApiResponse a = await PostAsync("auth", creds.ToString());
|
|
|
|
if (a.HttpResponse.IsSuccessStatusCode)
|
|
{
|
|
JWT = a.ObjectResponse["data"]["token"].Value<string>();
|
|
//Must be *the* SuperUser to continue:
|
|
a = await GetAsync("user/amsu");
|
|
var IsSuperUser = a.ObjectResponse["data"].Value<bool>();
|
|
if (!IsSuperUser)
|
|
{
|
|
JWT = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> GetAsync(string route)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryGetAsync(route);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
}
|
|
|
|
private async static Task<ApiResponse> TryGetAsync(string route)
|
|
{
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + route);
|
|
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception("GET error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace);
|
|
}
|
|
|
|
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new Exception("GET error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase);
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> PostAsync(string route, dynamic d)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPostAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> PostAsync(string route, string s = null)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPostAsync(route, s);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
}
|
|
|
|
|
|
private async static Task<ApiResponse> TryPostAsync(string route, string postJson = null)
|
|
{
|
|
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + route);
|
|
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
if (!string.IsNullOrWhiteSpace(postJson))
|
|
requestMessage.Content = new StringContent(postJson, System.Text.Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception("POST error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace + "\r\nPOSTED OBJECT:\r\n" + postJson);
|
|
}
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(postJson))
|
|
postJson = "n/a";
|
|
throw new Exception("POST error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase + "\r\nPOSTED OBJECT:\r\n" + postJson);
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async static Task<ApiResponse> PutAsync(string route, dynamic d)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPutAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> PutAsync(string route)
|
|
{
|
|
Exception FirstException = null;
|
|
|
|
for (int x = 0; x < MAX_TRIES; x++)
|
|
{
|
|
try
|
|
{
|
|
return await TryPutAsync(route);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (FirstException == null)
|
|
FirstException = ex;
|
|
}
|
|
await Task.Delay(API_RETRY_DELAY);
|
|
}
|
|
|
|
//no luck re-throw the exception
|
|
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
}
|
|
|
|
public async static Task<ApiResponse> TryPutAsync(string route, string putJson = null)
|
|
{
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + route);
|
|
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
if (!string.IsNullOrWhiteSpace(putJson))
|
|
requestMessage.Content = new StringContent(putJson, System.Text.Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception("PUT error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace + "\r\nPOSTED OBJECT:\r\n" + putJson);
|
|
}
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(putJson))
|
|
putJson = "n/a";
|
|
throw new Exception("PUT error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase + "\r\nPUT OBJECT:\r\n" + putJson);
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//public async static Task<ApiResponse> PostFormDataAsync(string route, MultipartFormDataContent formContent)
|
|
//{
|
|
// Exception FirstException = null;
|
|
|
|
// for (int x = 0; x < MAX_TRIES; x++)
|
|
// {
|
|
// try
|
|
// {
|
|
// return await TryPostFormDataAsync(route, formContent);
|
|
// }
|
|
// catch (Exception ex)
|
|
// {
|
|
// if (ex.Message.Contains("413")) throw;//too large, no point in retrying at all let upstream caller handle it immediately
|
|
|
|
// if (FirstException == null)
|
|
// FirstException = ex;
|
|
// }
|
|
// await Task.Delay(API_RETRY_DELAY);
|
|
// }
|
|
|
|
// //no luck re-throw the exception
|
|
// throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
|
|
//}
|
|
|
|
//NO RETRY TEST VERSION OF ABOVE
|
|
//UNCOMMENT ABOVE AND COMMENT THIS TO GO BACK
|
|
public async static Task<ApiResponse> PostFormDataAsync(string route, MultipartFormDataContent formContent)
|
|
{
|
|
return await TryPostFormDataAsync(route, formContent);
|
|
}
|
|
|
|
|
|
private async static Task<ApiResponse> TryPostFormDataAsync(string route, MultipartFormDataContent formContent)
|
|
{
|
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + route);
|
|
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
|
|
if (!string.IsNullOrWhiteSpace(JWT))
|
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
|
|
|
|
requestMessage.Content = formContent;
|
|
|
|
|
|
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
response = await client.SendAsync(requestMessage);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
var Err = ex.Message;
|
|
var InnerErr = "";
|
|
if (ex.InnerException != null)
|
|
InnerErr = ex.InnerException.Message;
|
|
throw new Exception("POST FORMDATA error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace);
|
|
}
|
|
|
|
|
|
var responseAsString = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new Exception("POST FORMDATA error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase);
|
|
}
|
|
else
|
|
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
|
|
}
|
|
|
|
//eoc
|
|
public class ApiResponse
|
|
{
|
|
public HttpResponseMessage HttpResponse { get; set; }
|
|
public JObject ObjectResponse { get; set; }
|
|
public string CompactResponse
|
|
{
|
|
get
|
|
{
|
|
return ObjectResponse.ToString(Newtonsoft.Json.Formatting.None);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//make an event log entry to preserve the original v7 history such as it is
|
|
public async static Task EventLog(AyaType RavenType, long RavenId, long RavenCreatorId, long RavenModifierId, string sCreated, string sModified)
|
|
{
|
|
//DateTime Created = DateTime.UtcNow;
|
|
//if (!string.IsNullOrWhiteSpace(sCreated))
|
|
// Created = DateTime.Parse(sCreated).ToUniversalTime();
|
|
|
|
//DateTime Modified = DateTime.UtcNow;
|
|
//if (!string.IsNullOrWhiteSpace(sCreated))
|
|
// Modified = DateTime.Parse(sModified).ToUniversalTime();
|
|
|
|
dynamic d = new JObject();
|
|
d.ayaType = (int)RavenType;
|
|
d.ayId = RavenId;
|
|
d.creator = RavenCreatorId;
|
|
d.modifier = RavenModifierId;
|
|
d.created = DateToV8(sCreated, true);
|
|
d.modified = DateToV8(sModified, true);
|
|
|
|
await PostAsync("event-log/v7", d.ToString());
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="jsonString"></param>
|
|
/// <returns></returns>
|
|
public static JObject Parse(string jsonString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(jsonString))
|
|
{
|
|
return null;
|
|
}
|
|
return JObject.Parse(jsonString);
|
|
}
|
|
|
|
|
|
public static long IdFromResponse(ApiResponse a)
|
|
{
|
|
return a.ObjectResponse["data"]["id"].Value<long>();
|
|
}
|
|
|
|
public static uint CTokenFromResponse(ApiResponse a)
|
|
{
|
|
return a.ObjectResponse["data"]["concurrency"].Value<uint>();
|
|
}
|
|
#endregion
|
|
|
|
#region Misc utils
|
|
|
|
//Used to ensure a unique name generated by appending -nnn is within length requirements by splitting and chopping part of text to keep name
|
|
public static string UniqueNameBuilder(string oldName, long appendValue, int maxLength)
|
|
{
|
|
//deadman switch
|
|
if (appendValue > int.MaxValue)
|
|
{
|
|
throw new System.OverflowException("UniqueNameBuilder: Unique name could not be generated for item \"" + oldName + "\" after " + int.MaxValue.ToString() + " attempts");
|
|
}
|
|
var appendString = "-" + appendValue.ToString();
|
|
string ret = oldName + appendString;
|
|
var diff = maxLength - ret.Length;
|
|
if (diff < 0)
|
|
{
|
|
ret = oldName.Substring(0, Math.Abs(diff)) + appendString;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
//used to set nonsense values from imported user login and password
|
|
//just in case they are set active after import but never have their passwords set
|
|
//so they can't be as easily hacked into
|
|
public static string RandomString()
|
|
{
|
|
var b = new byte[32];
|
|
var random = RandomNumberGenerator.Create();
|
|
random.GetNonZeroBytes(b);
|
|
return Convert.ToBase64String(b);
|
|
}
|
|
|
|
|
|
public static string NormalizeTag(string inObj)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(inObj)) return null;
|
|
//Must be lowercase per rules
|
|
//This may be naive when we get international cust omers but for now supporting utf-8 and it appears it's safe to do this with unicode
|
|
inObj = inObj.ToLowerInvariant();
|
|
//No spaces in tags, replace with dashes
|
|
inObj = inObj.Replace(" ", "-");
|
|
//Remove multiple dash sequences
|
|
inObj = System.Text.RegularExpressions.Regex.Replace(inObj, "-+", "-");
|
|
//Ensure doesn't start or end with a dash
|
|
inObj = inObj.Trim('-');
|
|
//No longer than 255 characters
|
|
inObj = MaxLength(inObj, 255);
|
|
return inObj;
|
|
}
|
|
|
|
|
|
//clean up tags from client submission
|
|
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
|
|
//and sorts before returning to ensure consistent ordering
|
|
public static List<string> NormalizeTags(List<string> inTags)
|
|
{
|
|
if (inTags == null || inTags.Count == 0) return inTags;
|
|
|
|
List<string> outTags = new List<string>();
|
|
foreach (var tag in inTags)
|
|
outTags.Add(NormalizeTag(tag));
|
|
outTags.Sort();
|
|
return outTags.Distinct().ToList();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Trim a string if necessary
|
|
/// </summary>
|
|
/// <param name="s"></param>
|
|
/// <param name="maxLength"></param>
|
|
/// <returns></returns>
|
|
public static string MaxLength(string s, int maxLength)
|
|
{
|
|
if (s.Length > maxLength)
|
|
s = s.Substring(0, maxLength);
|
|
return s;
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////
|
|
// v7 to v8 Date conversion
|
|
//NOTE: ensure null for empty date
|
|
//
|
|
public static string DateToV8(object o, bool neverEmpty = false) { return DateToV8(o.ToString(), neverEmpty); }
|
|
public static string DateToV8(string s, bool neverEmpty = false)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s))
|
|
{
|
|
if (neverEmpty)
|
|
{
|
|
return DateTime.UtcNow.ToString("o");
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
DateTime result = new DateTime();
|
|
if (DateTime.TryParse(s, out result))
|
|
return result.ToUniversalTime().ToString("o");
|
|
|
|
//couldn't be parsed
|
|
if (neverEmpty)
|
|
return DateTime.UtcNow.ToString("o");
|
|
return null;
|
|
}
|
|
|
|
///////////////////////////////
|
|
// v7 to v8 Date conversion
|
|
//
|
|
//
|
|
public static bool DateIsPast(object o, bool neverEmpty = false) { return DateIsPast(o.ToString(), neverEmpty); }
|
|
public static bool DateIsPast(string s, bool neverEmpty = false)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s))
|
|
return false;//no date so not past
|
|
|
|
|
|
DateTime result = new DateTime();
|
|
if (DateTime.TryParse(s, out result))
|
|
return result < DateTime.Now;
|
|
//can't parse so no, it's not past
|
|
return false;
|
|
}
|
|
|
|
|
|
public enum AyaUiFieldDataType : int
|
|
{
|
|
NoType = 0,
|
|
DateTime = 1,
|
|
Date = 2,
|
|
Time = 3,
|
|
Text = 4,
|
|
Integer = 5,
|
|
Bool = 6,
|
|
Decimal = 7,
|
|
Currency = 8,
|
|
Tags = 9,
|
|
Enum = 10,
|
|
EmailAddress = 11
|
|
|
|
}
|
|
|
|
|
|
public static AyaType RootObjectToAyaType(biz.RootObjectTypes rot, biz.WorkorderTypes specificWoType = biz.WorkorderTypes.Unknown)
|
|
{
|
|
|
|
switch (rot)
|
|
{
|
|
case biz.RootObjectTypes.User:
|
|
return AyaType.User;
|
|
case biz.RootObjectTypes.Client:
|
|
return AyaType.Customer;
|
|
case biz.RootObjectTypes.Contract:
|
|
return AyaType.Contract;
|
|
case biz.RootObjectTypes.HeadOffice:
|
|
return AyaType.HeadOffice;
|
|
case biz.RootObjectTypes.LoanItem:
|
|
return AyaType.LoanUnit;
|
|
case biz.RootObjectTypes.Part:
|
|
return AyaType.Part;
|
|
case biz.RootObjectTypes.WorkorderPreventiveMaintenance:
|
|
return AyaType.PM;
|
|
case biz.RootObjectTypes.WorkorderItem:
|
|
switch (specificWoType)
|
|
{
|
|
case biz.WorkorderTypes.PreventiveMaintenance:
|
|
return AyaType.PMItem;
|
|
case biz.WorkorderTypes.Quote:
|
|
return AyaType.QuoteItem;
|
|
case biz.WorkorderTypes.Service:
|
|
return AyaType.WorkOrderItem;
|
|
default:
|
|
throw new NotImplementedException("V8:util:RootObjectToAyaType -> type " + rot.ToString() + "," + specificWoType.ToString() + " is not coded yet");
|
|
}
|
|
case biz.RootObjectTypes.Workorder:
|
|
switch (specificWoType)
|
|
{
|
|
case biz.WorkorderTypes.PreventiveMaintenance:
|
|
return AyaType.PM;
|
|
case biz.WorkorderTypes.Quote:
|
|
return AyaType.Quote;
|
|
case biz.WorkorderTypes.Service:
|
|
return AyaType.WorkOrder;
|
|
default:
|
|
throw new NotImplementedException("V8:util:RootObjectToAyaType -> type " + rot.ToString() + ", needs specific wotype which was not provided");
|
|
}
|
|
|
|
case biz.RootObjectTypes.Memo:
|
|
return AyaType.Memo;
|
|
case biz.RootObjectTypes.Project:
|
|
return AyaType.Project;
|
|
case biz.RootObjectTypes.PurchaseOrder:
|
|
return AyaType.PurchaseOrder;
|
|
case biz.RootObjectTypes.Unit:
|
|
return AyaType.Unit;
|
|
case biz.RootObjectTypes.UnitModel:
|
|
return AyaType.UnitModel;
|
|
case biz.RootObjectTypes.Vendor:
|
|
return AyaType.Vendor;
|
|
case biz.RootObjectTypes.WorkorderItemLabor:
|
|
return AyaType.WorkOrderItemLabor;
|
|
case biz.RootObjectTypes.WorkorderItemTravel:
|
|
return AyaType.WorkOrderItemTravel;
|
|
//case biz.RootObjectTypes.ServiceBank:
|
|
// return AyaType.ServiceBank;
|
|
|
|
|
|
default:
|
|
throw new NotImplementedException("V8:util:RootObjectToAyaType -> v7 type " + rot.ToString() + " is not coded");
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Days of week
|
|
/// </summary>
|
|
[Flags]
|
|
public enum AyaDaysOfWeek : int
|
|
{
|
|
//https://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c
|
|
//MAX 31 (2147483647)!!! or will overflow int and needs to be turned into a long
|
|
//Must be a power of two: https://en.wikipedia.org/wiki/Power_of_two
|
|
//bitwise selection of days of week
|
|
//https://stackoverflow.com/a/24174625/8939
|
|
|
|
Monday = 1,
|
|
Tuesday = 2,
|
|
Wednesday = 4,
|
|
Thursday = 8,
|
|
Friday = 16,
|
|
Saturday = 32,
|
|
Sunday = 64
|
|
}
|
|
public enum AyaType : int
|
|
{
|
|
//COREBIZOBJECT attribute must be set on objects that are:
|
|
//Attachable objects can have attachments,
|
|
//wikiable objects can have a wiki
|
|
//reviewable objects can have a review which is basically the same as a Reminder but with an object attached (was follow up schedmarker in v7)
|
|
//PIckList-able (has picklist template)
|
|
//Pretty much everything that represents some kind of real world object is wikiable or attachable as long as it has an ID and a type
|
|
//exceptions would be utility type objects like datalistview, formcustom etc that are not
|
|
|
|
//NOTE: NEW CORE OBJECTS - All areas of server AND CLIENT code that require adding any new core objects have been tagged with the following comment:
|
|
//CoreBizObject add here
|
|
//Search for that IN SERVER AND CLIENT CODE and you will see all areas that need coding for the new object
|
|
|
|
//***IMPORTANT: Also need to add translations for any new biz objects added that don't match exactly the name here in the key
|
|
//because enumlist gets it that way, i.e. "Global" would be the expected key
|
|
|
|
|
|
NoType = 0,
|
|
Global = 1,
|
|
//corebizobject
|
|
Widget = 2,
|
|
//corebizobject
|
|
User = 3,
|
|
ServerState = 4,
|
|
License = 5,
|
|
LogFile = 6,
|
|
PickListTemplate = 7,
|
|
//corebizobject
|
|
Customer = 8,
|
|
ServerJob = 9,
|
|
//corebizobject
|
|
Contract = 10,
|
|
TrialSeeder = 11,
|
|
ServerMetrics = 12,
|
|
Translation = 13,
|
|
UserOptions = 14,
|
|
//corebizobject
|
|
HeadOffice = 15,
|
|
//corebizobject
|
|
LoanUnit = 16,
|
|
FileAttachment = 17,
|
|
DataListSavedFilter = 18,
|
|
FormCustom = 19,
|
|
//corebizobject
|
|
Part = 20,
|
|
//corebizobject
|
|
PM = 21,
|
|
//corebizobject
|
|
PMItem = 22,
|
|
//corebizobject
|
|
QuoteItemExpense = 23,
|
|
//corebizobject
|
|
QuoteItemLabor = 24,
|
|
//corebizobject
|
|
Project = 25,
|
|
//corebizobject
|
|
PurchaseOrder = 26,
|
|
//corebizobject
|
|
Quote = 27,
|
|
//corebizobject
|
|
QuoteItem = 28,
|
|
//corebizobject
|
|
QuoteItemLoan = 29,
|
|
//corebizobject
|
|
QuoteItemPart = 30,
|
|
//corebizobject
|
|
Unit = 31,
|
|
//corebizobject
|
|
UnitModel = 32,
|
|
//corebizobject
|
|
Vendor = 33,
|
|
//--- WorkOrder
|
|
//corebizobject
|
|
WorkOrder = 34,
|
|
//corebizobject
|
|
WorkOrderItem = 35,
|
|
//corebizobject
|
|
WorkOrderItemExpense = 36,
|
|
//corebizobject
|
|
WorkOrderItemLabor = 37,
|
|
//corebizobject
|
|
WorkOrderItemLoan = 38,
|
|
//corebizobject
|
|
WorkOrderItemPart = 39,
|
|
//corebizobject
|
|
WorkOrderItemPartRequest = 40,
|
|
//corebizobject
|
|
WorkOrderItemScheduledUser = 41,
|
|
//corebizobject
|
|
WorkOrderItemTask = 42,
|
|
//corebizobject
|
|
WorkOrderItemTravel = 43,
|
|
//corebizobject
|
|
WorkOrderItemUnit = 44,
|
|
//---
|
|
//corebizobject
|
|
QuoteItemScheduledUser = 45,
|
|
//corebizobject
|
|
QuoteItemTask = 46,
|
|
GlobalOps = 47,//really only used for rights, not an object type of any kind
|
|
BizMetrics = 48,//deprecate? Not used for anything as of nov 2020
|
|
Backup = 49,
|
|
Notification = 50,
|
|
NotifySubscription = 51,
|
|
//corebizobject
|
|
Reminder = 52,
|
|
UnitMeterReading = 53,
|
|
//corebizobject
|
|
CustomerServiceRequest = 54,
|
|
// ServiceBank = 55,
|
|
OpsNotificationSettings = 56,
|
|
Report = 57,
|
|
DashboardView = 58,
|
|
//corebizobject
|
|
CustomerNote = 59,
|
|
//corebizobject
|
|
Memo = 60,
|
|
//corebizobject
|
|
Review = 61,
|
|
//corebizobject
|
|
ServiceRate = 62,
|
|
//corebizobject
|
|
TravelRate = 63,
|
|
//corebizobject
|
|
TaxCode = 64,
|
|
//corebizobject
|
|
PartAssembly = 65,
|
|
//corebizobject
|
|
PartWarehouse = 66,
|
|
PartInventory = 67,
|
|
DataListColumnView = 68,
|
|
PartInventoryRestock = 69,//for list only, synthetic object
|
|
PartInventoryRequest = 70,//for list only not, synthetic object
|
|
WorkOrderStatus = 71,
|
|
TaskGroup = 72,
|
|
WorkOrderItemOutsideService = 73,
|
|
WorkOrderItemPriority = 74,
|
|
WorkOrderItemStatus = 75,
|
|
//corebizobject
|
|
QuoteItemTravel = 76,
|
|
//corebizobject
|
|
QuoteItemUnit = 77,
|
|
QuoteStatus = 78,
|
|
QuoteItemOutsideService = 79,
|
|
//corebizobject
|
|
PMItemExpense = 80,
|
|
//corebizobject
|
|
PMItemLabor = 81,
|
|
//corebizobject
|
|
PMItemLoan = 82,
|
|
//corebizobject
|
|
PMItemPart = 83,
|
|
//corebizobject
|
|
PMItemPartRequest = 84,
|
|
//corebizobject
|
|
PMItemScheduledUser = 85,
|
|
//corebizobject
|
|
PMItemTask = 86,
|
|
//corebizobject
|
|
PMItemTravel = 87,
|
|
//corebizobject
|
|
PMItemUnit = 88,
|
|
PMItemOutsideService = 89
|
|
|
|
|
|
|
|
//NOTE: New objects added here need to also be added to the following classes:
|
|
//AyaNova.Biz.BizObjectExistsInDatabase
|
|
//AyaNova.Biz.BizObjectFactory
|
|
//AyaNova.Biz.BizRoles
|
|
//AyaNova.Biz.BizObjectNameFetcherDIRECT
|
|
//and in the CLIENT in ayatype.js
|
|
//and their model needs to have the ICoreBizObjectModel interface
|
|
|
|
//and need TRANSLATION KEYS because any type could show in the event log at the client end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static bool LocaleIsCustomized(string localeName, GZTW.AyaNova.BLL.LocalizedTextTable lt, ProgressForm progress)
|
|
{
|
|
//checksum locales
|
|
//if a recognized checksum then don't import because it means it wasn't modified from stock
|
|
/*
|
|
Stock fresh trial db (fb) values:
|
|
Locale Custom English is customized [signature: -2007218741] exporting
|
|
Locale Deutsch is customized [signature: -605335883] exporting
|
|
Locale English is customized [signature: -2007218741] exporting
|
|
Locale Español is customized [signature: -1164206951] exporting
|
|
Locale Français is customized [signature: -720441286] exporting
|
|
|
|
Scotts:
|
|
Locale Custom English is customized [signature: 117115560] exporting
|
|
Locale Deutsch is customized [signature: 772843262] exporting
|
|
Locale English is customized [signature: 1382500515] exporting
|
|
Locale Español is customized [signature: 438715542] exporting
|
|
Locale Français is customized [signature: 804154061] exporting
|
|
|
|
Do not use these keys in hash calculation which were added / modified during various schema updates
|
|
Global.Label.UseInventory.Description
|
|
Locale.Label.LocaleFile
|
|
UnitNameDisplayFormats.Label.ModelSerial
|
|
UnitNameDisplayFormats.Label.SerialModel
|
|
UnitNameDisplayFormats.Label.SerialModelVendor
|
|
UnitNameDisplayFormats.Label.VendorModelSerial
|
|
O.PartBin
|
|
*/
|
|
List<int> StockLocaleHashes = new List<int>();
|
|
StockLocaleHashes.Add(-605335883);
|
|
StockLocaleHashes.Add(-2007218741);
|
|
StockLocaleHashes.Add(-1164206951);
|
|
StockLocaleHashes.Add(-720441286);
|
|
|
|
//calculate hash
|
|
List<string> allStrings = new List<string>();
|
|
foreach (var entry in lt.LT)
|
|
{
|
|
if (entry.Key == "Global.Label.UseInventory.Description") continue;
|
|
if (entry.Key == "Locale.Label.LocaleFile") continue;
|
|
if (entry.Key == "UnitNameDisplayFormats.Label.ModelSerial") continue;
|
|
if (entry.Key == "UnitNameDisplayFormats.Label.SerialModel") continue;
|
|
if (entry.Key == "UnitNameDisplayFormats.Label.SerialModelVendor") continue;
|
|
if (entry.Key == "UnitNameDisplayFormats.Label.VendorModelSerial") continue;
|
|
if (entry.Key == "O.PartBin") continue;
|
|
if (entry.Key == "UI.Label.CurrentUserName") continue;
|
|
|
|
allStrings.Add(entry.Value);
|
|
}
|
|
int CurrentLocaleHash = GetOrderIndependentHashCode<string>(allStrings);
|
|
|
|
if (StockLocaleHashes.Contains(CurrentLocaleHash)) return false;
|
|
progress.Append("Locale " + localeName + " is customized [signature: " + CurrentLocaleHash.ToString() + "] exporting");
|
|
|
|
return true;
|
|
}
|
|
|
|
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
|
|
{
|
|
int hash = 0;
|
|
foreach (T element in source)
|
|
{
|
|
hash = unchecked(hash +
|
|
EqualityComparer<T>.Default.GetHashCode(element));
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
|
|
public class NameIdItem
|
|
{
|
|
public long Id { get; set; }
|
|
public string Name { get; set; }
|
|
}
|
|
|
|
|
|
|
|
public static string v7StrokePathTov8SVG(string spath, int width, int height)
|
|
{
|
|
|
|
List<Tuple<int, int>> lPoints = new List<Tuple<int, int>>();
|
|
|
|
string[] sLines = spath.Split('X');
|
|
|
|
foreach (string sLine in sLines)
|
|
{
|
|
string[] sPoints = sLine.Trim().Split(' ');
|
|
|
|
if (sPoints.Length > 0)
|
|
{
|
|
foreach (string s in sPoints)
|
|
{
|
|
lPoints.Add(StringToPoint(s));
|
|
}
|
|
}
|
|
}
|
|
|
|
//"71.5,77.19999694824219 71.5,79.19999694824219 71.5,83.19999694824219 71.5,91.19999694824219 74.5,101.19999694824219 75.5,103.19999694824219 80.5,104.19999694824219 86.5,104.19999694824219 96.5,101.19999694824219 116.5,91.19999694824219 145.5,75.19999694824219 167.5,61.19999694824219 183.5,50.19999694824219 194.5,42.19999694824219 197.5,42.19999694824219 199.5,51.19999694824219 202.5,66.19999694824219 210.5,87.19999694824219 219.5,94.19999694824219 235.5,97.19999694824219 260.5,89.19999694824219 298.5,74.19999694824219 326.5,62.19999694824219 339.5,56.19999694824219 343.5,58.19999694824219 345.5,62.19999694824219 349.5,74.19999694824219 357.5,87.19999694824219 368.5,97.19999694824219 387.5,99.19999694824219 423.5,89.19999694824219 465.5,71.19999694824219 498.5,55.19999694824219 511.5,48.19999694824219 514.5,52.19999694824219 514.5,57.19999694824219 515.5,65.19999694824219 515.5,67.19999694824219 518.5,67.19999694824219 521.5,67.19999694824219 521.5,67.19999694824219"
|
|
//handmade equivalent
|
|
// <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 300 100" width="300" height="100"><path d="M 72,77 C 72,77 72,79 72,83 72,91 74,101 76,103 80,104 86,104 96,101 116,91 146,75 168,61 184,50 194,42 198,42 200,51 202,66 210,87 220,94 236,97 260,89 298,74 326,62 340,56 344,58 346,62 350,74 358,87 368,97 388,99 424,89 466,71 498,55 512,48 514,52 514,57 516,65 516,67 518,67 522,67 522,67 " stroke-width="5.525" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path></svg>
|
|
|
|
//Now turn lPoints into svg and return
|
|
// <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 552 120" width="552" height="120">
|
|
//<path d="M 79.400,15.300 C 82.236,17.326 82.400,16.800 85.400,18.300" stroke-width="5.525" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path>
|
|
//<path d="M 85.400,18.300 C 88.884,19.030 88.736,19.326 92.400,19.300" stroke-wke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path><path d="M 510.400,91.300 C 513.725,93.769 513.448,93.840 517.400,95.300" stroke-width="4.888" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path><path d="M 517.400,95.300 C 519.870,96.105 519.725,96.269 522.400,96.300" stroke-width="5.121" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path></svg>
|
|
//https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
|
|
|
|
|
|
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
|
//open
|
|
// sb.Append("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 300 100\" width=\"300\" height=\"100\">");
|
|
sb.Append("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"");
|
|
sb.Append(width.ToString());
|
|
sb.Append("\" height=\"");
|
|
sb.Append(height.ToString());
|
|
sb.Append("\">");
|
|
//draw
|
|
sb.Append("<path d=\"M ");
|
|
sb.Append(lPoints[0].Item1.ToString());
|
|
sb.Append(",");
|
|
sb.Append(lPoints[0].Item2.ToString());
|
|
sb.Append(" C ");
|
|
foreach (var p in lPoints)
|
|
{
|
|
sb.Append(p.Item1.ToString());
|
|
sb.Append(",");
|
|
sb.Append(p.Item2.ToString());
|
|
sb.Append(" ");
|
|
}
|
|
sb.Append("\" stroke-width=\"3\" stroke=\"rgb(0, 0, 255)\" fill=\"none\" stroke-linecap=\"round\"></path>");
|
|
|
|
//close
|
|
sb.Append("</svg>");
|
|
return "data:image/svg+xml;base64," + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sb.ToString()));
|
|
|
|
}
|
|
|
|
|
|
private static Tuple<int, int> StringToPoint(string s)
|
|
{
|
|
|
|
if (string.IsNullOrWhiteSpace(s))
|
|
return new Tuple<int, int>(0, 0);
|
|
|
|
if (!s.Contains(','))
|
|
return new Tuple<int, int>(0, 0);
|
|
|
|
string[] spoint = s.Split(',');
|
|
|
|
//case 1939 - can't treat the input in the parse as integers as they can apparently contain decimals now
|
|
//I suspect that the conversion with decimal is likely slower than checking to see if it has a decimal first so
|
|
//I'm breaking this out into two separate streams for performance
|
|
if (s.Contains('.'))
|
|
{
|
|
// if it contains a decimal then convert differently
|
|
return new Tuple<int, int>(
|
|
Convert.ToInt32(Math.Round(Convert.ToDouble(spoint[0]))),
|
|
Convert.ToInt32(Math.Round(Convert.ToDouble(spoint[1])))
|
|
);
|
|
}
|
|
else
|
|
{
|
|
//if it doesn't contain a decimal then use the original method
|
|
//case 1978 wrap this in a try catch block due to one customer having NaN values
|
|
//in their signature stroke path string.
|
|
try
|
|
{
|
|
return new Tuple<int, int>(int.Parse(spoint[0]), int.Parse(spoint[1]));
|
|
}
|
|
catch (System.FormatException)
|
|
{
|
|
return new Tuple<int, int>(0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public static DateTime GetDateFromSpanAndUnit(DateTime StartDate, GZTW.AyaNova.BLL.AyaUnitsOfTime unit, int multiple)
|
|
{
|
|
switch (unit)
|
|
{
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Seconds:
|
|
return StartDate.AddSeconds(multiple);
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Minutes:
|
|
return StartDate.AddMinutes(multiple);
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Hours:
|
|
return StartDate.AddHours(multiple);
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Days:
|
|
return StartDate.AddDays(multiple);
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Weeks:
|
|
throw new System.NotSupportedException("GetDateFromSpanAndUnit: Weeks not supported");
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Months:
|
|
return StartDate.AddMonths(multiple);
|
|
|
|
case GZTW.AyaNova.BLL.AyaUnitsOfTime.Years:
|
|
return StartDate.AddYears(multiple);
|
|
|
|
|
|
}
|
|
|
|
//fail safe:
|
|
return StartDate;
|
|
}
|
|
|
|
#endregion
|
|
|
|
}//eoc
|
|
|
|
|
|
}//ens
|