Commit b094feee by Elias Sarraf

reworking project - the webAppTemplate project will be based on this

parent 11ad67f8
emT3VCLDemo/__history/
emT3VCLDemo/Win64x/Debug/
emT3VCLDemo/Win64x/
emT3Web/__history/
emT3Web/config/__history/
emT3Web/__recovery/
emT3Web/config/
emT3Web/Win32/
emT3XDataServer/__history/
emT3XDataServer/bin/logs/
emT3XDataServer/bin/static/
emT3XDataServer/Source/__history/
emT3XDataServer/Win32/
......@@ -14,17 +17,4 @@ emT3XDataServer/Win32/
*.skincfg
*.tvsconfig
*.txt
emT3Web/Win32/Debug/
emT3Web/__recovery/
emT3WebApp/__history/
*.zip
emT3WebApp/css/__history/
emT3XDataServer/bin/static/
emT3WebApp/Win32/Debug/
......@@ -105,7 +105,7 @@ object fMain: TfMain
'MySQL.HttpTrustServerCertificate=False'
'MySQL.ProxyPort=0')
Username = 'etask'
Server = '192.168.12.51'
Server = '192.168.116.131'
LoginPrompt = False
Left = 390
Top = 342
......
......@@ -12,6 +12,8 @@ const
type
TOnLoginSuccess = reference to procedure;
TOnLoginError = reference to procedure(AMsg: string);
TOnProfileSuccess = reference to procedure;
TOnProfileError = reference to procedure(AMsg: string);
TAuthService = class
private
......@@ -21,9 +23,8 @@ type
public
constructor Create; reintroduce;
destructor Destroy; override;
procedure Login(const userId, taskId, urlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure Logout;
function GetToken: string;
function Authenticated: Boolean;
......@@ -41,7 +42,7 @@ type
class function DecodePayload(AToken: string): string;
end;
function AuthService: TAuthService;
function AuthService: TAuthService;
implementation
......@@ -54,7 +55,9 @@ var
function AuthService: TAuthService;
begin
if not Assigned(_AuthService) then
begin
_AuthService := TAuthService.Create;
end;
Result := _AuthService;
end;
......@@ -88,7 +91,8 @@ begin
Result := window.localStorage.getItem(TOKEN_NAME);
end;
procedure TAuthService.Login(const userId, taskId, urlCode: string; ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse);
var
......@@ -105,14 +109,14 @@ procedure TAuthService.Login(const userId, taskId, urlCode: string; ASuccess: TO
end;
begin
if (userId = '') or (taskId = '') or (urlCode = '') then
if (AUser = '') or (APassword = '') then
begin
AError('Missing URL parameters. Please reopen from emt3.');
AError('Please enter a username and a password');
Exit;
end;
FClient.RawInvoke(
'IAuthService.Login', [userId, taskId, urlCode],
'IAuthService.Login', [AUser, APassword, AClientVersion],
@OnLoad, @OnError
);
end;
......@@ -136,9 +140,17 @@ begin
ExpirationDate := TJwtHelper.TokenExpirationDate(GetToken);
Result :=
EncodeDate(ExpirationDate.FullYear, ExpirationDate.Month + 1, ExpirationDate.Date) +
EncodeTime(ExpirationDate.Hours, ExpirationDate.Minutes, ExpirationDate.Seconds, 0);
Result := EncodeDate(
ExpirationDate.FullYear,
ExpirationDate.Month + 1,
ExpirationDate.Date
) +
EncodeTime(
ExpirationDate.Hours,
ExpirationDate.Minutes,
ExpirationDate.Seconds,
0
);
end;
function TAuthService.TokenExpired: Boolean;
......@@ -164,7 +176,8 @@ begin
Result := '';
asm
const parts = AToken.split('.');
if (parts.length === 3) {
if (parts.length === 3) { // <- strict compare
// JWTs use url-safe base64; convert before atob
Result = atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'));
}
end;
......
......@@ -2,6 +2,7 @@ object DMConnection: TDMConnection
Height = 264
Width = 395
object ApiConnection: TXDataWebConnection
URL = 'http://localhost:2001/emsys/emt3/api'
OnError = ApiConnectionError
OnRequest = ApiConnectionRequest
OnResponse = ApiConnectionResponse
......@@ -9,6 +10,7 @@ object DMConnection: TDMConnection
Top = 80
end
object AuthConnection: TXDataWebConnection
URL = 'http://localhost:2001/emsys/emt3/auth'
OnError = AuthConnectionError
Left = 48
Top = 16
......
......@@ -4,7 +4,8 @@ interface
uses
System.SysUtils, System.Classes, WEBLib.Modules, XData.Web.Connection,
App.Types, App.Config, XData.Web.Client;
App.Types, App.Config, XData.Web.Client, WEBLib.Dialogs, Vcl.Menus,
WEBLib.Menus;
type
TDMConnection = class(TWebDataModule)
......@@ -17,10 +18,11 @@ type
procedure AuthConnectionError(Error: TXDataWebConnectionError);
private
FUnauthorizedAccessProc: TUnauthorizedAccessProc;
public
const clientVersion = '0.0.1';
procedure InitApp(SuccessProc: TSuccessProc; UnauthorizedAccessProc: TUnauthorizedAccessProc);
public
const clientVersion = '0.8.3';
procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure SetClientConfig(Callback: TVersionCheckCallback);
end;
......@@ -33,35 +35,59 @@ uses
JS, Web,
XData.Web.Request,
XData.Web.Response,
Auth.Service,
Utils;
Auth.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
procedure TDMConnection.ApiConnectionError(Error: TXDataWebConnectionError);
var
errorMsg: string;
begin
ShowErrorModal(Error.ToString);
errorMsg := Error.ErrorMessage;
if errorMsg = '' then
errorMsg := 'Connection error';
if Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(errorMsg)
else
ShowMessage(errorMsg);
end;
procedure TDMConnection.ApiConnectionRequest(Args: TXDataWebConnectionRequest);
begin
if AuthService.Authenticated then
Args.Request.Headers.SetValue('Authorization', 'Bearer ' + AuthService.GetToken);
end;
procedure TDMConnection.ApiConnectionResponse(Args: TXDataWebConnectionResponse);
procedure TDMConnection.ApiConnectionResponse(
Args: TXDataWebConnectionResponse);
begin
if (Args.Response.StatusCode = 401) and Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(Format('%d: %s', [Args.Response.StatusCode, Args.Response.ContentAsText]));
if Args.Response.StatusCode = 401 then
FUnauthorizedAccessProc(Format('%d: %s',[Args.Response.StatusCode, Args.Response.ContentAsText]));
end;
procedure TDMConnection.AuthConnectionError(Error: TXDataWebConnectionError);
var
errorMsg: string;
begin
ShowErrorModal(Error.ToString);
errorMsg := Error.ErrorMessage;
if errorMsg = '' then
errorMsg := 'Connection error';
if errorMsg = 'Error connecting to XData server' then
ShowMessage( 'Error connecting to emT3XDataServer' + sLineBreak + 'Please contact EM Systems support' )
else if Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(errorMsg)
else
ShowMessage(errorMsg);
end;
procedure TDMConnection.InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc);
......@@ -81,12 +107,12 @@ begin
LoadConfig(@ConfigLoaded);
end;
procedure TDMConnection.SetClientConfig(Callback: TVersionCheckCallback);
begin
XDataWebClient1.Connection := AuthConnection;
XDataWebClient1.RawInvoke(
'IAuthService.VerifyVersion', [clientVersion],
XDataWebClient1.RawInvoke('IAuthService.VerifyVersion', [clientVersion],
procedure(Response: TXDataClientResponse)
var
jsonResult: TJSObject;
......@@ -103,12 +129,9 @@ begin
Callback(False, error)
else
Callback(True, '');
end,
procedure(Error: TXDataClientError)
begin
Callback(False, Error.ErrorMessage);
end
);
end);
end;
end.
......@@ -10,13 +10,9 @@ procedure HideStatusMessage(const AElementId: string);
procedure ShowSpinner(SpinnerID: string);
procedure HideSpinner(SpinnerID: string);
procedure ShowErrorModal(msg: string);
function CalculateAge(DateOfBirth: TDateTime): Integer;
function FormatPhoneNumber(PhoneNumber: string): string;
procedure ApplyReportTitle(CurrentReportType: string);
procedure ShowToast(const MessageText: string; const ToastType: string = 'success');
procedure ShowConfirmationModal(msg, leftLabel, rightLabel: string; ConfirmProc: TProc<Boolean>);
procedure ShowNotificationModal(msg: string);
// function FormatDollarValue(ValueStr: string): string;
implementation
......@@ -153,28 +149,6 @@ begin
end;
// ShowConfirmationModal displays a two-button modal with custom labels.
// Params:
// - messageText: text shown in the modal body
// - leftButtonText: label for the left button (e.g., "Cancel")
// - rightButtonText: label for the right button (e.g., "Delete")
// - callback: procedure(confirmed: Boolean); confirmed = True if right button clicked
//
// Example:
// ShowConfirmationModal('Delete this?', 'Cancel', 'Delete',
// procedure(confirmed: Boolean)
// begin
// if confirmed then DeleteOrder();
// end);
// function ShowConfirmationModal(msg, leftLabel, rightLabel: string;): Boolean;
// if ShowConfirmationModal then
// doThing()
// else
// doOtherThing();
procedure ShowConfirmationModal(msg, leftLabel, rightLabel: string; ConfirmProc: TProc<Boolean>);
begin
asm
......@@ -211,49 +185,6 @@ begin
end;
function CalculateAge(DateOfBirth: TDateTime): Integer;
var
Today, BirthDate: TJSDate;
Year, Month, Day, BirthYear, BirthMonth, BirthDay: NativeInt;
DOBString: string;
begin
Today := TJSDate.New;
Year := Today.FullYear;
Month := Today.Month + 1;
Day := Today.Date;
// Formats the DateOfBirth as an ISO 8601 date string
DOBString := FormatDateTime('yyyy-mm-dd', DateOfBirth);
BirthDate := TJSDate.New(DOBString);
if BirthDate = nil then
begin
Exit(0); // Exit the function with an age of 0 if the date creation fails
end;
BirthYear := BirthDate.FullYear;
BirthMonth := BirthDate.Month + 1;
BirthDay := BirthDate.Date;
Result := Year - BirthYear;
if (Month < BirthMonth) or ((Month = BirthMonth) and (Day < BirthDay)) then
Dec(Result);
end;
function FormatPhoneNumber(PhoneNumber: string): string;
var
Digits: string;
begin
Digits := PhoneNumber.Replace('(', '').Replace(')', '').Replace('-', '').Replace(' ', '');
case Length(Digits) of
7: Result := Format('%s-%s', [Copy(Digits, 1, 3), Copy(Digits, 4, 4)]);
10: Result := Format('(%s) %s-%s', [Copy(Digits, 1, 3), Copy(Digits, 4, 3), Copy(Digits, 7, 4)]);
else
// If the number does not have 7 or 10 digits, whatever they typed is returned
Result := PhoneNumber;
end;
end;
procedure ShowToast(const MessageText: string; const ToastType: string = 'success');
var
ParsedText, ToastKind, MsgPrefix: string;
......@@ -318,35 +249,5 @@ begin
end;
procedure ApplyReportTitle(CurrentReportType: string);
var
CrimeTitleElement: TJSHTMLElement;
begin
CrimeTitleElement := TJSHTMLElement(document.getElementById('crime_title'));
if Assigned(CrimeTitleElement) then
CrimeTitleElement.innerText := CurrentReportType
else
Console.Log('Element with ID "crime_title" not found.');
end;
// Used html number input type to restrict the input instead of this function
// function FormatDollarValue(ValueStr: string): string;
// var
// i: Integer;
// begin
// Result := ''; // Initialize the result
// // Filter out any characters that are not digits or decimal point
// for i := 1 to Length(ValueStr) do
// begin
// if (Pos(ValueStr[i], '0123456789.') > 0) then
// begin
// Result := Result + ValueStr[i];
// end;
// end;
// end;
end.
object FHome: TFHome
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
object edtCode: TWebEdit
Left = 380
Top = 198
Width = 121
Height = 22
TabStop = False
ElementClassName = 'form-control'
ElementID = 'edt_code'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtTaskId: TWebEdit
Left = 104
Top = 198
Width = 121
Height = 22
TabStop = False
ChildOrder = 1
ElementClassName = 'form-control'
ElementID = 'edt_task_id'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtUserId: TWebEdit
Left = 240
Top = 198
Width = 121
Height = 22
TabStop = False
ChildOrder = 2
ElementClassName = 'form-control'
ElementID = 'edt_user_id'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
end
<div class="container-fluid py-3">
<div class="card shadow-sm">
<div class="card-body">
<h4 class="mb-3">Home Form</h4>
<div class="mb-3">
<label for="edt_task_id" class="form-label">Task Id</label>
<input id="edt_task_id" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="edt_user_id" class="form-label">User Id</label>
<input id="edt_user_id" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="edt_code" class="form-label">Code</label>
<input id="edt_code" type="text" class="form-control">
</div>
</div>
</div>
</div>
unit View.Home;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls;
type
TFHome = class(TWebForm)
edtCode: TWebEdit;
edtTaskId: TWebEdit;
edtUserId: TWebEdit;
private
FTaskId: string;
FUserId: string;
FCode: string;
public
class function CreateForm(AElementID, ATaskId, AUserId, ACode: string): TWebForm;
procedure InitializeForm;
end;
var
FHome: TFHome;
implementation
{$R *.dfm}
class function TFHome.CreateForm(AElementID, ATaskId, AUserId, ACode: string): TWebForm;
procedure AfterCreate(AForm: TObject);
begin
TFHome(AForm).FTaskId := ATaskId;
TFHome(AForm).FUserId := AUserId;
TFHome(AForm).FCode := ACode;
TFHome(AForm).InitializeForm;
end;
begin
Application.CreateForm(TFHome, AElementID, Result, @AfterCreate);
end;
procedure TFHome.InitializeForm;
begin
console.log('TFHome.InitializeForm fired');
console.log('TaskId=' + FTaskId);
console.log('UserId=' + FUserId);
console.log('Code=' + FCode);
edtTaskId.Text := FTaskId;
edtUserId.Text := FUserId;
edtCode.Text := FCode;
end;
end.
This source diff could not be displayed because it is too large. You can view the blob instead.
<nav class="navbar navbar-light bg-light login-navbar">
<div class="container-fluid">
<a class="navbar-brand" href="#">Koehler-Gibson Orders</a>
</div>
</nav>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-auto">
<img id="kgpicture" style="width: 250px; height: 250px;">
</div>
<div class="col-md-6 col-lg-4">
<div class="card login-card">
<div class="card-header">
<h3 id="view.login.title" class="fs-6 card-title">Please Sign In</h3>
</div>
<div class="card-body">
<div role="form">
<div id="view.login.message" class="alert alert-danger">
<button id="view.login.message.button" type="button" class="btn-close" aria-label="Close"></button>
<span id="view.login.message.label"></span>
</div>
<fieldset>
<div class="mb-3">
<input id="view.login.edtusername" class="form-control" type="text" autofocus placeholder="Username">
</div>
<div class="mb-3">
<input id="view.login.edtpassword" class="form-control" type="password" placeholder="Password">
</div>
<div class="mb-3">
<button id="view.login.btnlogin" class="btn btn-primary w-100">Login</button>
</div>
<div class="text-end text-muted small mt-1">
<span id="lbl_client_version"></span>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
</div>
unit View.Login;
interface
uses
System.SysUtils, System.Classes, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
JS, XData.Web.Connection, WEBLib.ExtCtrls,
App.Types, ConnectionModule, XData.Web.Client, Vcl.Imaging.pngimage;
type
TFViewLogin = class(TWebForm)
WebLabel1: TWebLabel;
edtUsername: TWebEdit;
edtPassword: TWebEdit;
btnLogin: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
btnCloseNotification: TWebButton;
XDataWebClient: TXDataWebClient;
WebImageControl1: TWebImageControl;
lblClientVersion: TWebLabel;
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormShow(Sender: TObject);
private
FLoginProc: TSuccessProc;
FMessage: string;
procedure ShowNotification(Notification: string);
procedure HideNotification;
public
class procedure Display(LoginProc: TSuccessProc); overload;
class procedure Display(LoginProc: TSuccessProc; AMsg: string); overload;
end;
var
FViewLogin: TFViewLogin;
implementation
uses
Auth.Service;
{$R *.dfm}
class procedure TFViewLogin.Display(LoginProc: TSuccessProc);
begin
TFViewLogin.Display(LoginProc, '');
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AMsg: string);
procedure FormCreate(AForm: TObject);
begin
TFViewLogin(AForm).FMessage := AMsg;
end;
begin
if Assigned(FViewLogin) then
FViewLogin.Free;
FViewLogin := TFViewLogin.CreateNew(@FormCreate);
FViewLogin.FLoginProc := LoginProc;
end;
procedure TFViewLogin.HideNotification;
begin
pnlMessage.ElementHandle.classList.add('d-none');
pnlMessage.Visible := False;
end;
procedure TFViewLogin.ShowNotification(Notification: string);
begin
if Notification <> '' then
begin
lblMessage.Caption := Notification;
pnlMessage.ElementHandle.classList.remove('d-none');
pnlMessage.Visible := True;
end;
end;
procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
procedure TFViewLogin.WebFormShow(Sender: TObject);
begin
console.log(DMConnection.clientVersion);
FViewLogin.lblClientVersion.Caption := 'v' + DMConnection.clientVersion;
if FMessage <> '' then
ShowNotification(FMessage)
else
HideNotification;
end;
end.
......@@ -10,50 +10,80 @@ object FViewMain: TFViewMain
Font.Style = []
ParentFont = False
OnCreate = WebFormCreate
object wllblLogout: TWebLinkLabel
Left = 501
Top = 33
object lblUsername: TWebLabel
Left = 536
Top = 4
Width = 49
Height = 14
Caption = 'Username'
ElementID = 'lbl_username'
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object lblUserProfile: TWebLinkLabel
Left = 529
Top = 21
Width = 59
Height = 14
ElementID = 'lbl_user_profile'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
Caption = ' User Profile'
end
object lblLogout: TWebLinkLabel
Left = 547
Top = 55
Width = 36
Height = 14
ElementID = 'dropdown.menu.logout'
ElementID = 'lbl_logout'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = wllblLogoutClick
OnClick = lblLogoutClick
Caption = ' Logout'
end
object lblVersion: TWebLabel
Left = 396
Top = 33
Width = 47
object lblHome: TWebLinkLabel
Left = 556
Top = 38
Width = 27
Height = 14
Caption = 'lblVersion'
ElementID = 'view.main.version'
ElementFont = efCSS
ElementPosition = epRelative
HeightStyle = ssAuto
ElementID = 'lbl_home'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
Caption = 'Home'
end
object lblAppTitle: TWebLabel
Left = 57
Top = 33
Width = 48
Height = 14
Caption = 'emT3web'
ElementID = 'view.main.apptitle'
Caption = 'emT3Web'
ElementID = 'lbl_app_title'
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object lblVersion: TWebLabel
Left = 536
Top = 71
Width = 47
Height = 14
Caption = 'lblVersion'
ElementID = 'lbl_version'
ElementFont = efCSS
ElementPosition = epRelative
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel
Left = 77
Top = 112
Width = 1322
Height = 0
ElementID = 'main.webpanel'
object pnlMain: TWebPanel
Left = 62
Top = 92
Width = 393
Height = 219
ElementID = 'pnl_main'
HeightStyle = ssAuto
WidthStyle = ssAuto
ChildOrder = 3
......@@ -68,67 +98,9 @@ object FViewMain: TFViewMain
Role = 'null'
TabOrder = 0
end
object WebMemo1: TWebMemo
Left = 77
Top = 479
Width = 471
Height = 83
ElementID = 'main.debugmemo'
ElementPosition = epRelative
Enabled = False
HeightPercent = 100.000000000000000000
Lines.Strings = (
'WebMemo1')
Role = 'null'
SelLength = 0
SelStart = 0
ShowFocus = False
Visible = False
WidthPercent = 100.000000000000000000
end
object WebMessageDlg1: TWebMessageDlg
Left = 47
Top = 232
Width = 24
Height = 24
Buttons = []
CustomButtons = <>
DialogText.Strings = (
'Warning'
'Error'
'Information'
'Confirm'
'Custom'
'OK'
'Cancel'
'Yes'
'No'
'Abort'
'Retry'
'Ignore'
'All'
'Yes to all'
'No to all'
'Help'
'Close')
Opacity = 0.200000000000000000
end
object edtTaskIdMain: TWebEdit
Left = 220
Top = 170
Width = 121
Height = 22
ChildOrder = 6
ElementClassName = 'form-control'
ElementID = 'edt_task_id_main'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object XDataWebClient: TXDataWebClient
object xdwcMain: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 44
Top = 280
Left = 76
Top = 332
end
end
<div id="wrapper" class="d-flex flex-column vh-100">
<nav class="navbar navbar-expand navbar-light bg-light" style="margin-bottom: 0px">
<div id="div_wrapper">
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom shadow-sm">
<div class="container-fluid">
<div class="d-flex align-items-center">
<a id="view.main.apptitle" class="navbar-brand" href="index.html">emT3web</a>
<span id="view.main.version" class="small text-muted ms-2"></span>
<div class="d-flex align-items-center gap-2">
<a id="lbl_app_title" class="navbar-brand fw-semibold" href="index.html">emT3Web</a>
<span id="lbl_version" class="badge text-bg-light border text-muted fw-normal"></span>
</div>
<li class="nav-item ms-2 me-2 d-flex align-items-center">
<input id="edt_task_id_main" type="text" class="form-control form-control-sm" placeholder="Task Id">
</li>
<div class="collapse navbar-collapse show" id="navbarNavDropdown">
<div class="collapse navbar-collapse show" id="pnl_navbar_nav_dropdown">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user fa-fw"></i><span class="panel-title" id="view.main.username">Username</span>
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" id="lnk_navbar_dropdown_menu_link"
role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user fa-fw"></i>
<span id="lbl_username" class="fw-semibold">Username</span>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownMenuLink">
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="lnk_navbar_dropdown_menu_link">
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_home" href="#">
<i class="fa fa-home fa-fw"></i><span>Home</span>
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_user_profile" href="#">
<i class="fa fa-user fa-fw"></i><span>User Profile</span>
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" id="dropdown.menu.logout" href="#">
<i class="fa fa-sign-out fa-fw"></i><span> Logout</span>
<a class="dropdown-item d-flex align-items-center gap-2 text-danger" id="lbl_logout" href="#">
<i class="fa fa-sign-out fa-fw"></i><span>Logout</span>
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Toast wrapper directly under navbar -->
<div id="toast-wrapper" class="position-fixed top-0 start-0 mt-5 ms-4" style="z-index: 1080; min-width: 300px; max-width: 500px">
<div id="bootstrapToast" class="toast align-items-center text-white bg-success border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
<!-- Toast -->
<div id="pnl_toast_wrapper" class="position-fixed top-0 start-0 mt-5 ms-4"
style="z-index: 1080; min-width: 300px; max-width: 500px;">
<div id="toast_bootstrap" class="toast align-items-center text-white bg-success border-0 shadow" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="bootstrapToastBody">Success message</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
<div class="toast-body" id="lbl_bootstrap_toast_body">
Success message
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
</div>
<div class="container-fluid d-flex flex-column flex-grow-1" style="min-height: 0">
<div id="main.webpanel" class="flex-grow-1 d-flex flex-column" style="min-height: 0"></div>
<!-- Main Panel (where all forms display) -->
<div class="container-fluid py-3 d-flex flex-column overflow-hidden" style="height: calc(100vh - 57px);">
<div id="pnl_main" class="flex-grow-1 min-h-0 overflow-hidden"></div>
</div>
</div>
<div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<!-- Spinner Modal -->
<div id="div_spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller">
<div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div>
</div>
</div>
</div>
<div class="modal fade" id="main_errormodal" tabindex="-1" aria-labelledby="main_lblmodal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="main_lblmodal">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="main_lblmodal_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_restart" class="btn btn-primary">
Restart
</button>
<!-- Error Modal -->
<div class="modal fade" id="mdl_error" tabindex="-1" aria-labelledby="lbl_modal_title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_modal_title">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_modal_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_restart" class="btn btn-primary">Back to Orders</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="main_modal_body">Placeholder text</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary me-3" id="btn_confirm_left">
Cancel
</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">
Confirm
</button>
<!-- Confirmation Modal -->
<div class="modal fade" id="mdl_confirmation" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="lbl_confirmation_body">
Placeholder text
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary me-3" id="btn_confirm_left">Cancel</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">Confirm</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="main_notification_modal" tabindex="-1" aria-labelledby="main_lblmodal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="main_notification_modal">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="main_notification_modal_body">Please contact EMSystems to solve the issue.</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_close" class="btn btn-primary">
Close
</button>
<!-- Notification Modal -->
<div class="modal fade" id="mdl_notification" tabindex="-1" aria-labelledby="lbl_notification_title"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_notification_title">Info</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_notification_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_close" class="btn btn-primary">Close</button>
</div>
</div>
</div>
</div>
</div>
......@@ -3,37 +3,29 @@ unit View.Main;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils, View.Home;
System.SysUtils, System.Classes, JS, Web,
WEBLib.Controls, WEBLib.Forms, WEBLib.ExtCtrls, WEBLib.StdCtrls,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Dialogs, Vcl.StdCtrls,
Vcl.Controls, Vcl.Graphics;
type
TFViewMain = class(TWebForm)
wllblLogout: TWebLinkLabel;
WebPanel1: TWebPanel;
WebMessageDlg1: TWebMessageDlg;
WebMemo1: TWebMemo;
XDataWebClient: TXDataWebClient;
pnlMain: TWebPanel;
lblUsername: TWebLabel;
lblUserProfile: TWebLinkLabel;
lblHome: TWebLinkLabel;
lblLogout: TWebLinkLabel;
lblVersion: TWebLabel;
lblAppTitle: TWebLabel;
edtTaskIdMain: TWebEdit;
xdwcMain: TXDataWebClient;
procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject);
procedure wllblLogoutClick(Sender: TObject);
private
{ Private declarations }
FTasksHtmlForm: TWebForm;
procedure lblLogoutClick(Sender: TObject);
private
FChildForm: TWebForm;
FLogoutProc: TLogoutProc;
procedure ConfirmLogout;
procedure LoadTasksHtmlForm;
procedure LoadHomeForm;
procedure ShowForm(aFormClass: TWebFormClass);
public
{ Public declarations }
FUserId: string;
FTaskId: string;
FCode: string;
class procedure Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
class procedure Display(logoutProc: TLogoutProc);
end;
var
......@@ -43,92 +35,43 @@ implementation
uses
Auth.Service,
View.Test,
View.TasksHTML;
{$R *.dfm}
class procedure TFViewMain.Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
begin
if Assigned(FViewMain) then
FViewMain.Free;
FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := LogoutProc;
FViewMain.FUserId := AUserId;
FViewMain.FTaskId := ATaskId;
FViewMain.FCode := ACode;
console.log('Main form values assigned after create');
console.log('UserId=' + FViewMain.FUserId);
console.log('TaskId=' + FViewMain.FTaskId);
console.log('Code=' + FViewMain.FCode);
end;
procedure TFViewMain.WebFormCreate(Sender: TObject);
var
userName: string;
begin
console.log('TFViewMain.WebFormCreate fired');
lblAppTitle.Caption := 'emT3web';
userName := JS.toString(AuthService.TokenPayload.Properties['user_name']);
lblUsername.Caption := userName;
lblVersion.Caption := 'v' + DMConnection.clientVersion;
console.log('Main form values assigned in webformcreate');
console.log('UserId=' + FViewMain.FUserId);
console.log('TaskId=' + FViewMain.FTaskId);
console.log('Code=' + FViewMain.FCode);
LoadHomeForm;
// LoadTasksHtmlForm;
ShowForm(TFTasksHTML);
end;
procedure TFViewMain.mnuLogoutClick(Sender: TObject);
procedure TFViewMain.lblLogoutClick(Sender: TObject);
begin
ConfirmLogout;
if Assigned(FLogoutProc) then
FLogoutProc('');
end;
procedure TFViewMain.wllblLogoutClick(Sender: TObject);
procedure TFViewMain.ShowForm(aFormClass: TWebFormClass);
begin
ConfirmLogout;
if Assigned(FChildForm) then
FChildForm.Free;
Application.CreateForm(aFormClass, pnlMain.ElementID, FChildForm);
end;
procedure TFViewMain.ConfirmLogout;
class procedure TFViewMain.Display(logoutProc: TLogoutProc);
begin
ShowConfirmationModal(
'End this session?.',
'Yes',
'No',
procedure(confirmed: Boolean)
begin
if confirmed and Assigned(FLogoutProc) then
FLogoutProc('');
end
);
end;
procedure TFViewMain.LoadTasksHtmlForm;
begin
if Assigned(FTasksHtmlForm) then
FTasksHtmlForm.Free;
console.log('About to create TFTasksHTML, host=' + WebPanel1.ElementID);
console.log('Main form task id is: ' + FTaskId);
FTasksHtmlForm := TFTasksHTML.CreateForm(WebPanel1.ElementID, FTaskId);
if Assigned(FViewMain) then
FViewMain.Free;
FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := logoutProc;
end;
procedure TFViewMain.LoadHomeForm;
begin
if Assigned(FTasksHtmlForm) then
FTasksHtmlForm.Free;
console.log('About to create TFHome, host=' + WebPanel1.ElementID);
console.log('Main form task id is: ' + FTaskId);
console.log('Main form user id is: ' + FUserId);
console.log('Main form code is: ' + FCode);
end.
FTasksHtmlForm := TFHome.CreateForm(WebPanel1.ElementID, FTaskId, FUserId, FCode);
end;
end.
......@@ -12,6 +12,7 @@ object FTasksHTML: TFTasksHTML
Caption = 'Reload'
ElementID = 'btn_reload'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnReloadClick
end
......@@ -24,9 +25,25 @@ object FTasksHTML: TFTasksHTML
ChildOrder = 1
ElementID = 'btn_add_row'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnAddRowClick
end
object btnDeleteRow: TWebButton
Left = 78
Top = 150
Width = 96
Height = 25
Caption = 'Delete Row'
ChildOrder = 2
ElementID = 'btn_delete_row'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnDeleteRowClick
end
object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 506
......@@ -38,6 +55,9 @@ object FTasksHTML: TFTasksHTML
object xdwdsTaskstaskID: TStringField
FieldName = 'taskId'
end
object xdwdsTasksitemNum: TIntegerField
FieldName = 'itemNum'
end
object xdwdsTasksapplication: TStringField
FieldName = 'application'
end
......@@ -68,7 +88,7 @@ object FTasksHTML: TFTasksHTML
object xdwdsTasksnotes: TStringField
FieldName = 'notes'
end
object xdwdsTaskstaskItemId: TStringField
object xdwdsTaskstaskItemId: TIntegerField
FieldName = 'taskItemId'
end
end
......
<div class="container-fluid p-2 d-flex flex-column h-100">
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0">
<h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-outline-success">Add Row</button>
<button id="btn_reload" class="btn btn-sm btn-outline-primary">Reload</button>
<div class="d-flex align-items-center gap-3">
<div id="lbl_total_rows"></div>
<div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-success">Add Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-danger">Delete Row</button>
<button id="btn_reload" class="btn btn-sm btn-primary">Reload</button>
</div>
</div>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNameManager" aria-labelledby="nm_title">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="nm_title">Add Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div id="nm_existing_list" class="list-group mb-3"></div>
<div id="nm_add_wrap" class="d-none mb-3">
<input id="nm_name_input" type="text" class="form-control" maxlength="100">
<div id="nm_name_invalid" class="invalid-feedback d-none"></div>
<div class="d-flex justify-content-end mt-2">
<button id="btn_nm_save" type="button" class="btn btn-success">Save</button>
</div>
</div>
<button id="btn_nm_add_another" type="button" class="btn btn-secondary">
Add another item
</button>
</div>
</div>
</div>
......@@ -5,7 +5,7 @@ interface
uses
System.SysUtils, System.Classes,
JS, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
WEBLib.ExtCtrls,
WEBLib.ExtCtrls, uNameManager,
XData.Web.Client, XData.Web.Dataset,
Utils, Data.DB, XData.Web.JsonDataset, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls;
......@@ -27,32 +27,58 @@ type
xdwdsTasksnotes: TStringField;
btnReload: TWebButton;
btnAddRow: TWebButton;
xdwdsTaskstaskItemId: TStringField;
procedure btnAddRowClick(Sender: TObject);
xdwdsTasksitemNum: TIntegerField;
xdwdsTaskstaskItemId: TIntegerField;
btnDeleteRow: TWebButton;
[async] procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
[async] procedure btnDeleteRowClick(Sender: TObject);
private
FTaskId: string;
FReportedByOptions: TJSArray;
FAssignedToOptions: TJSArray;
FStatusOptions: TJSArray;
FPendingFocusTaskItemId: integer;
FPendingFocusField: string;
FSelectedTaskItemId: Integer;
FSelectedTaskId: Integer;
FPendingScrollTop: Integer;
FPendingScrollLeft: Integer;
FPendingFocusItemNum: Integer;
FPendingFocusTaskField: string;
FNameManager: TNameManager;
[async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable;
procedure BindTableEditors;
procedure EditorInput(Event: TJSEvent);
procedure SelectChange(Event: TJSEvent);
procedure EnableColumnResize;
procedure EnableAutoGrowTextAreas;
procedure GotoRowIndex(AIndex: Integer);
function HtmlEncode(const s: string): string;
procedure SetTaskLabel(const ATaskId: string);
procedure SetTotalRowsLabel(ARowCount: Integer);
procedure SetTaskLabel(const ATitle: string);
[async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent);
procedure InitializeForm;
[async] function AddTaskRow: Boolean;
[async] function DeleteTaskRow: Boolean;
function ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
function GetOptionsForField(const AFieldName: string): TJSArray;
procedure FocusTrigger(const ATriggerId: string);
procedure DropdownItemClick(Event: TJSEvent);
procedure DropdownEditClick(Event: TJSEvent);
function ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
[async] procedure MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
procedure ApplyPendingFocus;
procedure RowClick(Event: TJSEvent);
procedure ApplySelectedRowState;
procedure ApplyPendingDeleteFocus;
procedure EditorKeyDown(Event: TJSEvent);
procedure CaptureTableScroll;
procedure RestoreTableScroll;
public
class function CreateForm(AElementID, ATaskId: string): TWebForm;
end;
var
......@@ -65,30 +91,32 @@ uses
{$R *.dfm}
class function TFTasksHTML.CreateForm(AElementID, ATaskId: string): TWebForm;
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
begin
console.log('TFTasksHTML.CreateForm called, host=' + AElementID + ', taskId=' + ATaskId);
Application.CreateForm(TFTasksHTML, AElementID, Result,
procedure(AForm: TObject)
console.log('TFTasksHTML.WebFormCreate fired');
FTaskId := Application.Parameters.Values['task_id'];
FReportedByOptions := TJSArray.new;
FAssignedToOptions := TJSArray.new;
FStatusOptions := TJSArray.new;
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
FNameManager := TNameManager.Create(
function(const AFieldName: string): TJSArray
begin
console.log('TFTasksHTML.CreateForm callback fired, assigned=' + BoolToStr(Assigned(AForm), True));
if Assigned(AForm) then
begin
TFTasksHTML(AForm).FTaskId := ATaskId;
TFTasksHTML(AForm).InitializeForm;
end
else
Utils.ShowErrorModal('TFTasksHTML form callback returned nil.');
Result := GetOptionsForField(AFieldName);
end,
procedure
begin
RenderTable;
end,
procedure(const ATriggerId: string)
begin
FocusTrigger(ATriggerId);
end
);
end;
procedure TFTasksHTML.InitializeForm;
begin
console.log('TFTasksHTML.InitializeForm fired');
console.log('The task id is: ' + FTaskId);
FNameManager.BindControls;
if FTaskId = '' then
begin
......@@ -97,6 +125,7 @@ begin
end;
btnAddRow.Enabled := False;
btnDeleteRow.Enabled := False;
if not DMConnection.ApiConnection.Connected then
begin
......@@ -122,6 +151,7 @@ begin
Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;
procedure TFTasksHTML.GotoRowIndex(AIndex: Integer);
var
i: Integer;
......@@ -138,6 +168,7 @@ begin
end;
end;
procedure TFTasksHTML.EditorInput(Event: TJSEvent);
var
el: TJSHTMLElement;
......@@ -165,14 +196,16 @@ begin
console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal);
xdwdsTasks.Edit;
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
if SameText(fieldName, 'itemNum') then
xdwdsTasks.FieldByName(fieldName).AsInteger := StrToIntDef(newVal, 0)
else
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
xdwdsTasks.Post;
el.setAttribute('data-unsaved-data', '1');
end;
procedure TFTasksHTML.SelectChange(Event: TJSEvent);
var
el: TJSHTMLElement;
......@@ -223,6 +256,7 @@ begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
el.addEventListener('keydown', TJSEventHandler(@EditorKeyDown));
end;
nodes := document.querySelectorAll('.task-select');
......@@ -232,11 +266,208 @@ begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('change', TJSEventHandler(@SelectChange));
end;
nodes := document.querySelectorAll('.task-dd-item');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
end;
nodes := document.querySelectorAll('.task-dd-edit-btn');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownEditClick));
end;
nodes := document.querySelectorAll('.task-row-selectable');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@RowClick));
end;
ApplySelectedRowState;
end;
procedure TFTasksHTML.EditorKeyDown(Event: TJSEvent);
var
el: TJSHTMLElement;
fieldName: string;
newItemNum: Integer;
idx: Integer;
begin
el := TJSHTMLElement(Event.target);
fieldName := string(el.getAttribute('data-field'));
if not SameText(fieldName, 'itemNum') then
Exit;
if TJSKeyboardEvent(Event).key <> 'Enter' then
Exit;
Event.preventDefault;
idx := StrToIntDef(string(el.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
el.removeAttribute('data-unsaved-data');
MoveTaskRow(idx, newItemNum);
end;
[async] procedure TFTasksHTML.btnAddRowClick(Sender: TObject);
begin
Utils.ShowSpinner('spinner');
try
if await(AddTaskRow) then
begin
CaptureTableScroll;
LoadTasks(FTaskId);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
[async] procedure TFTasksHTML.btnDeleteRowClick(Sender: TObject);
var
deletedItemNum: Integer;
begin
if FSelectedTaskItemId <= 0 then
Exit;
deletedItemNum := 0;
if xdwdsTasks.Active then
begin
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
if xdwdsTaskstaskItemId.AsInteger = FSelectedTaskItemId then
begin
deletedItemNum := xdwdsTasksitemNum.AsInteger;
Break;
end;
xdwdsTasks.Next;
end;
end;
Utils.ShowSpinner('spinner');
try
if await(DeleteTaskRow) then
begin
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
btnDeleteRow.Enabled := False;
FPendingFocusItemNum := deletedItemNum;
FPendingFocusTaskField := 'application';
CaptureTableScroll;
LoadTasks(FTaskId);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTasksHTML.RowClick(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
taskItemIdStr: string;
taskIdStr: string;
begin
rowEl := TJSHTMLElement(Event.currentTarget);
if not Assigned(rowEl) then
Exit;
taskItemIdStr := string(rowEl.getAttribute('data-task-item-id'));
taskIdStr := string(rowEl.getAttribute('data-task-id'));
FSelectedTaskItemId := StrToIntDef(taskItemIdStr, 0);
FSelectedTaskId := StrToIntDef(taskIdStr, 0);
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
ApplySelectedRowState;
end;
procedure TFTasksHTML.btnAddRowClick(Sender: TObject);
procedure TFTasksHTML.ApplySelectedRowState;
begin
Utils.ShowErrorModal('Add row is not enabled yet.');
asm
const selectedTaskItemId = this.FSelectedTaskItemId;
document.querySelectorAll('.task-row-selectable').forEach(function(row){
const rowTaskItemId = parseInt(row.getAttribute('data-task-item-id') || '0', 10);
if ((selectedTaskItemId > 0) && (rowTaskItemId === selectedTaskItemId))
row.classList.add('table-active');
else
row.classList.remove('table-active');
});
end;
end;
[async] function TFTasksHTML.AddTaskRow: Boolean;
var
response: TXDataClientResponse;
insertAfterItemNum: Integer;
newItemNum: Integer;
maxItemNum: Integer;
begin
Result := False;
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing task_id. Please reopen from emt3.');
Exit;
end;
insertAfterItemNum := 0;
maxItemNum := 0;
if xdwdsTasks.Active then
begin
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
if xdwdsTasksitemNum.AsInteger > maxItemNum then
maxItemNum := xdwdsTasksitemNum.AsInteger;
if xdwdsTaskstaskItemId.AsInteger = FSelectedTaskItemId then
insertAfterItemNum := xdwdsTasksitemNum.AsInteger;
xdwdsTasks.Next;
end;
end;
if insertAfterItemNum > 0 then
newItemNum := insertAfterItemNum + 1
else
newItemNum := maxItemNum + 1;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddTaskRow', [FTaskId, insertAfterItemNum]
));
console.log('AddTaskRow response=' + string(TJSJSON.stringify(response.Result)));
FPendingFocusItemNum := newItemNum;
FPendingFocusTaskField := 'application';
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('AddTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
......@@ -251,6 +482,7 @@ begin
LoadTasks(FTaskId);
end;
procedure TFTasksHTML.EnableAutoGrowTextAreas;
begin
asm
......@@ -266,30 +498,37 @@ begin
end;
end;
procedure TFTasksHTML.SetTaskLabel(const ATaskId: string);
procedure TFTasksHTML.SetTotalRowsLabel(ARowCount: Integer);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_project_name'));
el := TJSHTMLElement(document.getElementById('lbl_total_rows'));
if Assigned(el) then
el.innerText := 'Tasks - ' + ATaskId;
el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
end;
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
procedure TFTasksHTML.SetTaskLabel(const ATitle: string);
var
el: TJSHTMLElement;
begin
console.log('TFTasksHTML.WebFormCreate fired');
el := TJSHTMLElement(document.getElementById('lbl_project_name'));
if Assigned(el) then
el.innerText := ATitle;
end;
[async] procedure TFTasksHTML.LoadTasks(const ATaskId: string);
var
response: TXDataClientResponse;
resultObj, taskObj: TJSObject;
tasksArray, itemsArray, flatItems: TJSArray;
taskIndex, itemIndex: Integer;
itemsArray: TJSArray;
titleText: string;
rowCount: Integer;
begin
console.log('IApiService.GetTaskItems called with task_id: ' + ATaskId);
console.log('Load Tasks Fired');
SetTaskLabel(ATaskId);
Utils.ShowSpinner('spinner');
try
try
......@@ -308,22 +547,33 @@ begin
Exit;
resultObj := TJSObject(response.Result);
tasksArray := TJSArray(resultObj['data']);
flatItems := TJSArray.new;
taskObj := TJSObject(resultObj['task']);
for taskIndex := 0 to tasksArray.Length - 1 do
begin
taskObj := TJSObject(tasksArray[taskIndex]);
itemsArray := TJSArray(taskObj['items']);
if Assigned(taskObj) then
titleText := string(taskObj['title'])
else
titleText := 'Task - ' + ATaskId;
for itemIndex := 0 to itemsArray.Length - 1 do
flatItems.push(itemsArray[itemIndex]);
end;
SetTaskLabel(titleText);
rowCount := StrToIntDef(string(resultObj['count']), 0);
SetTotalRowsLabel(rowCount);
FReportedByOptions := ExtractOptionNames(TJSArray(resultObj['reportedByOptions']));
FAssignedToOptions := ExtractOptionNames(TJSArray(resultObj['assignedToOptions']));
FStatusOptions := ExtractCodeDescs(TJSArray(resultObj['statusOptions']));
itemsArray := TJSArray(resultObj['items']);
if not Assigned(itemsArray) then
itemsArray := TJSArray.new;
xdwdsTasks.Close;
xdwdsTasks.SetJsonData(flatItems);
xdwdsTasks.SetJsonData(itemsArray);
xdwdsTasks.Open;
btnAddRow.Enabled := True;
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
RenderTable;
finally
Utils.HideSpinner('spinner');
......@@ -342,6 +592,11 @@ var
Result := '<th scope="col">' + s + '</th>';
end;
function ThBlank: string;
begin
Result := '<th scope="col">&nbsp;</th>';
end;
function TdNowrap(const s: string): string;
begin
Result := '<td class="align-top nowrap-cell">' + s + '</td>';
......@@ -370,8 +625,18 @@ var
begin
Result :=
'<textarea class="form-control form-control-sm cell-textarea task-editor w-100" ' +
'style="height:31px; min-height:31px; overflow:hidden; resize:none;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'rows="2">' + HtmlEncode(Value) + '</textarea>';
'rows="1">' + HtmlEncode(Value) + '</textarea>';
end;
function ItemNumInput(const Value: Integer; const AIdx: Integer): string;
begin
Result :=
'<input type="number" min="1" class="form-control form-control-sm task-editor text-center px-1" ' +
'style="width: 30px;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="itemNum" ' +
'value="' + IntToStr(Value) + '">';
end;
function DateInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
......@@ -388,48 +653,76 @@ var
'value="' + HtmlEncode(Value) + '"' + w + '>';
end;
function SelectList(const FieldName, Current: string; const AIdx: Integer; const Items: array of string): string;
var
i: Integer;
sel: string;
begin
Result :=
'<select class="form-select form-select-sm task-select" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '">';
sel := '';
if Trim(Current) = '' then
sel := ' selected';
Result := Result + '<option value=""' + sel + '></option>';
for i := Low(Items) to High(Items) do
function SelectList(const FieldName, Current: string; const AIdx: Integer; const Items: TJSArray): string;
var
i: Integer;
itemText: string;
triggerId: string;
begin
sel := '';
if SameText(Current, Items[i]) then
sel := ' selected';
Result := Result + '<option value="' + HtmlEncode(Items[i]) + '"' + sel + '>' + HtmlEncode(Items[i]) + '</option>';
triggerId := 'task_dd_' + FieldName + '_' + IntToStr(AIdx);
Result :=
'<div class="dropdown w-100">' +
'<button id="' + triggerId + '" class="btn btn-sm btn-light border w-100 d-flex justify-content-between align-items-center text-start task-dd-toggle" ' +
'type="button" data-bs-toggle="dropdown" aria-expanded="false">' +
'<span class="task-dd-label text-truncate">' + HtmlEncode(Current) + '</span>' +
'<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>' +
'</button>' +
'<div class="dropdown-menu w-100 p-0 overflow-hidden">';
Result := Result +
'<button type="button" class="dropdown-item task-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value=""></button>';
if Assigned(Items) then
for i := 0 to Items.length - 1 do
begin
itemText := string(Items[i]);
Result := Result +
'<button type="button" class="dropdown-item task-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="' + HtmlEncode(itemText) + '" ' +
'data-trigger-id="' + triggerId + '">' + HtmlEncode(itemText) + '</button>';
end;
Result := Result +
'<div class="dropdown-divider my-1"></div>' +
'<div class="px-2 py-1 text-end">' +
'<button type="button" class="btn btn-link btn-sm p-0 text-body task-dd-edit-btn" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-trigger-id="' + triggerId + '">' +
'<i class="fas fa-pencil-alt"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
end;
Result := Result + '</select>';
end;
function StatusSelect(const Current: string; const AIdx: Integer): string;
const
Statuses: array[0..5] of string = ('Open', 'In Progress', 'Blocked', 'Testing', 'Done', 'Closed');
var
i: Integer;
statusText: string;
sel: string;
begin
Result :=
'<select class="form-select form-select-sm task-select" data-idx="' + IntToStr(AIdx) + '" data-field="status">';
for i := Low(Statuses) to High(Statuses) do
begin
sel := '';
if SameText(Current, Statuses[i]) then
sel := ' selected';
Result := Result + '<option value="' + HtmlEncode(Statuses[i]) + '"' + sel + '>' + HtmlEncode(Statuses[i]) + '</option>';
end;
Result := Result + '<option value=""></option>'; //Note: This adds the blank option on top
if Assigned(FStatusOptions) then
for i := 0 to FStatusOptions.length - 1 do
begin
statusText := string(FStatusOptions[i]);
sel := '';
if SameText(Current, statusText) then
sel := ' selected';
Result := Result + '<option value="' + HtmlEncode(statusText) + '"' + sel + '>' + HtmlEncode(statusText) + '</option>';
end;
Result := Result + '</select>';
end;
......@@ -442,22 +735,22 @@ begin
html :=
'<div class="tasks-vscroll">' +
'<div class="tasks-hscroll">' +
'<table class="table table-sm table-bordered table-hover align-middle mb-0">' +
'<table class="table table-sm table-bordered align-middle mb-0" style="min-width: 2000px;">' +
'<colgroup>' +
'<col style="width:110px">' +
'<col style="width:240px">' +
'<col style="width:90px">' +
'<col style="width:120px">' +
'<col style="width:120px">' +
'<col style="width:120px">' +
'<col style="width:140px">' +
'<col style="width:140px">' +
'<col style="width:160px">' +
'<col style="width:520px">' +
'<col style="width:520px">' +
'<col style="width:40px">' + // Item Num
'<col style="width:200px">' + // App
'<col style="width:90px">' + // Version
'<col style="width:120px">' + // Date
'<col style="width:120px">' + // Reported
'<col style="width:120px">' + // Assigned
'<col style="width:195px">' + // Status
'<col style="width:140px">' + // Status Date
'<col style="width:160px">' + // Form
'<col style="width:520px">' + // Issue
'<col style="width:520px">' + // Notes
'</colgroup>' +
'<thead><tr>' +
Th('Task') +
ThBlank +
Th('App') +
Th('Version') +
Th('Date') +
......@@ -475,13 +768,13 @@ begin
while not xdwdsTasks.Eof do
begin
html := html +
'<tr>' +
TdNowrap(TextInput('taskId', xdwdsTaskstaskId.AsString, rowIdx, 90)) +
'<tr class="task-row-selectable" data-task-item-id="' + IntToStr(xdwdsTaskstaskItemId.AsInteger) + '" data-task-id="' + xdwdsTaskstaskId.AsString + '" data-item-num="' + IntToStr(xdwdsTasksitemNum.AsInteger) + '">' +
TdNowrap(ItemNumInput(xdwdsTasksitemNum.AsInteger, rowIdx)) +
TdNowrap(TextInput('application', xdwdsTasksapplication.AsString, rowIdx, 180)) +
TdNowrap(TextInput('version', xdwdsTasksversion.AsString, rowIdx, 80)) +
TdNowrap(DateInput('taskDate', xdwdsTaskstaskDate.AsString, rowIdx, 110)) +
TdNowrap(SelectList('reportedBy', xdwdsTasksreportedBy.AsString, rowIdx, ['Elias','Mac','Mark'])) +
TdNowrap(SelectList('assignedTo', xdwdsTasksassignedTo.AsString, rowIdx, ['Elias','Mac','Mark'])) +
TdNowrap(SelectList('reportedBy', xdwdsTasksreportedBy.AsString, rowIdx, FReportedByOptions)) +
TdNowrap(SelectList('assignedTo', xdwdsTasksassignedTo.AsString, rowIdx, FAssignedToOptions)) +
TdNowrap(StatusSelect(xdwdsTasksstatus.AsString, rowIdx)) +
TdNowrap(DateInput('statusDate', xdwdsTasksstatusDate.AsString, rowIdx, 110)) +
TdNowrap(TextInput('formSection', xdwdsTasksformSection.AsString, rowIdx, 160)) +
......@@ -499,8 +792,12 @@ begin
BindTableEditors;
EnableAutoGrowTextAreas;
EnableColumnResize;
RestoreTableScroll;
ApplyPendingFocus;
ApplyPendingDeleteFocus;
end;
procedure TFTasksHTML.EnableColumnResize;
begin
asm
......@@ -545,6 +842,7 @@ var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName: string;
newItemNum: Integer;
begin
el := TJSHTMLElement(Event.target);
......@@ -563,12 +861,60 @@ begin
if idx < 0 then
Exit;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
if SameText(fieldName, 'itemNum') then
begin
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
console.log('EditorBlur: MOVE idx=' + IntToStr(idx) + ' newItemNum=' + IntToStr(newItemNum));
MoveTaskRow(idx, newItemNum);
Exit;
end;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
SaveRow(idx);
end;
[async] procedure TFTasksHTML.MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
var
response: TXDataClientResponse;
movedTaskItemId: Integer;
begin
if not xdwdsTasks.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTasks.Eof then
Exit;
movedTaskItemId := xdwdsTaskstaskItemId.AsInteger;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow',
[
StrToIntDef(xdwdsTaskstaskId.AsString, 0),
movedTaskItemId,
newItemNum
]
));
console.log('MoveTaskRow: response=' + string(TJSJSON.stringify(response.Result)));
FPendingFocusTaskItemId := movedTaskItemId;
FPendingFocusField := 'application';
CaptureTableScroll;
LoadTasks(FTaskId);
except
on E: EXDataClientRequestException do
begin
console.log('MoveTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
LoadTasks(FTaskId);
end;
end;
end;
[async] procedure TFTasksHTML.SaveRow(AIndex: Integer);
const
// Note: Use this to manipulate saving to the server or not for testing
......@@ -587,7 +933,7 @@ begin
payload := TJSObject.new;
payload['taskItemId'] := xdwdsTaskstaskItemId.AsString;
payload['taskItemId'] := xdwdsTaskstaskItemId.AsInteger;
payload['taskId'] := xdwdsTaskstaskId.AsString;
payload['application'] := xdwdsTasksapplication.AsString;
......@@ -620,6 +966,251 @@ begin
end;
[async] function TFTasksHTML.DeleteTaskRow: Boolean;
var
response: TXDataClientResponse;
begin
Result := False;
if (FSelectedTaskId <= 0) or (FSelectedTaskItemId <= 0) then
Exit;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteTaskRow',
[FSelectedTaskId, FSelectedTaskItemId]
));
console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result)));
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('DeleteTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
function TFTasksHTML.ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
var
i: Integer;
optionObj: TJSObject;
begin
Result := TJSArray.new;
if not Assigned(SourceArray) then
Exit;
for i := 0 to SourceArray.length - 1 do
begin
optionObj := TJSObject(SourceArray[i]);
if Assigned(optionObj) then
Result.push(string(optionObj['name']));
end;
end;
function TFTasksHTML.ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
var
i: Integer;
optionObj: TJSObject;
begin
Result := TJSArray.new;
if not Assigned(SourceArray) then
Exit;
for i := 0 to SourceArray.length - 1 do
begin
optionObj := TJSObject(SourceArray[i]);
if Assigned(optionObj) then
Result.push(string(optionObj['codeDesc']));
end;
end;
function TFTasksHTML.GetOptionsForField(const AFieldName: string): TJSArray;
begin
if SameText(AFieldName, 'reportedBy') then
Result := FReportedByOptions
else if SameText(AFieldName, 'assignedTo') then
Result := FAssignedToOptions
else
Result := nil;
end;
procedure TFTasksHTML.FocusTrigger(const ATriggerId: string);
var
el: TJSHTMLElement;
begin
if ATriggerId = '' then
Exit;
el := TJSHTMLElement(document.getElementById(ATriggerId));
if Assigned(el) then
el.focus;
end;
procedure TFTasksHTML.DropdownItemClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName, newVal, triggerId: string;
begin
if not xdwdsTasks.Active then
Exit;
Event.preventDefault;
el := TJSHTMLElement(Event.currentTarget);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
newVal := string(el.getAttribute('data-value'));
triggerId := string(el.getAttribute('data-trigger-id'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
GotoRowIndex(idx);
if xdwdsTasks.Eof then
Exit;
xdwdsTasks.Edit;
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
xdwdsTasks.Post;
if triggerId <> '' then
begin
asm
var btn = document.getElementById(triggerId);
if (btn) {
var labelEl = btn.querySelector('.task-dd-label');
if (labelEl) {
labelEl.textContent = newVal;
}
}
end;
end;
SaveRow(idx);
end;
procedure TFTasksHTML.DropdownEditClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName, triggerId: string;
begin
Event.preventDefault;
Event.stopPropagation;
el := TJSHTMLElement(Event.currentTarget);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
triggerId := string(el.getAttribute('data-trigger-id'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
FNameManager.OpenManager(fieldName, idx, triggerId);
end;
procedure TFTasksHTML.ApplyPendingFocus;
var
el: TJSHTMLElement;
selector: string;
begin
if (FPendingFocusTaskItemId <= 0) or (FPendingFocusField = '') then
Exit;
selector :=
'[data-task-item-id="' + IntToStr(FPendingFocusTaskItemId) + '"] ' +
'[data-field="' + FPendingFocusField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusTaskItemId := 0;
FPendingFocusField := '';
end;
procedure TFTasksHTML.ApplyPendingDeleteFocus;
var
el: TJSHTMLElement;
rowEl: TJSHTMLElement;
selector: string;
taskItemIdStr: string;
taskIdStr: string;
begin
if (FPendingFocusItemNum <= 0) or (FPendingFocusTaskField = '') then
Exit;
selector :=
'tr[data-item-num="' + IntToStr(FPendingFocusItemNum) + '"] ' +
'[data-field="' + FPendingFocusTaskField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
rowEl := TJSHTMLElement(el.closest('tr'));
if Assigned(rowEl) then
begin
taskItemIdStr := string(rowEl.getAttribute('data-task-item-id'));
taskIdStr := string(rowEl.getAttribute('data-task-id'));
FSelectedTaskItemId := StrToIntDef(taskItemIdStr, 0);
FSelectedTaskId := StrToIntDef(taskIdStr, 0);
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
ApplySelectedRowState;
end;
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusItemNum := 0;
FPendingFocusTaskField := '';
end;
procedure TFTasksHTML.CaptureTableScroll;
begin
asm
const vscroll = document.querySelector('.tasks-vscroll');
const hscroll = document.querySelector('.tasks-hscroll');
this.FPendingScrollTop = vscroll ? vscroll.scrollTop : 0;
this.FPendingScrollLeft = hscroll ? hscroll.scrollLeft : 0;
end;
end;
procedure TFTasksHTML.RestoreTableScroll;
begin
asm
const vscroll = document.querySelector('.tasks-vscroll');
const hscroll = document.querySelector('.tasks-hscroll');
if (vscroll) vscroll.scrollTop = this.FPendingScrollTop || 0;
if (hscroll) hscroll.scrollLeft = this.FPendingScrollLeft || 0;
end;
end;
......
/* Note: Base layout */
html, body{
height:100%;
margin:0;
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
#wrapper{
height:100vh;
display:flex;
flex-direction:column;
min-height:0;
.is-invalid .form-check-label {
color: #dc3545 !important;
}
/* Note: Embedded forms must be allowed to shrink inside flex containers */
#main\.webpanel{
min-height:0;
flex:1 1 auto;
display:flex;
flex-direction:column;
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
#main\.webpanel > *{
min-height:0;
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
/* Note: Primary button color */
.btn-primary{
background-color:#286090 !important;
border-color:#286090 !important;
color:#fff !important;
@keyframes slideInLeft {
from {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.btn-primary:hover{
background-color:#204d74 !important;
border-color:#204d74 !important;
.toast.slide-in {
animation: slideInLeft 0.4s ease-out forwards;
}
/* Note: Navbar tweaks */
#view\.main\.apptitle{
display:flex;
align-items:center;
#spinner {
position: fixed !important;
z-index: 9999 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
.navbar-nav .nav-link.active{
color:#fff !important;
background-color:#004F84 !important;
font-weight:700;
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.navbar-nav .nav-link:hover{
color:#fff !important;
background-color:#286090 !important;
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.navbar-toggler{
display:none;
.tasks-vscroll {
height: 100%;
overflow: auto;
}
/* Note: Dropdown menu items */
.dropdown-menu a{
display:flex;
align-items:center;
width:100%;
padding:.5rem 1rem;
color:#000;
text-decoration:none;
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.dropdown-menu a:hover{
background-color:#204d74;
color:#fff;
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
.dropdown-menu a span{
flex-grow:1;
span.card {
border: none;
}
/* Note: Login card (used on login view) */
.login-card{
display:inline-block;
width:300px;
padding:0;
border-radius:10px;
box-shadow:0 4px 8px rgba(0,0,0,.1);
background-color:#fff;
}
/* Note: Validation helpers */
.is-invalid .form-check-input{
border:1px solid #dc3545 !important;
}
.is-invalid .form-check-label{
color:#dc3545 !important;
}
/* Note: Toast animation */
@keyframes slideInLeft{
from{transform:translateX(-120%);opacity:0;}
to{transform:translateX(0);opacity:1;}
}
.toast.slide-in{
animation:slideInLeft .4s ease-out forwards;
}
/* Note: Spinner overlay */
#spinner{
position:fixed !important;
z-index:9999 !important;
top:50%;
left:50%;
transform:translate(-50%,-50%);
}
/* Note: TasksHTML (table experiment) */
#tasks_table_host{
height:100%;
min-height:0;
}
#tasks_table_host .tasks-vscroll{
height:100%;
overflow-y:auto;
overflow-x:hidden;
}
#tasks_table_host .tasks-hscroll{
overflow-x:auto;
}
#tasks_table_host .tasks-hscroll table{
width:max-content;
min-width:100%;
table-layout:fixed;
}
#tasks_table_host thead th{
position:sticky;
top:0;
z-index:2;
background:var(--bs-body-bg);
}
#tasks_table_host td,
#tasks_table_host th{
padding:.25rem;
}
#tasks_table_host .nowrap-cell{white-space:nowrap;}
#tasks_table_host .wrap-cell{white-space:normal;word-break:break-word;}
#tasks_table_host .cell-input,
#tasks_table_host .cell-textarea{
border:0;
background:transparent;
border-radius:0;
padding:0;
margin:0;
box-shadow:none;
}
#tasks_table_host .cell-input:focus,
#tasks_table_host .cell-textarea:focus{
outline:0;
box-shadow:inset 0 -2px 0 var(--bs-primary);
}
#tasks_table_host .cell-textarea{
resize:none;
overflow:hidden;
white-space:pre-wrap;
}
/* Note: TasksDataGrid (TWebDataGrid experiment) */
#data_grid_tasks{
height:100%;
min-height:0;
}
#data_grid_tasks .ag-cell{
line-height:1.25;
padding-top:4px;
padding-bottom:4px;
}
#data_grid_tasks .ag-cell-inline-editing textarea{
line-height:1.25;
padding:4px 6px;
resize:none;
height:100%;
box-sizing:border-box;
}
program emT3web;
{$R *.dres}
program emT3Web;
uses
System.Classes,
Vcl.Forms,
System.SysUtils,
JS,
Web,
XData.Web.Connection,
WEBLib.Dialogs,
Auth.Service in 'Auth.Service.pas',
App.Types in 'App.Types.pas',
ConnectionModule in 'ConnectionModule.pas' {DMConnection: TWebDataModule},
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
App.Config in 'App.Config.pas',
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
Utils in 'Utils.pas',
View.Test in 'View.Test.pas' {FTest: TWebForm} {*.html},
View.TasksHTML in 'View.TasksHTML.pas' {FTasksHTML: TWebForm} {*.html},
View.Home in 'View.Home.pas' {FHome: TWebForm} {*.html};
uNameManager in 'uNameManager.pas';
{$R *.res}
procedure DisplayAccessDeniedModal(const ErrorMessage: string);
procedure DoLogout(AMsg: string = ''); forward;
procedure DisplayMainView;
procedure ConnectProc;
begin
TFViewMain.Display(@DoLogout);
end;
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "500px";
dlg.style.width = "90%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>emT3web</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + ErrorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='actionBtn' class='btn btn-primary'></button></div>";
document.body.appendChild(dlg);
dlg.showModal();
var btn = document.getElementById("actionBtn");
if (
(ErrorMessage.indexOf("Version mismatch") >= 0) ||
(ErrorMessage.indexOf("old version") >= 0)
) {
btn.textContent = "Reload";
btn.addEventListener("click", function () {
location.reload(true);
});
} else {
btn.textContent = "Close";
btn.addEventListener("click", function () {
dlg.close();
dlg.remove();
});
}
if not DMConnection.ApiConnection.Connected then
DMConnection.ApiConnection.Open(@ConnectProc)
else
ConnectProc;
end;
procedure Login(userId: string; taskId: string; urlCode: string);
procedure LoginSuccess;
begin
DisplayMainView;
end;
procedure LoginError(AMsg: string);
begin
ShowMessage('Login Error: ' + AMsg);
end;
begin
AuthService.Login( userId, taskId, urlCode,
@LoginSuccess,
@LoginError
);
end;
procedure DisplayLoginView(AMessage: string = '');
procedure DoLogin();
var
userIdParam: string;
taskIdParam: string;
codeParam: string;
begin
userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id'];
codeParam := Application.Parameters.Values['url_code'];
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
if AMessage = '' then
DisplayAccessDeniedModal('Access requires a valid emt3 link. Please reopen from emt3.')
else
DisplayAccessDeniedModal(AMessage);
Login( userIdParam, taskIdParam, codeParam );
end;
procedure DisplayMainView(const AUserId, ATaskId, ACode: string);
procedure DoLogout(AMsg: string);
begin
TFViewMain.Display(@DisplayLoginView, AUserId, ATaskId, ACode);
AuthService.Logout;
ShowMessage('Logout successful: ' + AMsg);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
DisplayLoginView(AMessage);
ShowMessage('UnauthorizedAccessProc: ' + AMessage);
end;
procedure StartApplication;
var
userIdParam: string;
taskIdParam: string;
codeParam: string;
ClientVer: string;
dialogMsg: TStringList;
begin
userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id'];
codeParam := Application.Parameters.Values['code'];
DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
ClientVer := TDMConnection.clientVersion;
DMConnection.InitApp(
procedure
begin
if not Success then
begin
DisplayAccessDeniedModal(ErrorMessage);
Exit;
end;
if AuthService.Authenticated and (not AuthService.TokenExpired) then
begin
DisplayMainView(userIdParam, taskIdParam, codeParam);
Exit;
end;
if (userIdParam <> '') and (taskIdParam <> '') and (codeParam <> '') then
begin
AuthService.Login(
userIdParam, taskIdParam, codeParam,
procedure
DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
begin
if Success then
begin
DisplayMainView(userIdParam, taskIdParam, codeParam);
end,
procedure(LoginError: string)
begin
DisplayLoginView('Invalid or expired link.' + sLineBreak + LoginError);
DoLogin();
end
);
Exit;
end;
DisplayLoginView;
end
else
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "500px";
dlg.style.width = "90%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>emT3 web app</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + ErrorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='refreshBtn' class='btn btn-primary'>Reload</button></div>";
document.body.appendChild(dlg);
dlg.showModal();
document.getElementById("refreshBtn").addEventListener("click", function () {
var base = location.origin + location.pathname;
location.replace(base + "?ver=" + ClientVer + "&r=" + Date.now() + location.hash);
});
end;
end;
end);
end,
@UnauthorizedAccessProc
);
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection);
DMConnection.InitApp(@StartApplication, @UnauthorizedAccessProc);
StartApplication;
Application.Run;
end.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{DB6F5DBF-7E4B-45DA-AFFA-6C8DF15BA740}</ProjectGuid>
<ProjectVersion>20.3</ProjectVersion>
<ProjectVersion>20.4</ProjectVersion>
<FrameworkType>VCL</FrameworkType>
<MainSource>emT3web.dpr</MainSource>
<MainSource>emT3Web.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Application</AppType>
<ProjectName Condition="'$(ProjectName)'==''">emT3web</ProjectName>
<ProjectName Condition="'$(ProjectName)'==''">emT3Web</ProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
......@@ -58,7 +58,7 @@
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)</DCC_Namespace>
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
<Icns_MainIcns>$(BDS)\bin\delphi_PROJECTICNS.icns</Icns_MainIcns>
<SanitizedProjectName>emT3web</SanitizedProjectName>
<SanitizedProjectName>emT3Web</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.802;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;LastCompiledTime=2018/07/25 12:57:53</VerInfo_Keys>
......@@ -100,9 +100,9 @@
<VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebBrowser>1</TMSWebBrowser>
<TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSWebOutputPath>..\emT3XDataServer\bin</TMSWebOutputPath>
<TMSWebBrowser>1</TMSWebBrowser>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
......@@ -129,25 +129,22 @@
<Form>DMConnection</Form>
<DesignClass>TWebDataModule</DesignClass>
</DCCReference>
<DCCReference Include="View.Login.pas">
<Form>FViewLogin</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="App.Config.pas"/>
<DCCReference Include="View.Main.pas">
<Form>FViewMain</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="Utils.pas"/>
<DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form>
<DCCReference Include="View.Test.pas">
<Form>FTest</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.Home.pas">
<Form>FHome</Form>
<DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uNameManager.pas"/>
<None Include="index.html"/>
<None Include="css\app.css"/>
<None Include="config\config.json"/>
......@@ -170,22 +167,27 @@
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">emT3web.dpr</Source>
<Source Name="MainSource">emT3Web.dpr</Source>
</Source>
<Excluded_Packages/>
<Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k370.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp370.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k370.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dclofficexp370.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
<Deployment Version="5">
<DeployFile LocalName="Win32\Debug\emT3web.exe" Configuration="Debug" Class="ProjectOutput">
<DeployFile LocalName="Win32\Debug\emT3Web.exe" Configuration="Debug" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>emT3web.exe</RemoteName>
<RemoteName>emT3Web.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win32\Debug\webCharms.exe" Configuration="Debug" Class="ProjectOutput"/>
<DeployFile LocalName="Win32\Debug\webKGOrders.exe" Configuration="Debug" Class="ProjectOutput"/>
<DeployFile LocalName="Win32\Release\emT3web.exe" Configuration="Release" Class="ProjectOutput">
<DeployFile LocalName="Win32\Release\emT3Web.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>emT3web.exe</RemoteName>
<RemoteName>emT3Web.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
......@@ -909,6 +911,9 @@
<Platform Name="Win64x">
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceDebug">
<Platform Name="iOSDevice32">
......@@ -979,6 +984,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo44">
<Platform Name="Win32">
......@@ -989,6 +998,10 @@
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iOS_AppStore1024">
<Platform Name="iOSDevice64">
......@@ -1203,6 +1216,7 @@
<ProjectRoot Platform="Win32" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64x" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="WinARM64EC" Name="$(PROJECTNAME)"/>
</Deployment>
<Platforms>
<Platform value="Win32">True</Platform>
......
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>Em Systems - emT3 Web</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet" type="text/css"/>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" type="text/javascript"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="$(ProjectName).js" type="text/javascript"></script>
</head>
<body>
</body>
<script type="text/javascript">rtl.run();</script>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="data:;base64,=" rel="icon"/>
<title>emT3Web</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="$(ProjectName).js"></script>
</head>
<body>
<noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script>
</body>
</html>
unit App.Config;
interface
uses
JS,
XData.Web.Connection,
XData.Web.Request,
XData.Web.Response;
type
TAppConfig = class
private
FAuthUrl: string;
FApiUrl: string;
FAppUrl: string;
public
constructor Create;
property AuthUrl: string read FAuthUrl write FAuthUrl;
property ApiUrl: string read FApiUrl write FApiUrl;
property AppUrl: string read FAppUrl write FAppUrl;
end;
TConfigLoadedProc = reference to procedure(Config: TAppConfig);
procedure LoadConfig(LoadProc: TConfigLoadedProc);
implementation
procedure LoadConfig(LoadProc: TConfigLoadedProc);
procedure OnSuccess(Response: IHttpResponse);
var
Obj: TJSObject;
Config: TAppConfig;
begin
Config := TAppConfig.Create;
try
if Response.StatusCode = 200 then
begin
Obj := TJSObject(TJSJSON.parse(Response.ContentAsText));
if JS.toString(Obj['AuthUrl']) <> '' then
Config.AuthUrl := JS.toString(Obj['AuthUrl']);
if JS.toString(Obj['ApiUrl']) <> '' then
Config.ApiUrl := JS.toString(Obj['ApiUrl']);
end;
finally
LoadProc(Config);
Config.Free;
end;
end;
procedure OnError;
var
Config: TAppConfig;
begin
Config := TAppConfig.Create;
try
LoadProc(Config);
finally
Config.Free;
end;
end;
var
Conn: TXDataWebConnection;
begin
Conn := TXDataWebConnection.Create(nil);
try
Conn.SendRequest(THttpRequest.Create('config/config.json'), @OnSuccess, @OnError);
finally
Conn.Free;
end;
end;
{ TAppConfig }
constructor TAppConfig.Create;
begin
FAuthUrl := '';
FApiUrl := '';
FAppUrl := '';
end;
end.
unit App.Types;
interface
uses
Bcl.Rtti.Common;
type
TProc = reference to procedure;
TSuccessProc = reference to procedure;
TLogoutProc = reference to procedure(AMessage: string = '');
TUnauthorizedAccessProc = reference to procedure(AMessage: string);
TVersionCheckCallback = reference to procedure(Success: Boolean; ErrorMessage: string);
TListProc = reference to procedure;
TSelectProc = reference to procedure(AParam: string);
TSelectProc2 = reference to procedure(AParam: string; BParam: string);
TSelectProc3 = reference to procedure(AParam: string; BParam: string; CParam: Boolean);
TSelectProc4 = reference to procedure(AParam: string; BParam: string; CParam: string; DParam: Boolean);
TSearchProc = reference to procedure(AParam: string; BParam: string; CParam: Integer; DParam: Boolean);
TReportProc = reference to procedure(AParam: string);
implementation
end.
unit Auth.Service;
interface
uses
SysUtils, Web, JS,
XData.Web.Client;
const
TOKEN_NAME = 'EMT3_WEB_TOKEN';
type
TOnLoginSuccess = reference to procedure;
TOnLoginError = reference to procedure(AMsg: string);
TOnProfileSuccess = reference to procedure;
TOnProfileError = reference to procedure(AMsg: string);
TAuthService = class
private
FClient: TXDataWebClient;
procedure SetToken(AToken: string);
procedure DeleteToken;
public
constructor Create; reintroduce;
destructor Destroy; override;
procedure Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure Logout;
function GetToken: string;
function Authenticated: Boolean;
function TokenExpirationDate: TDateTime;
function TokenExpired: Boolean;
function TokenPayload: JS.TJSObject;
end;
TJwtHelper = class
private
class function HasExpirationDate(AToken: string): Boolean;
public
class function TokenExpirationDate(AToken: string): TJSDate;
class function TokenExpired(AToken: string): Boolean;
class function DecodePayload(AToken: string): string;
end;
function AuthService: TAuthService;
implementation
uses
ConnectionModule;
var
_AuthService: TAuthService;
function AuthService: TAuthService;
begin
if not Assigned(_AuthService) then
begin
_AuthService := TAuthService.Create;
end;
Result := _AuthService;
end;
{ TAuthService }
function TAuthService.Authenticated: Boolean;
begin
Result := not isNull(window.localStorage.getItem(TOKEN_NAME)) and
(window.localStorage.getItem(TOKEN_NAME) <> '');
end;
constructor TAuthService.Create;
begin
FClient := TXDataWebClient.Create(nil);
FClient.Connection := DMConnection.AuthConnection;
end;
procedure TAuthService.DeleteToken;
begin
window.localStorage.removeItem(TOKEN_NAME);
end;
destructor TAuthService.Destroy;
begin
FClient.Free;
inherited;
end;
function TAuthService.GetToken: string;
begin
Result := window.localStorage.getItem(TOKEN_NAME);
end;
procedure TAuthService.Login(AUser, APassword, AClientVersion: string; ASuccess: TOnLoginSuccess;
AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse);
var
Token: JS.TJSObject;
begin
Token := JS.TJSObject(Response.Result);
SetToken(JS.toString(Token.Properties['value']));
ASuccess;
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
if (AUser = '') or (APassword = '') then
begin
AError('Please enter a username and a password');
Exit;
end;
FClient.RawInvoke(
'IAuthService.Login', [AUser, APassword, AClientVersion],
@OnLoad, @OnError
);
end;
procedure TAuthService.Logout;
begin
DeleteToken;
end;
procedure TAuthService.SetToken(AToken: string);
begin
window.localStorage.setItem(TOKEN_NAME, AToken);
end;
function TAuthService.TokenExpirationDate: TDateTime;
var
ExpirationDate: TJSDate;
begin
if not Authenticated then
Exit(Now);
ExpirationDate := TJwtHelper.TokenExpirationDate(GetToken);
Result := EncodeDate(
ExpirationDate.FullYear,
ExpirationDate.Month + 1,
ExpirationDate.Date
) +
EncodeTime(
ExpirationDate.Hours,
ExpirationDate.Minutes,
ExpirationDate.Seconds,
0
);
end;
function TAuthService.TokenExpired: Boolean;
begin
if not Authenticated then
Exit(False);
Result := TJwtHelper.TokenExpired(GetToken);
end;
function TAuthService.TokenPayload: JS.TJSObject;
begin
if not Authenticated then
Exit(nil);
Result := TJSObject(TJSJSON.parse(TJwtHelper.DecodePayload(GetToken)));
end;
{ TJwtHelper }
class function TJwtHelper.DecodePayload(AToken: string): string;
begin
if Trim(AToken) = '' then
Exit('');
Result := '';
asm
const parts = AToken.split('.');
if (parts.length === 3) { // <- strict compare
// JWTs use url-safe base64; convert before atob
Result = atob(parts[1].replace(/-/g,'+').replace(/_/g,'/'));
}
end;
end;
class function TJwtHelper.HasExpirationDate(AToken: string): Boolean;
var
Payload: string;
Obj: TJSObject;
begin
Payload := DecodePayload(AToken);
Obj := TJSObject(TJSJSON.parse(Payload));
Result := Obj.hasOwnProperty('exp');
end;
class function TJwtHelper.TokenExpirationDate(AToken: string): TJSDate;
var
Payload: string;
Obj: TJSObject;
Epoch: NativeInt;
begin
if not HasExpirationDate(AToken) then
raise Exception.Create('Token has no expiration date');
Payload := DecodePayload(AToken);
Obj := TJSObject(TJSJSON.parse(Payload));
Epoch := toInteger(Obj.Properties['exp']);
Result := TJSDate.New(Epoch * 1000);
end;
class function TJwtHelper.TokenExpired(AToken: string): Boolean;
begin
if not HasExpirationDate(AToken) then
Exit(False);
Result := TJSDate.now > toInteger(TokenExpirationDate(AToken).valueOf);
end;
end.
object DMConnection: TDMConnection
Height = 264
Width = 395
object ApiConnection: TXDataWebConnection
URL = 'http://localhost:2004/emsys/emt3/api'
OnError = ApiConnectionError
OnRequest = ApiConnectionRequest
OnResponse = ApiConnectionResponse
Left = 48
Top = 80
end
object AuthConnection: TXDataWebConnection
URL = 'http://localhost:2004/emsys/emt3/auth'
OnError = AuthConnectionError
Left = 48
Top = 16
end
object XDataWebClient1: TXDataWebClient
Connection = AuthConnection
Left = 269
Top = 164
end
end
unit ConnectionModule;
interface
uses
System.SysUtils, System.Classes, WEBLib.Modules, XData.Web.Connection,
App.Types, App.Config, XData.Web.Client, WEBLib.Dialogs;
type
TDMConnection = class(TWebDataModule)
ApiConnection: TXDataWebConnection;
AuthConnection: TXDataWebConnection;
XDataWebClient1: TXDataWebClient;
procedure ApiConnectionError(Error: TXDataWebConnectionError);
procedure ApiConnectionRequest(Args: TXDataWebConnectionRequest);
procedure ApiConnectionResponse(Args: TXDataWebConnectionResponse);
procedure AuthConnectionError(Error: TXDataWebConnectionError);
private
FUnauthorizedAccessProc: TUnauthorizedAccessProc;
public
const clientVersion = '0.8.2';
procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure SetClientConfig(Callback: TVersionCheckCallback);
end;
var
DMConnection: TDMConnection;
implementation
uses
JS, Web,
XData.Web.Request,
XData.Web.Response,
Auth.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
procedure TDMConnection.ApiConnectionError(Error: TXDataWebConnectionError);
var
errorMsg: string;
begin
errorMsg := Error.ErrorMessage;
if errorMsg = '' then
errorMsg := 'Connection error';
if Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(errorMsg)
else
ShowMessage(errorMsg);
end;
procedure TDMConnection.ApiConnectionRequest(Args: TXDataWebConnectionRequest);
begin
if AuthService.Authenticated then
Args.Request.Headers.SetValue('Authorization', 'Bearer ' + AuthService.GetToken);
end;
procedure TDMConnection.ApiConnectionResponse(
Args: TXDataWebConnectionResponse);
begin
if Args.Response.StatusCode = 401 then
FUnauthorizedAccessProc(Format('%d: %s',[Args.Response.StatusCode, Args.Response.ContentAsText]));
end;
procedure TDMConnection.AuthConnectionError(Error: TXDataWebConnectionError);
var
errorMsg: string;
begin
errorMsg := Error.ErrorMessage;
if errorMsg = '' then
errorMsg := 'Connection error';
if Assigned(FUnauthorizedAccessProc) then
FUnauthorizedAccessProc(errorMsg)
else
ShowMessage(errorMsg);
end;
procedure TDMConnection.InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc);
procedure ConfigLoaded(Config: TAppConfig);
begin
if Config.AuthUrl <> '' then
AuthConnection.URL := Config.AuthUrl;
if Config.ApiUrl <> '' then
ApiConnection.URL := Config.ApiUrl;
AuthConnection.Open(SuccessProc);
end;
begin
FUnauthorizedAccessProc := UnauthorizedAccessProc;
LoadConfig(@ConfigLoaded);
end;
procedure TDMConnection.SetClientConfig(Callback: TVersionCheckCallback);
begin
XDataWebClient1.Connection := AuthConnection;
XDataWebClient1.RawInvoke('IAuthService.VerifyVersion', [clientVersion],
procedure(Response: TXDataClientResponse)
var
jsonResult: TJSObject;
error: string;
begin
jsonResult := TJSObject(Response.Result);
if jsonResult.HasOwnProperty('error') then
error := string(jsonResult['error'])
else
error := '';
if error <> '' then
Callback(False, error)
else
Callback(True, '');
end);
end;
end.
unit Utils;
interface
uses
System.Classes, SysUtils, JS, Web, WEBLib.Forms, WEBLib.Toast, DateUtils, WebLib.Dialogs;
procedure ShowStatusMessage(const AMessage, AClass: string; const AElementId: string);
procedure HideStatusMessage(const AElementId: string);
procedure ShowSpinner(SpinnerID: string);
procedure HideSpinner(SpinnerID: string);
procedure ShowErrorModal(msg: string);
procedure ShowToast(const MessageText: string; const ToastType: string = 'success');
procedure ShowConfirmationModal(msg, leftLabel, rightLabel: string; ConfirmProc: TProc<Boolean>);
procedure ShowNotificationModal(msg: string);
implementation
procedure ShowStatusMessage(const AMessage, AClass: string; const AElementId: string);
var
StatusMessage: TJSHTMLElement;
begin
StatusMessage := TJSHTMLElement(document.getElementById(AElementId));
if Assigned(StatusMessage) then
begin
if AMessage = '' then
begin
StatusMessage.style.setProperty('display', 'none');
StatusMessage.className := '';
StatusMessage.innerHTML := '';
end
else
begin
StatusMessage.innerHTML := AMessage;
StatusMessage.className := 'alert ' + AClass;
StatusMessage.style.setProperty('display', 'block');
end
end
else
console.log('Error: Status message element not found');
end;
procedure HideStatusMessage(const AElementId: string);
var
StatusMessage: TJSHTMLElement;
begin
StatusMessage := TJSHTMLElement(document.getElementById(AElementId));
if Assigned(StatusMessage) then
begin
StatusMessage.style.setProperty('display', 'none');
StatusMessage.className := '';
StatusMessage.innerHTML := '';
end
else
console.log('Error: Status message element not found');
end;
procedure ShowSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then
begin
// Move spinner to the <body> if it's not already there
asm
if (SpinnerElement.parentNode !== document.body) {
document.body.appendChild(SpinnerElement);
}
end;
SpinnerElement.classList.remove('d-none');
SpinnerElement.classList.add('d-block');
end;
end;
procedure HideSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then
begin
SpinnerElement.classList.remove('d-block');
SpinnerElement.classList.add('d-none');
end;
end;
procedure ShowErrorModal(msg: string);
begin
asm
var modal = document.getElementById('main_errormodal');
var label = document.getElementById('main_lblmodal_body');
var reloadBtn = document.getElementById('btn_modal_restart');
if (label) label.innerText = msg;
// Ensure modal is a direct child of <body>
if (modal && modal.parentNode !== document.body) {
document.body.appendChild(modal);
}
// Bind hard reload to button
if (reloadBtn) {
reloadBtn.onclick = function () {
window.location.reload(true); // hard reload, bypass cache
};
}
// Show the Bootstrap modal
var bsModal = new bootstrap.Modal(modal, { keyboard: false });
bsModal.show();
end;
end;
procedure ShowNotificationModal(msg: string);
begin
asm
var modal = document.getElementById('main_notification_modal');
var label = document.getElementById('main_notification_modal_body');
var closeBtn = document.getElementById('btn_modal_close');
if (label) label.innerText = msg;
// Ensure modal is a direct child of <body>
if (modal && modal.parentNode !== document.body) {
document.body.appendChild(modal);
}
// Button simply closes the modal
if (closeBtn) {
closeBtn.onclick = function () {
var existing = bootstrap.Modal.getInstance(modal);
if (existing) {
existing.hide();
}
};
}
// Show the Bootstrap modal
var bsModal = new bootstrap.Modal(modal, { keyboard: false });
bsModal.show();
end;
end;
procedure ShowConfirmationModal(msg, leftLabel, rightLabel: string; ConfirmProc: TProc<Boolean>);
begin
asm
var modal = document.getElementById('main_confirmation_modal');
var body = document.getElementById('main_modal_body');
var btnLeft = document.getElementById('btn_confirm_left');
var btnRight = document.getElementById('btn_confirm_right');
var bsModal;
if (body) body.innerText = msg;
if (btnLeft) btnLeft.innerText = leftLabel;
if (btnRight) btnRight.innerText = rightLabel;
if (modal && modal.parentNode !== document.body) {
document.body.appendChild(modal);
}
btnLeft.onclick = null;
btnRight.onclick = null;
btnLeft.onclick = function () {
bsModal.hide();
ConfirmProc(true); // user confirmed
};
btnRight.onclick = function () {
bsModal.hide();
ConfirmProc(false); // user canceled
};
bsModal = new bootstrap.Modal(modal, { keyboard: false });
bsModal.show();
end;
end;
procedure ShowToast(const MessageText: string; const ToastType: string = 'success');
var
ParsedText, ToastKind, MsgPrefix: string;
Parts: TArray<string>;
begin
ParsedText := MessageText.Trim;
ToastKind := ToastType.ToLower;
// Check for "Success:" or "Failure:" at the start of message
if ParsedText.Contains(':') then
begin
Parts := ParsedText.Split([':'], 2);
MsgPrefix := Parts[0].Trim.ToLower;
if (MsgPrefix = 'success') or (MsgPrefix = 'failure') then
begin
ParsedText := Parts[1].Trim;
if MsgPrefix = 'success' then
ToastKind := 'success'
else
ToastKind := 'danger';
end;
end;
asm
var toastEl = document.getElementById('bootstrapToast');
var toastBody = document.getElementById('bootstrapToastBody');
if (!toastEl || !toastBody) return;
toastBody.innerText = ParsedText;
toastEl.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-primary');
toastEl.classList.remove('slide-in');
switch (ToastKind) {
case 'danger':
toastEl.classList.add('bg-danger');
break;
case 'warning':
toastEl.classList.add('bg-warning');
break;
case 'info':
toastEl.classList.add('bg-primary');
break;
default:
toastEl.classList.add('bg-success');
}
// Add slide-in animation
toastEl.classList.add('slide-in');
var toast = new bootstrap.Toast(toastEl, { delay: 2500 });
toast.show();
// Remove animation class after it's done (so it can be reapplied)
setTimeout(function() {
toastEl.classList.remove('slide-in');
}, 500);
end;
end;
end.
object FViewMain: TFViewMain
Width = 1322
Height = 764
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnCreate = WebFormCreate
object lblUsername: TWebLabel
Left = 536
Top = 4
Width = 49
Height = 14
Caption = 'Username'
ElementID = 'lbl_username'
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object lblUserProfile: TWebLinkLabel
Left = 529
Top = 21
Width = 59
Height = 14
ElementID = 'lbl_user_profile'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
Caption = ' User Profile'
end
object lblLogout: TWebLinkLabel
Left = 547
Top = 55
Width = 36
Height = 14
ElementID = 'lbl_logout'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = lblLogoutClick
Caption = ' Logout'
end
object lblHome: TWebLinkLabel
Left = 556
Top = 38
Width = 27
Height = 14
ElementID = 'lbl_home'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
Caption = 'Home'
end
object lblAppTitle: TWebLabel
Left = 57
Top = 33
Width = 48
Height = 14
Caption = 'emT3Web'
ElementID = 'lbl_app_title'
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object lblVersion: TWebLabel
Left = 536
Top = 71
Width = 47
Height = 14
Caption = 'lblVersion'
ElementID = 'lbl_version'
ElementFont = efCSS
ElementPosition = epRelative
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object pnlMain: TWebPanel
Left = 62
Top = 92
Width = 393
Height = 219
ElementID = 'pnl_main'
HeightStyle = ssAuto
WidthStyle = ssAuto
ChildOrder = 3
ElementFont = efCSS
ElementPosition = epIgnore
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
Role = 'null'
TabOrder = 0
end
object xdwcMain: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 76
Top = 332
end
end
<div id="div_wrapper">
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom shadow-sm">
<div class="container-fluid">
<div class="d-flex align-items-center gap-2">
<a id="lbl_app_title" class="navbar-brand fw-semibold" href="index.html">emT3Web</a>
<span id="lbl_version" class="badge text-bg-light border text-muted fw-normal"></span>
</div>
<div class="collapse navbar-collapse show" id="pnl_navbar_nav_dropdown">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" id="lnk_navbar_dropdown_menu_link"
role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user fa-fw"></i>
<span id="lbl_username" class="fw-semibold">Username</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="lnk_navbar_dropdown_menu_link">
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_home" href="#">
<i class="fa fa-home fa-fw"></i><span>Home</span>
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2" id="lbl_user_profile" href="#">
<i class="fa fa-user fa-fw"></i><span>User Profile</span>
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2 text-danger" id="lbl_logout" href="#">
<i class="fa fa-sign-out fa-fw"></i><span>Logout</span>
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Toast -->
<div id="pnl_toast_wrapper" class="position-fixed top-0 start-0 mt-5 ms-4"
style="z-index: 1080; min-width: 300px; max-width: 500px;">
<div id="toast_bootstrap" class="toast align-items-center text-white bg-success border-0 shadow" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="lbl_bootstrap_toast_body">
Success message
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
</div>
</div>
<!-- Main Panel (where all forms display) -->
<div class="container-fluid py-3 d-flex flex-column overflow-hidden" style="height: calc(100vh - 57px);">
<div id="pnl_main" class="flex-grow-1 min-h-0 overflow-hidden"></div>
</div>
<!-- Spinner Modal -->
<div id="div_spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller">
<div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div>
</div>
</div>
<!-- Error Modal -->
<div class="modal fade" id="mdl_error" tabindex="-1" aria-labelledby="lbl_modal_title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_modal_title">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_modal_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_restart" class="btn btn-primary">Back to Orders</button>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="mdl_confirmation" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="lbl_confirmation_body">
Placeholder text
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary me-3" id="btn_confirm_left">Cancel</button>
<button type="button" class="btn btn-secondary" id="btn_confirm_right">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div class="modal fade" id="mdl_notification" tabindex="-1" aria-labelledby="lbl_notification_title"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="lbl_notification_title">Info</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="lbl_notification_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<button type="button" id="btn_modal_close" class="btn btn-primary">Close</button>
</div>
</div>
</div>
</div>
</div>
unit View.Main;
interface
uses
System.SysUtils, System.Classes, JS, Web,
WEBLib.Controls, WEBLib.Forms, WEBLib.ExtCtrls, WEBLib.StdCtrls,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Dialogs, Vcl.StdCtrls,
Vcl.Controls, Vcl.Graphics;
type
TFViewMain = class(TWebForm)
pnlMain: TWebPanel;
lblUsername: TWebLabel;
lblUserProfile: TWebLinkLabel;
lblHome: TWebLinkLabel;
lblLogout: TWebLinkLabel;
lblVersion: TWebLabel;
lblAppTitle: TWebLabel;
xdwcMain: TXDataWebClient;
procedure WebFormCreate(Sender: TObject);
procedure lblLogoutClick(Sender: TObject);
private
FChildForm: TWebForm;
FLogoutProc: TLogoutProc;
procedure ShowForm(aFormClass: TWebFormClass);
public
class procedure Display(logoutProc: TLogoutProc);
end;
var
FViewMain: TFViewMain;
implementation
uses
Auth.Service,
View.Test,
View.TasksHTML;
{$R *.dfm}
procedure TFViewMain.WebFormCreate(Sender: TObject);
var
userName: string;
begin
userName := JS.toString(AuthService.TokenPayload.Properties['user_name']);
lblUsername.Caption := userName;
lblVersion.Caption := 'v' + DMConnection.clientVersion;
ShowForm(TFTasksHTML);
end;
procedure TFViewMain.lblLogoutClick(Sender: TObject);
begin
if Assigned(FLogoutProc) then
FLogoutProc('');
end;
procedure TFViewMain.ShowForm(aFormClass: TWebFormClass);
begin
if Assigned(FChildForm) then
FChildForm.Free;
Application.CreateForm(aFormClass, pnlMain.ElementID, FChildForm);
end;
class procedure TFViewMain.Display(logoutProc: TLogoutProc);
begin
if Assigned(FViewMain) then
FViewMain.Free;
FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := logoutProc;
end;
end.
object FTasksHTML: TFTasksHTML
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object btnReload: TWebButton
Left = 78
Top = 88
Width = 96
Height = 25
Caption = 'Reload'
ElementID = 'btn_reload'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnReloadClick
end
object btnAddRow: TWebButton
Left = 78
Top = 119
Width = 96
Height = 25
Caption = 'Add Row'
ChildOrder = 1
ElementID = 'btn_add_row'
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnAddRowClick
end
object btnDeleteRow: TWebButton
Left = 78
Top = 150
Width = 96
Height = 25
Caption = 'Delete Row'
ChildOrder = 2
ElementID = 'btn_delete_row'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnDeleteRowClick
end
object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 506
Top = 92
end
object xdwdsTasks: TXDataWebDataSet
Left = 506
Top = 148
object xdwdsTaskstaskID: TStringField
FieldName = 'taskId'
end
object xdwdsTasksitemNum: TIntegerField
FieldName = 'itemNum'
end
object xdwdsTasksapplication: TStringField
FieldName = 'application'
end
object xdwdsTasksversion: TStringField
FieldName = 'version'
end
object xdwdsTaskstaskDate: TStringField
FieldName = 'taskDate'
end
object xdwdsTasksreportedBy: TStringField
FieldName = 'reportedBy'
end
object xdwdsTasksassignedTo: TStringField
FieldName = 'assignedTo'
end
object xdwdsTasksstatus: TStringField
FieldName = 'status'
end
object xdwdsTasksstatusDate: TStringField
FieldName = 'statusDate'
end
object xdwdsTasksformSection: TStringField
FieldName = 'formSection'
end
object xdwdsTasksissue: TStringField
FieldName = 'issue'
end
object xdwdsTasksnotes: TStringField
FieldName = 'notes'
end
object xdwdsTaskstaskItemId: TIntegerField
FieldName = 'taskItemId'
end
end
end
<div class="container-fluid p-2 d-flex flex-column h-100 overflow-hidden">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0">
<h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex align-items-center gap-3">
<div id="lbl_total_rows"></div>
<div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-success">Add Row</button>
<button id="btn_delete_row" class="btn btn-sm btn-danger">Delete Row</button>
<button id="btn_reload" class="btn btn-sm btn-primary">Reload</button>
</div>
</div>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-h-0 overflow-auto"></div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNameManager" aria-labelledby="nm_title">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="nm_title">Add Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div id="nm_existing_list" class="list-group mb-3"></div>
<div id="nm_add_wrap" class="d-none mb-3">
<input id="nm_name_input" type="text" class="form-control" maxlength="100">
<div id="nm_name_invalid" class="invalid-feedback d-none"></div>
<div class="d-flex justify-content-end mt-2">
<button id="btn_nm_save" type="button" class="btn btn-success">Save</button>
</div>
</div>
<button id="btn_nm_add_another" type="button" class="btn btn-secondary">
Add another item
</button>
</div>
</div>
</div>
unit View.TasksHTML;
interface
uses
System.SysUtils, System.Classes,
JS, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
WEBLib.ExtCtrls, uNameManager,
XData.Web.Client, XData.Web.Dataset,
Utils, Data.DB, XData.Web.JsonDataset, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls;
type
TFTasksHTML = class(TWebForm)
xdwcTasks: TXDataWebClient;
xdwdsTasks: TXDataWebDataSet;
xdwdsTaskstaskId: TStringField;
xdwdsTasksapplication: TStringField;
xdwdsTasksversion: TStringField;
xdwdsTaskstaskDate: TStringField;
xdwdsTasksreportedBy: TStringField;
xdwdsTasksassignedTo: TStringField;
xdwdsTasksstatus: TStringField;
xdwdsTasksstatusDate: TStringField;
xdwdsTasksformSection: TStringField;
xdwdsTasksissue: TStringField;
xdwdsTasksnotes: TStringField;
btnReload: TWebButton;
btnAddRow: TWebButton;
xdwdsTasksitemNum: TIntegerField;
xdwdsTaskstaskItemId: TIntegerField;
btnDeleteRow: TWebButton;
[async] procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
[async] procedure btnDeleteRowClick(Sender: TObject);
private
FTaskId: string;
FReportedByOptions: TJSArray;
FAssignedToOptions: TJSArray;
FStatusOptions: TJSArray;
FPendingFocusTaskItemId: integer;
FPendingFocusField: string;
FSelectedTaskItemId: Integer;
FSelectedTaskId: Integer;
FPendingScrollTop: Integer;
FPendingScrollLeft: Integer;
FPendingFocusItemNum: Integer;
FPendingFocusTaskField: string;
FNameManager: TNameManager;
[async] procedure LoadTasks(const ATaskId: string);
procedure RenderTable;
procedure BindTableEditors;
procedure EditorInput(Event: TJSEvent);
procedure SelectChange(Event: TJSEvent);
procedure EnableColumnResize;
procedure EnableAutoGrowTextAreas;
procedure GotoRowIndex(AIndex: Integer);
function HtmlEncode(const s: string): string;
procedure SetTotalRowsLabel(ARowCount: Integer);
procedure SetTaskLabel(const ATitle: string);
[async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent);
[async] function AddTaskRow: Boolean;
[async] function DeleteTaskRow: Boolean;
function ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
function GetOptionsForField(const AFieldName: string): TJSArray;
procedure FocusTrigger(const ATriggerId: string);
procedure DropdownItemClick(Event: TJSEvent);
procedure DropdownEditClick(Event: TJSEvent);
function ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
[async] procedure MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
procedure ApplyPendingFocus;
procedure RowClick(Event: TJSEvent);
procedure ApplySelectedRowState;
procedure ApplyPendingDeleteFocus;
procedure EditorKeyDown(Event: TJSEvent);
procedure CaptureTableScroll;
procedure RestoreTableScroll;
public
end;
var
FTasksHTML: TFTasksHTML;
implementation
uses
ConnectionModule;
{$R *.dfm}
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
begin
console.log('TFTasksHTML.WebFormCreate fired');
FTaskId := Application.Parameters.Values['task_id'];
FReportedByOptions := TJSArray.new;
FAssignedToOptions := TJSArray.new;
FStatusOptions := TJSArray.new;
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
FNameManager := TNameManager.Create(
function(const AFieldName: string): TJSArray
begin
Result := GetOptionsForField(AFieldName);
end,
procedure
begin
RenderTable;
end,
procedure(const ATriggerId: string)
begin
FocusTrigger(ATriggerId);
end
);
FNameManager.BindControls;
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing task_id. Please reopen from emt3.');
Exit;
end;
btnAddRow.Enabled := False;
btnDeleteRow.Enabled := False;
if not DMConnection.ApiConnection.Connected then
begin
DMConnection.ApiConnection.Open(
procedure
begin
LoadTasks(FTaskId);
end
);
end
else
LoadTasks(FTaskId);
end;
function TFTasksHTML.HtmlEncode(const s: string): string;
begin
Result := s;
Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;
procedure TFTasksHTML.GotoRowIndex(AIndex: Integer);
var
i: Integer;
begin
if (AIndex < 0) or (not xdwdsTasks.Active) then
Exit;
xdwdsTasks.First;
i := 0;
while (i < AIndex) and (not xdwdsTasks.Eof) do
begin
xdwdsTasks.Next;
Inc(i);
end;
end;
procedure TFTasksHTML.EditorInput(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName: string;
newVal: string;
begin
if not xdwdsTasks.Active then
Exit;
el := TJSHTMLElement(Event.target);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
GotoRowIndex(idx);
if xdwdsTasks.Eof then
Exit;
newVal := string(TJSObject(el)['value']);
console.log('EditorInput: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + newVal);
xdwdsTasks.Edit;
if SameText(fieldName, 'itemNum') then
xdwdsTasks.FieldByName(fieldName).AsInteger := StrToIntDef(newVal, 0)
else
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
xdwdsTasks.Post;
el.setAttribute('data-unsaved-data', '1');
end;
procedure TFTasksHTML.SelectChange(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName: string;
sel: TJSHTMLSelectElement;
begin
if not xdwdsTasks.Active then
Exit;
el := TJSHTMLElement(Event.target);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
GotoRowIndex(idx);
if xdwdsTasks.Eof then
Exit;
sel := TJSHTMLSelectElement(el);
console.log('SelectChange: idx=' + IntToStr(idx) + ' field=' + fieldName + ' val=' + string(sel.value));
xdwdsTasks.Edit;
xdwdsTasks.FieldByName(fieldName).AsString := string(sel.value);
xdwdsTasks.Post;
el.setAttribute('data-unsaved-data', '1');
SaveRow(idx);
end;
procedure TFTasksHTML.BindTableEditors;
var
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
begin
console.log('BindTableEditors: wiring handlers...');
nodes := document.querySelectorAll('.task-editor');
console.log('BindTableEditors: task-editor count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('input', TJSEventHandler(@EditorInput));
el.addEventListener('blur', TJSEventHandler(@EditorBlur));
el.addEventListener('keydown', TJSEventHandler(@EditorKeyDown));
end;
nodes := document.querySelectorAll('.task-select');
console.log('BindTableEditors: task-select count=' + IntToStr(nodes.length));
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('change', TJSEventHandler(@SelectChange));
end;
nodes := document.querySelectorAll('.task-dd-item');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownItemClick));
end;
nodes := document.querySelectorAll('.task-dd-edit-btn');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@DropdownEditClick));
end;
nodes := document.querySelectorAll('.task-row-selectable');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
el.addEventListener('click', TJSEventHandler(@RowClick));
end;
ApplySelectedRowState;
end;
procedure TFTasksHTML.EditorKeyDown(Event: TJSEvent);
var
el: TJSHTMLElement;
fieldName: string;
newItemNum: Integer;
idx: Integer;
begin
el := TJSHTMLElement(Event.target);
fieldName := string(el.getAttribute('data-field'));
if not SameText(fieldName, 'itemNum') then
Exit;
if TJSKeyboardEvent(Event).key <> 'Enter' then
Exit;
Event.preventDefault;
idx := StrToIntDef(string(el.getAttribute('data-idx')), -1);
if idx < 0 then
Exit;
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
el.removeAttribute('data-unsaved-data');
MoveTaskRow(idx, newItemNum);
end;
[async] procedure TFTasksHTML.btnAddRowClick(Sender: TObject);
begin
Utils.ShowSpinner('spinner');
try
if await(AddTaskRow) then
begin
CaptureTableScroll;
LoadTasks(FTaskId);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
[async] procedure TFTasksHTML.btnDeleteRowClick(Sender: TObject);
var
deletedItemNum: Integer;
begin
if FSelectedTaskItemId <= 0 then
Exit;
deletedItemNum := 0;
if xdwdsTasks.Active then
begin
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
if xdwdsTaskstaskItemId.AsInteger = FSelectedTaskItemId then
begin
deletedItemNum := xdwdsTasksitemNum.AsInteger;
Break;
end;
xdwdsTasks.Next;
end;
end;
Utils.ShowSpinner('spinner');
try
if await(DeleteTaskRow) then
begin
FSelectedTaskItemId := 0;
FSelectedTaskId := 0;
btnDeleteRow.Enabled := False;
FPendingFocusItemNum := deletedItemNum;
FPendingFocusTaskField := 'application';
CaptureTableScroll;
LoadTasks(FTaskId);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTasksHTML.RowClick(Event: TJSEvent);
var
rowEl: TJSHTMLElement;
taskItemIdStr: string;
taskIdStr: string;
begin
rowEl := TJSHTMLElement(Event.currentTarget);
if not Assigned(rowEl) then
Exit;
taskItemIdStr := string(rowEl.getAttribute('data-task-item-id'));
taskIdStr := string(rowEl.getAttribute('data-task-id'));
FSelectedTaskItemId := StrToIntDef(taskItemIdStr, 0);
FSelectedTaskId := StrToIntDef(taskIdStr, 0);
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
ApplySelectedRowState;
end;
procedure TFTasksHTML.ApplySelectedRowState;
begin
asm
const selectedTaskItemId = this.FSelectedTaskItemId;
document.querySelectorAll('.task-row-selectable').forEach(function(row){
const rowTaskItemId = parseInt(row.getAttribute('data-task-item-id') || '0', 10);
if ((selectedTaskItemId > 0) && (rowTaskItemId === selectedTaskItemId))
row.classList.add('table-active');
else
row.classList.remove('table-active');
});
end;
end;
[async] function TFTasksHTML.AddTaskRow: Boolean;
var
response: TXDataClientResponse;
insertAfterItemNum: Integer;
newItemNum: Integer;
maxItemNum: Integer;
begin
Result := False;
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing task_id. Please reopen from emt3.');
Exit;
end;
insertAfterItemNum := 0;
maxItemNum := 0;
if xdwdsTasks.Active then
begin
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
if xdwdsTasksitemNum.AsInteger > maxItemNum then
maxItemNum := xdwdsTasksitemNum.AsInteger;
if xdwdsTaskstaskItemId.AsInteger = FSelectedTaskItemId then
insertAfterItemNum := xdwdsTasksitemNum.AsInteger;
xdwdsTasks.Next;
end;
end;
if insertAfterItemNum > 0 then
newItemNum := insertAfterItemNum + 1
else
newItemNum := maxItemNum + 1;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.AddTaskRow', [FTaskId, insertAfterItemNum]
));
console.log('AddTaskRow response=' + string(TJSJSON.stringify(response.Result)));
FPendingFocusItemNum := newItemNum;
FPendingFocusTaskField := 'application';
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('AddTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
procedure TFTasksHTML.btnReloadClick(Sender: TObject);
begin
if FTaskId = '' then
begin
Utils.ShowErrorModal('Missing Task Id. Update url params or resend from emT3.');
Exit;
end;
LoadTasks(FTaskId);
end;
procedure TFTasksHTML.EnableAutoGrowTextAreas;
begin
asm
(function(){
const host = document.getElementById('tasks_table_host');
if(!host) return;
host.querySelectorAll('textarea.cell-textarea').forEach(ta => {
const fit = () => { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; };
fit();
ta.addEventListener('input', fit);
});
})();
end;
end;
procedure TFTasksHTML.SetTotalRowsLabel(ARowCount: Integer);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_total_rows'));
if Assigned(el) then
el.innerText := 'Total Rows: ' + IntToStr(ARowCount);
end;
procedure TFTasksHTML.SetTaskLabel(const ATitle: string);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_project_name'));
if Assigned(el) then
el.innerText := ATitle;
end;
[async] procedure TFTasksHTML.LoadTasks(const ATaskId: string);
var
response: TXDataClientResponse;
resultObj, taskObj: TJSObject;
itemsArray: TJSArray;
titleText: string;
rowCount: Integer;
begin
console.log('IApiService.GetTaskItems called with task_id: ' + ATaskId);
console.log('Load Tasks Fired');
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.GetTaskItems', [ATaskId]
));
except
on E: EXDataClientRequestException do
begin
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
Exit;
end;
end;
if not Assigned(response.Result) then
Exit;
resultObj := TJSObject(response.Result);
taskObj := TJSObject(resultObj['task']);
if Assigned(taskObj) then
titleText := string(taskObj['title'])
else
titleText := 'Task - ' + ATaskId;
SetTaskLabel(titleText);
rowCount := StrToIntDef(string(resultObj['count']), 0);
SetTotalRowsLabel(rowCount);
FReportedByOptions := ExtractOptionNames(TJSArray(resultObj['reportedByOptions']));
FAssignedToOptions := ExtractOptionNames(TJSArray(resultObj['assignedToOptions']));
FStatusOptions := ExtractCodeDescs(TJSArray(resultObj['statusOptions']));
itemsArray := TJSArray(resultObj['items']);
if not Assigned(itemsArray) then
itemsArray := TJSArray.new;
xdwdsTasks.Close;
xdwdsTasks.SetJsonData(itemsArray);
xdwdsTasks.Open;
btnAddRow.Enabled := True;
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
RenderTable;
finally
Utils.HideSpinner('spinner');
end;
end;
procedure TFTasksHTML.RenderTable;
var
host: TJSHTMLElement;
html: string;
rowIdx: Integer;
function Th(const s: string): string;
begin
Result := '<th scope="col">' + s + '</th>';
end;
function ThBlank: string;
begin
Result := '<th scope="col">&nbsp;</th>';
end;
function TdNowrap(const s: string): string;
begin
Result := '<td class="align-top nowrap-cell">' + s + '</td>';
end;
function TdWrap(const s: string): string;
begin
Result := '<td class="align-top wrap-cell">' + s + '</td>';
end;
function TextInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
var
w: string;
begin
w := '';
if MinWidth > 0 then
w := ' style="min-width: ' + IntToStr(MinWidth) + 'px;"';
Result :=
'<input class="form-control form-control-sm cell-input task-editor w-100" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(Value) + '"' + w + '>';
end;
function TextArea(const FieldName, Value: string; const AIdx: Integer): string;
begin
Result :=
'<textarea class="form-control form-control-sm cell-textarea task-editor w-100" ' +
'style="height:31px; min-height:31px; overflow:hidden; resize:none;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'rows="1">' + HtmlEncode(Value) + '</textarea>';
end;
function ItemNumInput(const Value: Integer; const AIdx: Integer): string;
begin
Result :=
'<input type="number" min="1" class="form-control form-control-sm task-editor text-center px-1" ' +
'style="width: 30px;" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="itemNum" ' +
'value="' + IntToStr(Value) + '">';
end;
function DateInput(const FieldName, Value: string; const AIdx: Integer; const MinWidth: Integer = 0): string;
var
w: string;
begin
w := '';
if MinWidth > 0 then
w := ' style="min-width: ' + IntToStr(MinWidth) + 'px;"';
Result :=
'<input type="date" class="form-control form-control-sm cell-input task-editor w-100" ' +
'data-idx="' + IntToStr(AIdx) + '" data-field="' + FieldName + '" ' +
'value="' + HtmlEncode(Value) + '"' + w + '>';
end;
function SelectList(const FieldName, Current: string; const AIdx: Integer; const Items: TJSArray): string;
var
i: Integer;
itemText: string;
triggerId: string;
begin
triggerId := 'task_dd_' + FieldName + '_' + IntToStr(AIdx);
Result :=
'<div class="dropdown w-100">' +
'<button id="' + triggerId + '" class="btn btn-sm btn-light border w-100 d-flex justify-content-between align-items-center text-start task-dd-toggle" ' +
'type="button" data-bs-toggle="dropdown" aria-expanded="false">' +
'<span class="task-dd-label text-truncate">' + HtmlEncode(Current) + '</span>' +
'<span class="dropdown-toggle dropdown-toggle-split border-0 ms-2"></span>' +
'</button>' +
'<div class="dropdown-menu w-100 p-0 overflow-hidden">';
Result := Result +
'<button type="button" class="dropdown-item task-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value=""></button>';
if Assigned(Items) then
for i := 0 to Items.length - 1 do
begin
itemText := string(Items[i]);
Result := Result +
'<button type="button" class="dropdown-item task-dd-item" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-value="' + HtmlEncode(itemText) + '" ' +
'data-trigger-id="' + triggerId + '">' + HtmlEncode(itemText) + '</button>';
end;
Result := Result +
'<div class="dropdown-divider my-1"></div>' +
'<div class="px-2 py-1 text-end">' +
'<button type="button" class="btn btn-link btn-sm p-0 text-body task-dd-edit-btn" ' +
'data-idx="' + IntToStr(AIdx) + '" ' +
'data-field="' + FieldName + '" ' +
'data-trigger-id="' + triggerId + '">' +
'<i class="fas fa-pencil-alt"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
end;
function StatusSelect(const Current: string; const AIdx: Integer): string;
var
i: Integer;
statusText: string;
sel: string;
begin
Result :=
'<select class="form-select form-select-sm task-select" data-idx="' + IntToStr(AIdx) + '" data-field="status">';
Result := Result + '<option value=""></option>'; //Note: This adds the blank option on top
if Assigned(FStatusOptions) then
for i := 0 to FStatusOptions.length - 1 do
begin
statusText := string(FStatusOptions[i]);
sel := '';
if SameText(Current, statusText) then
sel := ' selected';
Result := Result + '<option value="' + HtmlEncode(statusText) + '"' + sel + '>' + HtmlEncode(statusText) + '</option>';
end;
Result := Result + '</select>';
end;
begin
host := TJSHTMLElement(document.getElementById('tasks_table_host'));
if not Assigned(host) then
Exit;
html :=
'<div class="tasks-vscroll">' +
'<div class="tasks-hscroll">' +
'<table class="table table-sm table-bordered align-middle mb-0" style="min-width: 2000px;">' +
'<colgroup>' +
'<col style="width:40px">' + // Item Num
'<col style="width:200px">' + // App
'<col style="width:90px">' + // Version
'<col style="width:120px">' + // Date
'<col style="width:120px">' + // Reported
'<col style="width:120px">' + // Assigned
'<col style="width:195px">' + // Status
'<col style="width:140px">' + // Status Date
'<col style="width:160px">' + // Form
'<col style="width:520px">' + // Issue
'<col style="width:520px">' + // Notes
'</colgroup>' +
'<thead><tr>' +
ThBlank +
Th('App') +
Th('Version') +
Th('Date') +
Th('Reported') +
Th('Assigned') +
Th('Status') +
Th('Status Date') +
Th('Form') +
Th('Issue') +
Th('Notes') +
'</tr></thead><tbody>';
rowIdx := 0;
xdwdsTasks.First;
while not xdwdsTasks.Eof do
begin
html := html +
'<tr class="task-row-selectable" data-task-item-id="' + IntToStr(xdwdsTaskstaskItemId.AsInteger) + '" data-task-id="' + xdwdsTaskstaskId.AsString + '" data-item-num="' + IntToStr(xdwdsTasksitemNum.AsInteger) + '">' +
TdNowrap(ItemNumInput(xdwdsTasksitemNum.AsInteger, rowIdx)) +
TdNowrap(TextInput('application', xdwdsTasksapplication.AsString, rowIdx, 180)) +
TdNowrap(TextInput('version', xdwdsTasksversion.AsString, rowIdx, 80)) +
TdNowrap(DateInput('taskDate', xdwdsTaskstaskDate.AsString, rowIdx, 110)) +
TdNowrap(SelectList('reportedBy', xdwdsTasksreportedBy.AsString, rowIdx, FReportedByOptions)) +
TdNowrap(SelectList('assignedTo', xdwdsTasksassignedTo.AsString, rowIdx, FAssignedToOptions)) +
TdNowrap(StatusSelect(xdwdsTasksstatus.AsString, rowIdx)) +
TdNowrap(DateInput('statusDate', xdwdsTasksstatusDate.AsString, rowIdx, 110)) +
TdNowrap(TextInput('formSection', xdwdsTasksformSection.AsString, rowIdx, 160)) +
TdWrap(TextArea('issue', xdwdsTasksissue.AsString, rowIdx)) +
TdWrap(TextArea('notes', xdwdsTasksnotes.AsString, rowIdx)) +
'</tr>';
xdwdsTasks.Next;
Inc(rowIdx);
end;
html := html + '</tbody></table></div></div>';
host.innerHTML := html;
BindTableEditors;
EnableAutoGrowTextAreas;
EnableColumnResize;
RestoreTableScroll;
ApplyPendingFocus;
ApplyPendingDeleteFocus;
end;
procedure TFTasksHTML.EnableColumnResize;
begin
asm
(function(){
const host = document.getElementById('tasks_table_host');
if(!host) return;
const table = host.querySelector('table');
if(!table) return;
const ths = table.querySelectorAll('thead th');
ths.forEach(th => {
th.classList.add('th-resize');
if(th.querySelector('.th-resize-handle')) return;
const handle = document.createElement('div');
handle.className = 'th-resize-handle';
th.appendChild(handle);
handle.addEventListener('mousedown', function(e){
e.preventDefault();
const startX = e.clientX;
const startW = th.getBoundingClientRect().width;
function onMove(ev){
const w = Math.max(40, startW + (ev.clientX - startX));
th.style.width = w + 'px';
}
function onUp(){
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
})();
end;
end;
procedure TFTasksHTML.EditorBlur(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName: string;
newItemNum: Integer;
begin
el := TJSHTMLElement(Event.target);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
if string(el.getAttribute('data-unsaved-data')) <> '1' then
begin
console.log('EditorBlur: skip (not unsaved) idx=' + idxStr + ' field=' + fieldName);
Exit;
end;
el.removeAttribute('data-unsaved-data');
idx := StrToIntDef(idxStr, -1);
if idx < 0 then
Exit;
if SameText(fieldName, 'itemNum') then
begin
newItemNum := StrToIntDef(string(TJSObject(el)['value']), 0);
console.log('EditorBlur: MOVE idx=' + IntToStr(idx) + ' newItemNum=' + IntToStr(newItemNum));
MoveTaskRow(idx, newItemNum);
Exit;
end;
console.log('EditorBlur: SAVE idx=' + IntToStr(idx) + ' field=' + fieldName);
SaveRow(idx);
end;
[async] procedure TFTasksHTML.MoveTaskRow(AIndex: Integer; const newItemNum: Integer);
var
response: TXDataClientResponse;
movedTaskItemId: Integer;
begin
if not xdwdsTasks.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTasks.Eof then
Exit;
movedTaskItemId := xdwdsTaskstaskItemId.AsInteger;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.MoveTaskRow',
[
StrToIntDef(xdwdsTaskstaskId.AsString, 0),
movedTaskItemId,
newItemNum
]
));
console.log('MoveTaskRow: response=' + string(TJSJSON.stringify(response.Result)));
FPendingFocusTaskItemId := movedTaskItemId;
FPendingFocusField := 'application';
CaptureTableScroll;
LoadTasks(FTaskId);
except
on E: EXDataClientRequestException do
begin
console.log('MoveTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
LoadTasks(FTaskId);
end;
end;
end;
[async] procedure TFTasksHTML.SaveRow(AIndex: Integer);
const
// Note: Use this to manipulate saving to the server or not for testing
ENABLE_SERVER_SAVE = True;
var
response: TXDataClientResponse;
payload: TJSObject;
payloadJson: string;
begin
if not xdwdsTasks.Active then
Exit;
GotoRowIndex(AIndex);
if xdwdsTasks.Eof then
Exit;
payload := TJSObject.new;
payload['taskItemId'] := xdwdsTaskstaskItemId.AsInteger;
payload['taskId'] := xdwdsTaskstaskId.AsString;
payload['application'] := xdwdsTasksapplication.AsString;
payload['version'] := xdwdsTasksversion.AsString;
payload['taskDate'] := xdwdsTaskstaskDate.AsString;
payload['reportedBy'] := xdwdsTasksreportedBy.AsString;
payload['assignedTo'] := xdwdsTasksassignedTo.AsString;
payload['status'] := xdwdsTasksstatus.AsString;
payload['statusDate'] := xdwdsTasksstatusDate.AsString;
payload['formSection'] := xdwdsTasksformSection.AsString;
payload['issue'] := xdwdsTasksissue.AsString;
payload['notes'] := xdwdsTasksnotes.AsString;
payloadJson := string(TJSJSON.stringify(payload));
console.log('SaveRow: idx=' + IntToStr(AIndex) + ' payload=' + payloadJson);
if not ENABLE_SERVER_SAVE then
Exit;
try
response := await(xdwcTasks.RawInvokeAsync('IApiService.SaveTaskRow', [payload]));
console.log('SaveRow: response=' + string(TJSJSON.stringify(response.Result)));
except
on E: EXDataClientRequestException do
begin
console.log('SaveRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
[async] function TFTasksHTML.DeleteTaskRow: Boolean;
var
response: TXDataClientResponse;
begin
Result := False;
if (FSelectedTaskId <= 0) or (FSelectedTaskItemId <= 0) then
Exit;
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.DeleteTaskRow',
[FSelectedTaskId, FSelectedTaskItemId]
));
console.log('DeleteTaskRow response=' + string(TJSJSON.stringify(response.Result)));
Result := True;
except
on E: EXDataClientRequestException do
begin
console.log('DeleteTaskRow ERROR: ' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
end;
end;
function TFTasksHTML.ExtractOptionNames(const SourceArray: TJSArray): TJSArray;
var
i: Integer;
optionObj: TJSObject;
begin
Result := TJSArray.new;
if not Assigned(SourceArray) then
Exit;
for i := 0 to SourceArray.length - 1 do
begin
optionObj := TJSObject(SourceArray[i]);
if Assigned(optionObj) then
Result.push(string(optionObj['name']));
end;
end;
function TFTasksHTML.ExtractCodeDescs(const SourceArray: TJSArray): TJSArray;
var
i: Integer;
optionObj: TJSObject;
begin
Result := TJSArray.new;
if not Assigned(SourceArray) then
Exit;
for i := 0 to SourceArray.length - 1 do
begin
optionObj := TJSObject(SourceArray[i]);
if Assigned(optionObj) then
Result.push(string(optionObj['codeDesc']));
end;
end;
function TFTasksHTML.GetOptionsForField(const AFieldName: string): TJSArray;
begin
if SameText(AFieldName, 'reportedBy') then
Result := FReportedByOptions
else if SameText(AFieldName, 'assignedTo') then
Result := FAssignedToOptions
else
Result := nil;
end;
procedure TFTasksHTML.FocusTrigger(const ATriggerId: string);
var
el: TJSHTMLElement;
begin
if ATriggerId = '' then
Exit;
el := TJSHTMLElement(document.getElementById(ATriggerId));
if Assigned(el) then
el.focus;
end;
procedure TFTasksHTML.DropdownItemClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName, newVal, triggerId: string;
begin
if not xdwdsTasks.Active then
Exit;
Event.preventDefault;
el := TJSHTMLElement(Event.currentTarget);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
newVal := string(el.getAttribute('data-value'));
triggerId := string(el.getAttribute('data-trigger-id'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
GotoRowIndex(idx);
if xdwdsTasks.Eof then
Exit;
xdwdsTasks.Edit;
xdwdsTasks.FieldByName(fieldName).AsString := newVal;
xdwdsTasks.Post;
if triggerId <> '' then
begin
asm
var btn = document.getElementById(triggerId);
if (btn) {
var labelEl = btn.querySelector('.task-dd-label');
if (labelEl) {
labelEl.textContent = newVal;
}
}
end;
end;
SaveRow(idx);
end;
procedure TFTasksHTML.DropdownEditClick(Event: TJSEvent);
var
el: TJSHTMLElement;
idx: Integer;
idxStr, fieldName, triggerId: string;
begin
Event.preventDefault;
Event.stopPropagation;
el := TJSHTMLElement(Event.currentTarget);
idxStr := string(el.getAttribute('data-idx'));
fieldName := string(el.getAttribute('data-field'));
triggerId := string(el.getAttribute('data-trigger-id'));
idx := StrToIntDef(idxStr, -1);
if (idx < 0) or (fieldName = '') then
Exit;
FNameManager.OpenManager(fieldName, idx, triggerId);
end;
procedure TFTasksHTML.ApplyPendingFocus;
var
el: TJSHTMLElement;
selector: string;
begin
if (FPendingFocusTaskItemId <= 0) or (FPendingFocusField = '') then
Exit;
selector :=
'[data-task-item-id="' + IntToStr(FPendingFocusTaskItemId) + '"] ' +
'[data-field="' + FPendingFocusField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusTaskItemId := 0;
FPendingFocusField := '';
end;
procedure TFTasksHTML.ApplyPendingDeleteFocus;
var
el: TJSHTMLElement;
rowEl: TJSHTMLElement;
selector: string;
taskItemIdStr: string;
taskIdStr: string;
begin
if (FPendingFocusItemNum <= 0) or (FPendingFocusTaskField = '') then
Exit;
selector :=
'tr[data-item-num="' + IntToStr(FPendingFocusItemNum) + '"] ' +
'[data-field="' + FPendingFocusTaskField + '"]';
el := TJSHTMLElement(document.querySelector(selector));
if Assigned(el) then
begin
rowEl := TJSHTMLElement(el.closest('tr'));
if Assigned(rowEl) then
begin
taskItemIdStr := string(rowEl.getAttribute('data-task-item-id'));
taskIdStr := string(rowEl.getAttribute('data-task-id'));
FSelectedTaskItemId := StrToIntDef(taskItemIdStr, 0);
FSelectedTaskId := StrToIntDef(taskIdStr, 0);
btnDeleteRow.Enabled := FSelectedTaskItemId > 0;
ApplySelectedRowState;
end;
asm
el.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' });
el.focus();
end;
end;
FPendingFocusItemNum := 0;
FPendingFocusTaskField := '';
end;
procedure TFTasksHTML.CaptureTableScroll;
begin
asm
const vscroll = document.querySelector('.tasks-vscroll');
const hscroll = document.querySelector('.tasks-hscroll');
this.FPendingScrollTop = vscroll ? vscroll.scrollTop : 0;
this.FPendingScrollLeft = hscroll ? hscroll.scrollLeft : 0;
end;
end;
procedure TFTasksHTML.RestoreTableScroll;
begin
asm
const vscroll = document.querySelector('.tasks-vscroll');
const hscroll = document.querySelector('.tasks-hscroll');
if (vscroll) vscroll.scrollTop = this.FPendingScrollTop || 0;
if (hscroll) hscroll.scrollLeft = this.FPendingScrollLeft || 0;
end;
end;
end.
{
"AuthUrl" : "http://localhost:2001/emsys/template/auth/",
"ApiUrl" : "http://localhost:2001/emsys/template/api/"
}
{
"AuthUrl" : "http://localhost:2001/emsys/emt3/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emt3/api/"
}
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
.is-invalid .form-check-label {
color: #dc3545 !important;
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
@keyframes slideInLeft {
from {
transform: translateX(-120%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.slide-in {
animation: slideInLeft 0.4s ease-out forwards;
}
#spinner {
position: fixed !important;
z-index: 9999 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* This hides the up and down arrows on the item_num box, comment or remove it to add them back */
input[data-field="itemNum"]::-webkit-outer-spin-button,
input[data-field="itemNum"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[data-field="itemNum"] {
-moz-appearance: textfield;
appearance: textfield;
}
.tasks-vscroll {
height: 100%;
overflow: auto;
}
.tasks-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.tasks-vscroll thead th.th-resize {
z-index: 3;
}
span.card {
border: none;
}
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #204d74;
margin: -5px 0 0 -5px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
program emT3WebApp;
uses
Vcl.Forms,
XData.Web.Connection,
WEBLib.Dialogs,
Auth.Service in 'Auth.Service.pas',
App.Types in 'App.Types.pas',
ConnectionModule in 'ConnectionModule.pas' {DMConnection: TWebDataModule},
App.Config in 'App.Config.pas',
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
Utils in 'Utils.pas',
View.Test in 'View.Test.pas' {FTest: TWebForm} {*.html},
View.TasksHTML in 'View.TasksHTML.pas' {FTasksHTML: TWebForm} {*.html},
uNameManager in 'uNameManager.pas';
{$R *.res}
procedure DoLogout(AMsg: string = ''); forward;
procedure DisplayMainView;
procedure ConnectProc;
begin
TFViewMain.Display(@DoLogout);
end;
begin
if not DMConnection.ApiConnection.Connected then
DMConnection.ApiConnection.Open(@ConnectProc)
else
ConnectProc;
end;
procedure Login(userId: string; taskId: string; urlCode: string);
procedure LoginSuccess;
begin
DisplayMainView;
end;
procedure LoginError(AMsg: string);
begin
ShowMessage('Login Error: ' + AMsg);
end;
begin
AuthService.Login( userId, taskId, urlCode,
@LoginSuccess,
@LoginError
);
end;
procedure DoLogin();
var
userIdParam: string;
taskIdParam: string;
codeParam: string;
begin
userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id'];
codeParam := Application.Parameters.Values['url_code'];
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
Login( userIdParam, taskIdParam, codeParam );
end;
procedure DoLogout(AMsg: string);
begin
AuthService.Logout;
ShowMessage('Logout successful: ' + AMsg);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
ShowMessage('UnauthorizedAccessProc: ' + AMessage);
end;
procedure StartApplication;
var
ClientVer: string;
begin
ClientVer := TDMConnection.clientVersion;
DMConnection.InitApp(
procedure
begin
DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
begin
if Success then
begin
DoLogin();
end
else
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "500px";
dlg.style.width = "90%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>kgOrders web app</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + ErrorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='refreshBtn' class='btn btn-primary'>Reload</button></div>";
document.body.appendChild(dlg);
dlg.showModal();
document.getElementById("refreshBtn").addEventListener("click", function () {
var base = location.origin + location.pathname;
location.replace(base + "?ver=" + ClientVer + "&r=" + Date.now() + location.hash);
});
end;
end;
end);
end,
@UnauthorizedAccessProc
);
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection);
StartApplication;
Application.Run;
end.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{DB6F5DBF-7E4B-45DA-AFFA-6C8DF15BA740}</ProjectGuid>
<ProjectVersion>20.4</ProjectVersion>
<FrameworkType>VCL</FrameworkType>
<MainSource>emT3WebApp.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Application</AppType>
<ProjectName Condition="'$(ProjectName)'==''">emT3WebApp</ProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
<Base_Win64>true</Base_Win64>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
<Cfg_1_Win32>true</Cfg_1_Win32>
<CfgParent>Cfg_1</CfgParent>
<Cfg_1>true</Cfg_1>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win32)'!=''">
<Cfg_2_Win32>true</Cfg_2_Win32>
<CfgParent>Cfg_2</CfgParent>
<Cfg_2>true</Cfg_2>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_DcuOutput>.\$(Platform)\$(Config)</DCC_DcuOutput>
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
<DCC_E>false</DCC_E>
<DCC_N>false</DCC_N>
<DCC_S>false</DCC_S>
<DCC_F>false</DCC_F>
<DCC_K>false</DCC_K>
<DCC_UsePackage>RESTComponents;emsclientfiredac;DataSnapFireDAC;FireDACIBDriver;xdata;emsclient;FireDACCommon;RESTBackendComponents;soapserver;CloudService;FireDACCommonDriver;inet;FireDAC;FireDACSqliteDriver;soaprtl;soapmidas;aurelius;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)</DCC_Namespace>
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
<Icns_MainIcns>$(BDS)\bin\delphi_PROJECTICNS.icns</Icns_MainIcns>
<SanitizedProjectName>emT3WebApp</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.802;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;LastCompiledTime=2018/07/25 12:57:53</VerInfo_Keys>
<TMSWebHTMLFile>index.html</TMSWebHTMLFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<DCC_UsePackage>DBXSqliteDriver;DataSnapServerMidas;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;svnui;tethering;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;svn;Intraweb;DBXOracleDriver;inetdb;FmxTeeUI;emsedge;fmx;fmxdae;vclib;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;DataSnapCommon;DataSnapConnectors;VCLRESTComponents;vclie;TMSWEBCorePkgDXE11;bindengine;DBXMySQLDriver;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;DataSnapClient;bindcompdbx;IndyIPCommon;vcl;DBXSybaseASEDriver;IndyIPServer;IndySystem;FireDACDb2Driver;TMSWEBCorePkgLibDXE11;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;TeeDB;emshosting;FireDACPgDriver;ibmonitor;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;FMXTee;DbxCommonDriver;ibxpress;Tee;DataSnapServer;xmlrtl;DataSnapNativeClient;fmxobj;vclwinx;ibxbindings;rtl;FireDACDSDriver;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;TeeUI;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;inetdbxpress;FireDACMongoDBDriver;IndyProtocols;fmxase;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
<BT_BuildType>Debug</BT_BuildType>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.5.0;Comments=</VerInfo_Keys>
<VerInfo_Locale>1033</VerInfo_Locale>
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>5</VerInfo_Release>
<AppDPIAwarenessMode>none</AppDPIAwarenessMode>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<DCC_UsePackage>DBXSqliteDriver;DataSnapServerMidas;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;tethering;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;Intraweb;DBXOracleDriver;inetdb;FmxTeeUI;emsedge;fmx;fmxdae;vclib;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;DataSnapCommon;DataSnapConnectors;VCLRESTComponents;vclie;bindengine;DBXMySQLDriver;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;DataSnapClient;bindcompdbx;IndyIPCommon;vcl;DBXSybaseASEDriver;IndyIPServer;IndySystem;FireDACDb2Driver;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;TeeDB;emshosting;FireDACPgDriver;ibmonitor;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;FMXTee;DbxCommonDriver;ibxpress;Tee;DataSnapServer;xmlrtl;DataSnapNativeClient;fmxobj;vclwinx;ibxbindings;rtl;FireDACDSDriver;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;TeeUI;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;inetdbxpress;FireDACMongoDBDriver;IndyProtocols;fmxase;$(DCC_UsePackage)</DCC_UsePackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
<DCC_DebugDCUs>true</DCC_DebugDCUs>
<DCC_Optimize>false</DCC_Optimize>
<DCC_GenerateStackFrames>true</DCC_GenerateStackFrames>
<DCC_DebugInfoInExe>true</DCC_DebugInfoInExe>
<DCC_RemoteDebug>true</DCC_RemoteDebug>
<TMSWebDebugInfo>2</TMSWebDebugInfo>
<TMSWebDefines>DEBUG</TMSWebDefines>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
<DCC_RemoteDebug>false</DCC_RemoteDebug>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.8.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.8.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebBrowser>1</TMSWebBrowser>
<TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSWebOutputPath>..\emT3XDataServer\bin\static</TMSWebOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>0</DCC_DebugInformation>
<TMSWebOptimization>2</TMSWebOptimization>
<TMSWebDefines>RELEASE</TMSWebDefines>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;LastCompiledTime=2018/08/22 16:25:56</VerInfo_Keys>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<TMSWebBrowser>1</TMSWebBrowser>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="Auth.Service.pas"/>
<DCCReference Include="App.Types.pas"/>
<DCCReference Include="ConnectionModule.pas">
<Form>DMConnection</Form>
<DesignClass>TWebDataModule</DesignClass>
</DCCReference>
<DCCReference Include="App.Config.pas"/>
<DCCReference Include="View.Main.pas">
<Form>FViewMain</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="Utils.pas"/>
<DCCReference Include="View.Test.pas">
<Form>FTest</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uNameManager.pas"/>
<None Include="index.html"/>
<None Include="css\app.css"/>
<None Include="config\config.json"/>
<None Include="css\spinner.css"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType>Application</Borland.ProjectType>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">emT3WebApp.dpr</Source>
</Source>
<Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k370.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp370.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k370.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dclofficexp370.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
<Deployment Version="5">
<DeployFile LocalName="Win32\Debug\emT3WebApp.exe" Configuration="Debug" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>emT3WebApp.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win32\Debug\webCharms.exe" Configuration="Debug" Class="ProjectOutput"/>
<DeployFile LocalName="Win32\Debug\webKGOrders.exe" Configuration="Debug" Class="ProjectOutput"/>
<DeployFile LocalName="Win32\Release\emT3WebApp.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>emT3WebApp.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="config\config.json" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="config\config.json" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="config\config.json" Configuration="Release" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\app.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="css\app.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\app.css" Configuration="Release" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\spinner.css" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="css\spinner.css" Configuration="Release" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="index.html" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="index.html" Configuration="Release" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="template\bootstrap\bootstrap.min.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\bootstrap\bootstrap.min.js" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\bootstrap\dataTables.bootstrap.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\css\emsys.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\css\metisMenu.min.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\css\morris.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\css\sb-admin-2.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\font-awesome\font-awesome.min.css" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\font-awesome\fonts\fontawesome-webfont.ttf" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\font-awesome\fonts\fontawesome-webfont.woff2" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\font-awesome\fonts\fontawesome-webfont.woff" Configuration="Debug" Class="ProjectFile"/>
<DeployFile LocalName="template\jquery\jquery.min.js" Configuration="Debug" Class="ProjectFile"/>
<DeployClass Name="AdditionalDebugSymbols">
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidFileProvider">
<Platform Name="Android">
<RemoteDir>res\xml</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\xml</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeArmeabiFile">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeArmeabiv7aFile">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeMipsFile">
<Platform Name="Android">
<RemoteDir>library\lib\mips</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\mips</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidServiceOutput">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\arm64-v8a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidServiceOutput_Android32">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashImageDef">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashImageDefV21">
<Platform Name="Android">
<RemoteDir>res\drawable-anydpi-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-anydpi-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStyles">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStylesV21">
<Platform Name="Android">
<RemoteDir>res\values-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStylesV31">
<Platform Name="Android">
<RemoteDir>res\values-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStylesV35">
<Platform Name="Android">
<RemoteDir>res\values-v35</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values-v35</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_AdaptiveIcon">
<Platform Name="Android">
<RemoteDir>res\drawable-anydpi-v26</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-anydpi-v26</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_AdaptiveIconBackground">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_AdaptiveIconForeground">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_AdaptiveIconMonochrome">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_AdaptiveIconV33">
<Platform Name="Android">
<RemoteDir>res\drawable-anydpi-v33</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-anydpi-v33</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_Colors">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_ColorsDark">
<Platform Name="Android">
<RemoteDir>res\values-night-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values-night-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_DefaultAppIcon">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon144">
<Platform Name="Android">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon192">
<Platform Name="Android">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon36">
<Platform Name="Android">
<RemoteDir>res\drawable-ldpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-ldpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon48">
<Platform Name="Android">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon72">
<Platform Name="Android">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon96">
<Platform Name="Android">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon24">
<Platform Name="Android">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon36">
<Platform Name="Android">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon48">
<Platform Name="Android">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon72">
<Platform Name="Android">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon96">
<Platform Name="Android">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage426">
<Platform Name="Android">
<RemoteDir>res\drawable-small</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-small</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage470">
<Platform Name="Android">
<RemoteDir>res\drawable-normal</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-normal</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage640">
<Platform Name="Android">
<RemoteDir>res\drawable-large</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-large</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage960">
<Platform Name="Android">
<RemoteDir>res\drawable-xlarge</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xlarge</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_Strings">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_VectorizedNotificationIcon">
<Platform Name="Android">
<RemoteDir>res\drawable-anydpi-v24</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-anydpi-v24</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_VectorizedSplash">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_VectorizedSplashDark">
<Platform Name="Android">
<RemoteDir>res\drawable-night-anydpi-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-night-anydpi-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_VectorizedSplashV31">
<Platform Name="Android">
<RemoteDir>res\drawable-anydpi-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-anydpi-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_VectorizedSplashV31Dark">
<Platform Name="Android">
<RemoteDir>res\drawable-night-anydpi-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-night-anydpi-v31</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DebugSymbols">
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DependencyFramework">
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.framework</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.framework</Extensions>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.framework</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DependencyModule">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.dll;.bpl</Extensions>
</Platform>
</DeployClass>
<DeployClass Required="true" Name="DependencyPackage">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.bpl</Extensions>
</Platform>
</DeployClass>
<DeployClass Name="File">
<Platform Name="Android">
<Operation>0</Operation>
</Platform>
<Platform Name="Android64">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSDevice32">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>0</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\Resources\StartUp\</RemoteDir>
<Operation>0</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\Resources\StartUp\</RemoteDir>
<Operation>0</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\Resources\StartUp\</RemoteDir>
<Operation>0</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectAndroidManifest">
<Platform Name="Android">
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXDebug">
<Platform Name="OSX64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXEntitlements">
<Platform Name="OSX32">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXInfoPList">
<Platform Name="OSX32">
<RemoteDir>Contents</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXResource">
<Platform Name="OSX32">
<RemoteDir>Contents\Resources</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\Resources</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\Resources</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Required="true" Name="ProjectOutput">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\arm64-v8a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>1</Operation>
</Platform>
<Platform Name="Linux64">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSXARM64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOutput_Android32">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectUWPManifest">
<Platform Name="Win32">
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<Operation>1</Operation>
</Platform>
<Platform Name="Win64x">
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceDebug">
<Platform Name="iOSDevice32">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSEntitlements">
<Platform Name="iOSDevice32">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSInfoPList">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSLaunchScreen">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen</RemoteDir>
<Operation>64</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen</RemoteDir>
<Operation>64</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSResource">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo150">
<Platform Name="Win32">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo44">
<Platform Name="Win32">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="WinARM64EC">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iOS_AppStore1024">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_AppIcon152">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_AppIcon167">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_LaunchDark2x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Notification40">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Setting58">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_SpotLight80">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_AppIcon120">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_AppIcon180">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch2x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch3x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_LaunchDark2x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_LaunchDark3x">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Notification40">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Notification60">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Setting58">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Setting87">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Spotlight120">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Spotlight80">
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimARM64">
<RemoteDir>..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<ProjectRoot Platform="Android" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Android64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="iOSDevice32" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="iOSDevice64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="iOSSimARM64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="iOSSimulator" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Linux64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="OSX32" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="OSX64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="OSXARM64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Win32" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win64x" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="WinARM64EC" Name="$(PROJECTNAME)"/>
</Deployment>
<Platforms>
<Platform value="Win32">True</Platform>
<Platform value="Win64">False</Platform>
</Platforms>
<ModelSupport>False</ModelSupport>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
<Import Project="$(MSBuildProjectName).deployproj" Condition="Exists('$(MSBuildProjectName).deployproj')"/>
</Project>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="data:;base64,=" rel="icon"/>
<title>emT3Web</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.3.1/css/flag-icon.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="$(ProjectName).js"></script>
</head>
<body>
<noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script>
</body>
</html>
......@@ -91,7 +91,8 @@ begin
begin
Result.AddPair('error',
'Your browser is running an old version of the app.' + sLineBreak +
'Please click below to reload.');
'Please click button to reload.' + sLineBreak + sLineBreak +
'if Error continues, then you need to Empty Cache and Hard Reload!');
end;
finally
iniFile.Free;
......@@ -99,20 +100,48 @@ begin
end;
function TAuthService.CheckUrlLogin(const userId, taskId, urlCode: string): Integer;
var
sql: string;
timeNow: TDateTime;
timeDiff: integer;
begin
Result := 0;
Logger.Log(3, 'TAuthService.CheckUrlLogin(const userId, taskId, urlCode: string): Integer' );
sql := 'select u.USER_ID, u.USER_NAME, u.NAME, u.STATUS, u.EMAIL, u.ACCESS_LEVEL, ';
sql := sql + 'u.TASK_RIGHTS, u.PERSPECTIVE_ID, u.LAST_NAME, u.FIRST_NAME, w.URL_TIME, w.URL_TIME_EXP ';
sql := sql + 'from web_tasks_url w ';
sql := sql + 'join users u on u.USER_ID = w.USER_ID ';
sql := sql + 'where w.USER_ID = :USER_ID and w.TASK_ID = :TASK_ID and w.URL_CODE = :URL_CODE ';
//sql := sql + 'and TIMESTAMPDIFF(SECOND, w.URL_TIME, NOW()) between 0 and w.URL_TIME_EXP';
authDB.uqWebTasksUrl.Close;
authDB.uqWebTasksUrl.SQL.Text := sql;
authDB.uqWebTasksUrl.ParamByName('USER_ID').AsString := userId;
authDB.uqWebTasksUrl.ParamByName('TASK_ID').AsString := taskId;
authDB.uqWebTasksUrl.ParamByName('URL_CODE').AsString := urlCode;
authDB.uqWebTasksUrl.Open;
if authDB.uqWebTasksUrl.IsEmpty then
begin
Logger.Log(3, '--URL Login failed 0: authDB.uqWebTasksUrl.IsEmpty');
Result := 0;
Exit;
end;
if authDB.uqWebTasksUrl.FieldByName('STATUS').AsString <> 'ACTIVE' then
if authDB.uqWebTasksUrlSTATUS.AsString <> 'ACTIVE' then
begin
Logger.Log(3, '--URL Login failed 1: authDB.uqWebTasksUrlSTATUS.AsString <> ACTIVE');
Result := 1;
Exit;
end;
timeNow := Now;
timeDiff := SecondsBetween( timeNow, authDB.uqWebTasksUrlURL_TIME.AsDateTime );
if timeDiff > authDB.uqWebTasksUrlURL_TIME_EXP.AsInteger then
begin
Logger.Log( 3, '--timeNow: ' + timeNow.ToString + ' -urlTime: ' + authDB.uqWebTasksUrlURL_TIME.AsString );
Logger.Log( 3, '--timeDiff: ' + IntToStr(timeDiff) + ' -timeExp (authDB.uqWebTasksUrlURL_TIME_EXP.AsInteger): ' + authDB.uqWebTasksUrlURL_TIME_EXP.AsString );
Logger.Log( 3, '--URL Login failed 2: timeDiff > timeExp' );
Result := 2;
Exit;
end;
......@@ -161,21 +190,27 @@ begin
if userState = 0 then
begin
Logger.Log(2, 'Login Error: Invalid code or expired link');
raise EXDataHttpUnauthorized.Create('Invalid code or expired link');
Logger.Log(2, 'Login Error: Invalid code');
raise EXDataHttpUnauthorized.Create('Invalid code');
end;
if userState = 2 then
if userState = 1 then
begin
Logger.Log(2, 'Login Error: User not active!');
raise EXDataHttpUnauthorized.Create('User not active!');
end;
if userState = 2 then
begin
Logger.Log(2, 'Login Error: Expired link');
raise EXDataHttpUnauthorized.Create('Expired link');
end;
jwt := TJWT.Create;
try
jwt.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
jwt.Claims.IssuedAt := Now;
jwt.Claims.Expiration := IncHour(Now, 24);
jwt.Claims.Expiration := IncHour(Now, 12);
jwt.Claims.SetClaimOfType<string>('user_id', Self.userId);
jwt.Claims.SetClaimOfType<string>('user_name', userName);
......
......@@ -3,7 +3,7 @@ unit Common.Config;
interface
const
defaultServerUrl = 'http://localhost:2004/emsys/emt3';
defaultServerUrl = 'http://localhost:2001/emsys/emt3';
type
TServerConfig = class
......@@ -78,10 +78,16 @@ begin
adminPassword := 'whatisthisusedfor';
jwtTokenSecret := 'super_secret0123super_secret4567';
webAppFolder := 'static';
reportsFolder := 'static/';
reportsFolder := 'static\reports\';
ServerConfigStr := Bcl.Json.TJson.Serialize(ServerConfig);
Logger.Log(1, '--ServerConfigSerialize: ' + ServerConfigStr);
Logger.Log(1, '--TServerConfig.Create - end');
end;
initialization
ServerConfig := TServerConfig.Create;
finalization
ServerConfig.Free;
end.
unit Common.Ini;
interface
uses
System.SysUtils, System.IniFiles, Vcl.Forms;
type
TIniEntries = class
private
// [Settings]
FMemoLogLevel: Integer;
FFileLogLevel: Integer;
FLogFileNum: Integer;
FJWTSecret: string;
// [Database]
FDBServer: string;
FDBPort: Integer;
FDBDatabase: string;
FDBUsername: string;
FDBPassword: string;
public
constructor Create;
// Properties
property memoLogLevel: Integer read FMemoLogLevel;
property fileLogLevel: Integer read FFileLogLevel;
property logFileNum: Integer read FLogFileNum;
property dbServer: string read FDBServer;
property dbPort: Integer read FDBPort;
property dbDatabase: string read FDBDatabase;
property dbUsername: string read FDBUsername;
property dbPassword: string read FDBPassword;
end;
procedure LoadIniEntries;
var
IniEntries: TIniEntries;
implementation
procedure LoadIniEntries;
begin
if Assigned(IniEntries) then
IniEntries.Free;
IniEntries := TIniEntries.Create;
end;
{ TIniEntries }
constructor TIniEntries.Create;
var
iniFile: TIniFile;
begin
iniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
// [Settings]
FMemoLogLevel := iniFile.ReadInteger('Settings', 'ConsoleLogLevel', 3);
FFileLogLevel := iniFile.ReadInteger('Settings', 'FileLogLevel', 3);
FLogFileNum := iniFile.ReadInteger('Settings', 'LogFileNum', 0);
Inc(FLogFileNum);
iniFile.WriteInteger( 'Settings', 'LogFileNum', FlogFileNum );
// [Database]
FDBServer := iniFile.ReadString('Database', 'Server', '');
FDBPort := iniFile.ReadInteger('Database', 'Port', 0);
FDBDatabase := iniFile.ReadString('Database', 'Database', 'etask');
FDBUsername := iniFile.ReadString('Database', 'Username', 'root');
FDBPassword := iniFile.ReadString('Database', 'Password', 'emsys01');
finally
iniFile.Free;
end;
end;
end.
......@@ -10,7 +10,6 @@ object FMain: TFMain
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OnClose = FormClose
DesignSize = (
773
597)
......
......@@ -20,16 +20,14 @@ type
initTimer: TTimer;
btnAuthSwaggerUI: TButton;
ExeInfo1: TExeInfo;
procedure btnAuthSwaggerUIClick(Sender: TObject);
procedure btnApiSwaggerUIClick(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure ContactFormData(AText: String);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure initTimerTimer(Sender: TObject);
procedure btnAuthSwaggerUIClick(Sender: TObject);
strict private
procedure StartServers;
procedure UpdateGUI;
end;
var
......@@ -39,6 +37,7 @@ implementation
uses
Common.Logging,
Common.Ini,
Common.Config,
Sparkle.Utils,
Api.Database;
......@@ -61,13 +60,11 @@ begin
end);
end;
procedure TFMain.btnExitClick(Sender: TObject);
begin
Close;
end;
procedure TFMain.btnAuthSwaggerUIClick(Sender: TObject);
begin
ShellExecute(Handle, 'open', PChar(TSparkleUtils.CombineUrlFast(AuthServerModule.XDataServer.BaseUrl, 'swaggerui')), nil, nil, SW_SHOWNORMAL);
......@@ -82,7 +79,6 @@ procedure TFMain.initTimerTimer(Sender: TObject);
begin
initTimer.Enabled := False;
Caption := Caption + ' ver ' + ExeInfo1.FileVersion;
ServerConfig := TServerConfig.Create;
LoadServerConfig;
StartServers;
end;
......@@ -91,69 +87,66 @@ procedure TFMain.StartServers;
var
iniFile: TIniFile;
iniStr: string;
bStop: boolean;
devMode: boolean;
begin
Logger.Log( 1, '*******************************************************' );
Logger.Log( 1, '* emT3XDataServer *' );
Logger.Log(1, Format(' Version: %s ', [FMain.ExeInfo1.FileVersion]));
Logger.Log( 1, '* Developed by EM Systems, Inc. *' );
Logger.Log( 1, '*******************************************************' );
Logger.Log( 1, '' );
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'emT3XDataServer.ini' );
try
Logger.Log( 1, 'iniFile: ' + ExtractFilePath(Application.ExeName) + 'emT3XDataServer.ini' );
bStop := False;
Logger.Log( 1, '' );
Logger.Log(1, '--- Settings ---');
iniStr := iniFile.ReadString( 'Settings', 'MemoLogLevel', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Settings->memoLogLevel: Entry not found - default: 3' )
else
Logger.Log( 1, '--Settings->memoLogLevel: ' + iniStr );
iniStr := iniFile.ReadString( 'Settings', 'FileLogLevel', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Settings->fileLogLevel: Entry not found - default: 4' )
else
Logger.Log( 1, '--Settings->fileLogLevel: ' + iniStr );
iniFile := TIniFile.Create( ChangeFileExt(Application.ExeName, '.ini') );
try
devMode := iniFile.ReadBool( 'Settings', 'devMode', True );
Logger.Log( 1, 'devMode: ' + BoolToStr(devMode, True) );
Logger.Log( 1, '' );
iniStr := iniFile.ReadString( 'Settings', 'LogFileNum', '' );
iniStr := iniFile.ReadString( 'Settings', 'webClientVersion', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Settings->LogFileNum: Entry not found' )
begin
Logger.Log( 1, '--Settings->WebClientVersion: Entry not found - ERROR: ini entry required!!!');
bStop := True;
end
else
Logger.Log( 1, '--Settings->LogFileNum: ' + IntToStr(StrToInt(iniStr) - 1) );
Logger.Log( 1, '--Settings->WebClientVersion: ' + iniStr );
Logger.Log(1, '--- Database ---');
iniStr := IniFile.ReadString( 'Database', 'Server', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Database->Server: Entry not found' )
begin
Logger.Log( 1, '----Database->Server: Entry not found - ERROR: ini entry required!!!' );
bStop := True;
end
else
Logger.Log( 1, '--Database->Server: ' + iniStr );
Logger.Log( 1, '----Database->Server: ' + iniStr );
iniStr := iniFile.ReadString('Database', 'Database', '');
if iniStr.IsEmpty then
Logger.Log( 1, '----Database->Database: Entry not found' )
Logger.Log( 1, '----Database->Database: ini entry not found - default: kg_order_entry' )
else
Logger.Log( 1, '----Database->Database: ' + iniStr );
Logger.Log( 1, '----Database->Database: ini entry: ' + iniStr );
iniStr := iniFile.ReadString('Database', 'Username', '');
if iniStr.IsEmpty then
Logger.Log( 1, '----Database->Username: Entry not found' )
Logger.Log( 1, '----Database->Username: Entry not found - default: root' )
else
Logger.Log( 1, '----Database->Username: ' + iniStr );
iniStr := iniFile.ReadString('Database', 'Password', '');
if iniStr.IsEmpty then
Logger.Log( 1, '----Database->Password: Entry not found' )
Logger.Log( 1, '----Database->Password: Entry not found - default: xxxxxx' )
else
Logger.Log( 1, '----Database->Password: xxxxxxxx' );
Logger.Log( 1, '----Database->Password: ini entry: xxxxxxxx' );
Logger.Log( 1, '' );
finally
IniFile.Free;
end;
if bStop then
begin
Logger.Log( 1, 'ini configuration error: Existing program!' );
if devMode then
MessageDlgPos( 'ini configuration error: Existing program!', mtConfirmation, [mbOk], 0, 250, 350 );
Close();
end;
AuthServerModule := TAuthServerModule.Create(Self);
AuthServerModule.StartAuthServer(serverConfig.url, AUTH_MODEL);
......@@ -163,29 +156,7 @@ begin
AppServerModule := TAppServerModule.Create(Self);
AppServerModule.StartAppServer( serverConfig.url );
Logger.Log(1, 'Exe=' + Application.ExeName);
UpdateGUI;
end;
procedure TFMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
ServerConfig.Free;
AuthServerModule.Free;
ApiServerModule.Free;
AppServerModule.Free;
end;
procedure TFMain.UpdateGUI;
begin
if AuthServerModule.SparkleHttpSysDispatcher.Active then
memoInfo.Lines.Add( 'AuthServer started at: ' + AuthServerModule.XDataServer.BaseUrl )
else
memoInfo.Lines.Add( 'AuthServer stopped' );
if ApiServerModule.SparkleHttpSysDispatcher.Active then
memoInfo.Lines.Add( 'ApiServer started at: ' + ApiServerModule.XDataServer.BaseUrl )
else
memoInfo.Lines.Add( 'ApiServer stopped' );
end;
end.
{
"AuthUrl" : "http://localhost:2001/emsys/emt3/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emt3/api/"
}
/* Note: Base layout */
html, body{
height:100%;
margin:0;
}
#wrapper{
height:100vh;
display:flex;
flex-direction:column;
min-height:0;
}
/* Note: Embedded forms must be allowed to shrink inside flex containers */
#main\.webpanel{
min-height:0;
flex:1 1 auto;
display:flex;
flex-direction:column;
}
#main\.webpanel > *{
min-height:0;
}
/* Note: Primary button color */
.btn-primary{
background-color:#286090 !important;
border-color:#286090 !important;
color:#fff !important;
}
.btn-primary:hover{
background-color:#204d74 !important;
border-color:#204d74 !important;
}
/* Note: Navbar tweaks */
#view\.main\.apptitle{
display:flex;
align-items:center;
}
.navbar-nav .nav-link.active{
color:#fff !important;
background-color:#004F84 !important;
font-weight:700;
}
.navbar-nav .nav-link:hover{
color:#fff !important;
background-color:#286090 !important;
}
.navbar-toggler{
display:none;
}
/* Note: Dropdown menu items */
.dropdown-menu a{
display:flex;
align-items:center;
width:100%;
padding:.5rem 1rem;
color:#000;
text-decoration:none;
}
.dropdown-menu a:hover{
background-color:#204d74;
color:#fff;
}
.dropdown-menu a span{
flex-grow:1;
}
/* Note: Login card (used on login view) */
.login-card{
display:inline-block;
width:300px;
padding:0;
border-radius:10px;
box-shadow:0 4px 8px rgba(0,0,0,.1);
background-color:#fff;
}
/* Note: Validation helpers */
.is-invalid .form-check-input{
border:1px solid #dc3545 !important;
}
.is-invalid .form-check-label{
color:#dc3545 !important;
}
/* Note: Toast animation */
@keyframes slideInLeft{
from{transform:translateX(-120%);opacity:0;}
to{transform:translateX(0);opacity:1;}
}
.toast.slide-in{
animation:slideInLeft .4s ease-out forwards;
}
/* Note: Spinner overlay */
#spinner{
position:fixed !important;
z-index:9999 !important;
top:50%;
left:50%;
transform:translate(-50%,-50%);
}
/* Note: TasksHTML (table experiment) */
#tasks_table_host{
height:100%;
min-height:0;
}
#tasks_table_host .tasks-vscroll{
height:100%;
overflow-y:auto;
overflow-x:hidden;
}
#tasks_table_host .tasks-hscroll{
overflow-x:auto;
}
#tasks_table_host .tasks-hscroll table{
width:max-content;
min-width:100%;
table-layout:fixed;
}
#tasks_table_host thead th{
position:sticky;
top:0;
z-index:2;
background:var(--bs-body-bg);
}
#tasks_table_host td,
#tasks_table_host th{
padding:.25rem;
}
#tasks_table_host .nowrap-cell{white-space:nowrap;}
#tasks_table_host .wrap-cell{white-space:normal;word-break:break-word;}
#tasks_table_host .cell-input,
#tasks_table_host .cell-textarea{
border:0;
background:transparent;
border-radius:0;
padding:0;
margin:0;
box-shadow:none;
}
#tasks_table_host .cell-input:focus,
#tasks_table_host .cell-textarea:focus{
outline:0;
box-shadow:inset 0 -2px 0 var(--bs-primary);
}
#tasks_table_host .cell-textarea{
resize:none;
overflow:hidden;
white-space:pre-wrap;
}
/* Note: TasksDataGrid (TWebDataGrid experiment) */
#data_grid_tasks{
height:100%;
min-height:0;
}
#data_grid_tasks .ag-cell{
line-height:1.25;
padding-top:4px;
padding-bottom:4px;
}
#data_grid_tasks .ag-cell-inline-editing textarea{
line-height:1.25;
padding:4px 6px;
resize:none;
height:100%;
box-sizing:border-box;
}
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #204d74;
margin: -5px 0 0 -5px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
[Settings]
MemoLogLevel=4
FileLogLevel=4
webClientVersion=0.8.2
LogFileNum=176
webClientVersion=0.8.3
LogFileNum=111
[Database]
Server=192.168.116.131
--Server=192.168.116.128
Server=192.168.102.131
--Server=192.168.75.133
--Server=192.168.159.10
Database=eTask
......
[2026-04-03 14:52:54.494][1] --TServerConfig.Create - start
[2026-04-03 14:52:54.494][1] --ServerConfigSerialize: null
[2026-04-03 14:52:54.504][1] --TServerConfig.Create - end
[2026-04-03 14:52:54.512][1] --LoadServerConfig - start
[2026-04-03 14:52:54.512][1] -- Config file: C:\Projects\emT3web\emT3XDataServer\bin\emT3XDataServer.json
[2026-04-03 14:52:54.512][1] -- Config file found.
[2026-04-03 14:52:54.528][1] --TServerConfig.Create - start
[2026-04-03 14:52:54.528][1] --ServerConfigSerialize: {"url":"http://localhost:2004/emsys/emt3","jwtTokenSecret":"super_secret0123super_secret4567","adminPassword":"whatisthisusedfor","webAppFolder":"static","reportsFolder":"static/"}
[2026-04-03 14:52:54.538][1] --TServerConfig.Create - end
[2026-04-03 14:52:54.543][1] -- localConfig loaded from config file
[2026-04-03 14:52:54.551][1] -- serverConfig.Free - called
[2026-04-03 14:52:54.556][1] -- serverConfig := localConfig - called
[2026-04-03 14:52:54.563][1] --- Server Config Values ---
[2026-04-03 14:52:54.563][1] -- url: http://localhost:2001/emsys/emt3 [from config]
[2026-04-03 14:52:54.575][1] -- adminPassword: whatisthisusedfor [default]
[2026-04-03 14:52:54.582][1] -- jwtTokenSecret: super_secret0123super_secret4567 [default]
[2026-04-03 14:52:54.589][1] -- webAppFolder: static [default]
[2026-04-03 14:52:54.596][1] -- serverConfig.reportsFolder: .\static\
[2026-04-03 14:52:54.603][1] --LoadServerConfig - end
[2026-04-03 14:52:54.612][1] *******************************************************
[2026-04-03 14:52:54.617][1] * emT3XDataServer *
[2026-04-03 14:52:54.624][1] Version: 0.8.0.0
[2026-04-03 14:52:54.634][1] * Developed by EM Systems, Inc. *
[2026-04-03 14:52:54.641][1] *******************************************************
[2026-04-03 14:52:54.656][1] iniFile: C:\Projects\emT3web\emT3XDataServer\bin\emT3XDataServer.ini
[2026-04-03 14:52:54.670][1] --- Settings ---
[2026-04-03 14:52:54.679][1] --Settings->memoLogLevel: 4
[2026-04-03 14:52:54.688][1] --Settings->fileLogLevel: 4
[2026-04-03 14:52:54.701][1] --Settings->LogFileNum: 175
[2026-04-03 14:52:54.713][1] --- Database ---
[2026-04-03 14:52:54.722][1] --Database->Server: 192.168.102.131
[2026-04-03 14:52:54.730][1] ----Database->Database: eTask
[2026-04-03 14:52:54.739][1] ----Database->Username: root
[2026-04-03 14:52:54.749][1] ----Database->Password: xxxxxxxx
[2026-04-03 14:52:54.824][1] Auth server module listening at "http://localhost:2001/emsys/emt3/auth"
[2026-04-03 14:52:54.828][1] API XDataServer.ModelName=Api
[2026-04-03 14:52:54.849][1] Api server module listening at "http://localhost:2001/emsys/emt3/api"
[2026-04-03 14:52:54.864][1] App server module listening at "http://localhost:2001/emsys/emt3/app", rootDir: static
[2026-04-03 14:52:54.868][1] Exe=C:\Projects\emT3web\emT3XDataServer\bin\emT3XDataServer.exe
......@@ -7,6 +7,7 @@ uses
Vcl.StdCtrls,
IniFiles,
Vcl.Forms,
Vcl.Dialogs,
Api.Server.Module in 'Source\Api.Server.Module.pas' {ApiServerModule: TDataModule},
Main in 'Source\Main.pas' {FMain},
Common.Logging in 'Source\Common.Logging.pas',
......@@ -20,7 +21,8 @@ uses
Auth.ServiceImpl in 'Source\Auth.ServiceImpl.pas',
App.Server.Module in 'Source\App.Server.Module.pas' {AppServerModule: TDataModule},
Api.Service in 'Source\Api.Service.pas',
Api.ServiceImpl in 'Source\Api.ServiceImpl.pas';
Api.ServiceImpl in 'Source\Api.ServiceImpl.pas',
Common.Ini in 'Source\Common.Ini.pas';
type
TMemoLogAppender = class( TInterfacedObject, ILogAppender )
......@@ -40,7 +42,7 @@ type
FLogFile: string;
FCriticalSection: TCriticalSection;
public
constructor Create(ALogLevel: Integer; AFilename: string);
constructor Create(ALogLevel: Integer; AFilename: string; AFileNum: Integer);
destructor Destroy; override;
procedure Send(logLevel: Integer; Log: ILog);
end;
......@@ -85,10 +87,9 @@ begin
end;
{ TFileLogAppender }
constructor TFileLogAppender.Create(ALogLevel: integer; AFilename: string);
constructor TFileLogAppender.Create(ALogLevel: integer; AFilename: string; AFileNum: integer);
var
iniFile: TIniFile;
fileNum: integer;
logsDir: string;
begin
FLogLevel := ALogLevel;
......@@ -97,14 +98,7 @@ begin
if not DirectoryExists(logsDir) then
CreateDir(logsDir);
iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'emT3XDataServer.ini' );
try
fileNum := iniFile.ReadInteger( 'Settings', 'LogFileNum', 0 );
FLogFile := logsDir + AFilename + Format( '%.4d', [fileNum] ) + '.log';
iniFile.WriteInteger( 'Settings', 'LogFileNum', fileNum + 1 );
finally
iniFile.Free;
end;
FLogFile := logsDir + AFilename + Format( '%.4d', [AFileNum] ) + '.log';
end;
destructor TFileLogAppender.Destroy;
......@@ -151,27 +145,60 @@ end;
{$R *.res}
var
iniFilename: string;
iniFile: TIniFile;
memoLogLevel: Integer;
fileLogLevel: Integer;
//memoLogLevel: Integer;
//fileLogLevel: Integer;
//fileNum: Integer;
iniStr: string;
begin
ReportMemoryLeaksOnShutdown := True;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TFMain, FMain);
iniFile := TIniFile.Create(ExtractFilePath(Application.ExeName) + 'emT3XDataServer.ini');
iniFilename := ChangeFileExt( Application.ExeName, '.ini' );
iniFile := TIniFile.Create( iniFilename );
try
memoLogLevel := iniFile.ReadInteger('Settings', 'memoLogLevel', 3);
fileLogLevel := iniFile.ReadInteger('Settings', 'fileLogLevel', 4);
LoadIniEntries;
//memoLogLevel := iniFile.ReadInteger('Settings', 'MemoLogLevel', 3);
//fileLogLevel := iniFile.ReadInteger('Settings', 'FileLogLevel', 4);
//fileNum := iniFile.ReadInteger( 'Settings', 'LogFileNum', 0 );
Logger.AddAppender( TMemoLogAppender.Create(iniEntries.memoLogLevel, FMain.memoinfo) );
Logger.AddAppender( TFileLogAppender.Create(iniEntries.fileLogLevel, 'emT3XDataServer', iniEntries.logFileNum) );
Logger.Log( 1, '*******************************************************' );
Logger.Log( 1, '* emT3XDataServer *' );
Logger.Log( 1, Format('* Version: %s *', [FMain.ExeInfo1.FileVersion]));
Logger.Log( 1, '* Developed by EM Systems, Inc. *' );
Logger.Log( 1, '*******************************************************' );
Logger.Log( 1, '' );
Logger.Log( 1, 'iniFile: ' + iniFilename );
iniStr := iniFile.ReadString( 'Settings', 'MemoLogLevel', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Settings->MemoLogLevel: ini entry not found - default: ' + IntToStr(iniEntries.memoLogLevel) )
else
Logger.Log( 1, '--Settings->MemoLogLevel: ini entry: ' + iniStr );
iniStr := iniFile.ReadString( 'Settings', 'FileLogLevel', '' );
if iniStr.IsEmpty then
Logger.Log( 1, '--Settings->FileLogLevel: ini entry not found - default: ' + IntToStr(iniEntries.fileLogLevel) )
else
Logger.Log( 1, '--Settings->FileLogLevel: ini entry: ' + iniStr );
Logger.Log( 1, '' );
iniStr := iniFile.ReadString( 'Settings', 'LogFileNum', '' );
if iniStr = '1' then
Logger.Log( 1, '--Settings->LogFileNum: ini entry not found - LogFileNum 1 added to iniFile' )
else
Logger.Log( 1, '--Settings->LogFileNum: ini entry: ' + iniStr );
finally
iniFile.Free;
end;
Logger.AddAppender(TMemoLogAppender.Create(memoLogLevel, FMain.memoinfo));
Logger.AddAppender(TFileLogAppender.Create(fileLogLevel, 'emT3XDataServer'));
Application.Run;
end.
......@@ -114,9 +114,10 @@
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_ExeOutput>.\bin</DCC_ExeOutput>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.8.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys>
<VerInfo_Keys>CompanyName=EM Systems;FileDescription=$(MSBuildProjectName);FileVersion=0.8.3.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.11;Comments=</VerInfo_Keys>
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>8</VerInfo_MinorVer>
<VerInfo_Release>3</VerInfo_Release>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
......@@ -161,12 +162,10 @@
<DCCReference Include="Source\Common.Config.pas"/>
<DCCReference Include="Source\Auth.Server.Module.pas">
<Form>AuthServerModule</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="Source\Auth.Database.pas">
<Form>AuthDatabase</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="Source\uLibrary.pas"/>
......@@ -174,11 +173,11 @@
<DCCReference Include="Source\Auth.ServiceImpl.pas"/>
<DCCReference Include="Source\App.Server.Module.pas">
<Form>AppServerModule</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="Source\Api.Service.pas"/>
<DCCReference Include="Source\Api.ServiceImpl.pas"/>
<DCCReference Include="Source\Common.Ini.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment