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>
/* 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>
{
"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.
<!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