Commit 95158366 by Mac Stephens

WIP building new client and adding item_num system

parent ba563f22
...@@ -16,3 +16,7 @@ emT3XDataServer/Win32/ ...@@ -16,3 +16,7 @@ emT3XDataServer/Win32/
*.txt *.txt
emT3Web/Win32/Debug/ emT3Web/Win32/Debug/
emT3Web/__recovery/
emT3WebApp/__history/
...@@ -110,6 +110,6 @@ String TfMain::BuildLaunchUrl(const String &baseUrl, const String &userId, const ...@@ -110,6 +110,6 @@ String TfMain::BuildLaunchUrl(const String &baseUrl, const String &userId, const
String qTaskId = TNetEncoding::URL->Encode(taskId); String qTaskId = TNetEncoding::URL->Encode(taskId);
String qCode = TNetEncoding::URL->Encode(code); String qCode = TNetEncoding::URL->Encode(code);
return cleanBaseUrl + sep + "user_id=" + qUserId + "&task_id=" + qTaskId + "&code=" + qCode; return cleanBaseUrl + sep + "user_id=" + qUserId + "&task_id=" + qTaskId + "&url_code=" + qCode;
} }
...@@ -80,7 +80,7 @@ object fMain: TfMain ...@@ -80,7 +80,7 @@ object fMain: TfMain
Width = 337 Width = 337
Height = 23 Height = 23
TabOrder = 4 TabOrder = 4
Text = 'http://127.0.0.1:8000/emT3webClient/index.html' Text = 'http://localhost:8000/emT3webApp/index.html'
end end
object edtExpSeconds: TEdit object edtExpSeconds: TEdit
Left = 500 Left = 500
......
...@@ -120,10 +120,6 @@ end; ...@@ -120,10 +120,6 @@ end;
procedure TAuthService.Logout; procedure TAuthService.Logout;
begin begin
DeleteToken; DeleteToken;
window.localStorage.removeItem('EMT3_USER_ID');
window.localStorage.removeItem('EMT3_TASK_ID');
window.localStorage.removeItem('EMT3_CODE');
end; end;
procedure TAuthService.SetToken(AToken: string); procedure TAuthService.SetToken(AToken: string);
......
...@@ -17,9 +17,6 @@ type ...@@ -17,9 +17,6 @@ type
procedure AuthConnectionError(Error: TXDataWebConnectionError); procedure AuthConnectionError(Error: TXDataWebConnectionError);
private private
FUnauthorizedAccessProc: TUnauthorizedAccessProc; FUnauthorizedAccessProc: TUnauthorizedAccessProc;
FUserIdParam: string;
FTaskIdParam: string;
FCodeParam: string;
public public
const clientVersion = '0.0.1'; const clientVersion = '0.0.1';
......
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.
...@@ -113,6 +113,19 @@ object FViewMain: TFViewMain ...@@ -113,6 +113,19 @@ object FViewMain: TFViewMain
'Close') 'Close')
Opacity = 0.200000000000000000 Opacity = 0.200000000000000000
end 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 XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 44 Left = 44
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
<a id="view.main.apptitle" class="navbar-brand" href="index.html">emT3web</a> <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> <span id="view.main.version" class="small text-muted ms-2"></span>
</div> </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="navbarNavDropdown">
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
......
...@@ -6,7 +6,7 @@ uses ...@@ -6,7 +6,7 @@ uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls, System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls, WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset, WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils; App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils, View.Home;
type type
TFViewMain = class(TWebForm) TFViewMain = class(TWebForm)
...@@ -17,25 +17,23 @@ type ...@@ -17,25 +17,23 @@ type
XDataWebClient: TXDataWebClient; XDataWebClient: TXDataWebClient;
lblVersion: TWebLabel; lblVersion: TWebLabel;
lblAppTitle: TWebLabel; lblAppTitle: TWebLabel;
edtTaskIdMain: TWebEdit;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject); procedure mnuLogoutClick(Sender: TObject);
procedure wllblLogoutClick(Sender: TObject); procedure wllblLogoutClick(Sender: TObject);
private private
{ Private declarations } { Private declarations }
FUserInfo: string; FTasksHtmlForm: TWebForm;
FSearchSettings: string;
FChildForm: TWebForm;
FLogoutProc: TLogoutProc; FLogoutProc: TLogoutProc;
FSearchProc: TSearchProc;
procedure ShowCrudForm( AFormClass: TWebFormClass );
procedure ConfirmLogout; procedure ConfirmLogout;
procedure LoadTasksHtmlForm;
procedure LoadHomeForm;
public public
{ Public declarations } { Public declarations }
class procedure Display(LogoutProc: TLogoutProc); FUserId: string;
procedure ShowForm( AFormClass: TWebFormClass ); FTaskId: string;
var FCode: string;
search: string; class procedure Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
change: boolean;
end; end;
var var
...@@ -45,30 +43,40 @@ implementation ...@@ -45,30 +43,40 @@ implementation
uses uses
Auth.Service, Auth.Service,
View.Tasks, View.TasksHTML;
View.TasksHTML,
View.TasksDataGrid,
View.TasksDBGrid;
{$R *.dfm} {$R *.dfm}
class procedure TFViewMain.Display(LogoutProc: TLogoutProc); class procedure TFViewMain.Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
begin begin
if Assigned(FViewMain) then if Assigned(FViewMain) then
FViewMain.Free; FViewMain.Free;
FViewMain := TFViewMain.CreateNew; FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := LogoutProc; 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; end;
procedure TFViewMain.WebFormCreate(Sender: TObject); procedure TFViewMain.WebFormCreate(Sender: TObject);
begin begin
console.log('TFViewMain.WebFormCreate fired'); console.log('TFViewMain.WebFormCreate fired');
// FChildForm := nil;
console.log('About to ShowForm(TFTasksHTML), host=' + WebPanel1.ElementID);
ShowForm(TFTasksHTML);
lblAppTitle.Caption := 'emT3web'; lblAppTitle.Caption := 'emT3web';
lblVersion.Caption := 'v' + DMConnection.clientVersion; 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;
end; end;
...@@ -99,26 +107,28 @@ begin ...@@ -99,26 +107,28 @@ begin
); );
end; end;
procedure TFViewMain.LoadTasksHtmlForm;
procedure TFViewMain.ShowCrudForm(AFormClass: TWebFormClass);
begin begin
ShowForm(AFormClass); if Assigned(FTasksHtmlForm) then
end; FTasksHtmlForm.Free;
console.log('About to create TFTasksHTML, host=' + WebPanel1.ElementID);
console.log('Main form task id is: ' + FTaskId);
procedure TFViewMain.ShowForm(AFormClass: TWebFormClass); FTasksHtmlForm := TFTasksHTML.CreateForm(WebPanel1.ElementID, FTaskId);
begin
if Assigned(FChildForm) then
FChildForm.Free;
Application.CreateForm(AFormClass, WebPanel1.ElementID, FChildForm);
console.log('CreateForm called, FChildForm assigned: ' + BoolToStr(Assigned(FChildForm)));
end; end;
//procedure TFViewMain.ShowTasksForm(Info: string); procedure TFViewMain.LoadHomeForm;
//begin begin
// if Assigned(FChildForm) then if Assigned(FTasksHtmlForm) then
// FChildForm.Free; FTasksHtmlForm.Free;
// FChildForm := TFViewUsers.CreateForm(WebPanel1.ElementID, Info);
//end; 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);
FTasksHtmlForm := TFHome.CreateForm(WebPanel1.ElementID, FTaskId, FUserId, FCode);
end;
end. end.
<div id="pnl_grid" class="h-100"></div>
unit View.Tasks;
interface
uses
System.SysUtils, System.Classes,
JS, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
VCL.TMSFNCTypes, VCL.TMSFNCUtils, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes,
VCL.TMSFNCDataGridCell, VCL.TMSFNCDataGridData, VCL.TMSFNCDataGridBase,
VCL.TMSFNCDataGridCore, VCL.TMSFNCDataGridRenderer, VCL.TMSFNCCustomControl,
VCL.TMSFNCDataGrid,
XData.Web.Client,
Utils, WEBLib.ExtCtrls, System.Rtti, Vcl.Controls;
type
TFTasks = class(TWebForm)
xdwcTasks: TXDataWebClient;
pnlGrid: TWebPanel;
grdTasks: TTMSFNCDataGrid;
procedure WebFormCreate(Sender: TObject);
private
[async] procedure LoadTasks(const AProjectId: string);
public
end;
var
FTasks: TFTasks;
implementation
uses
ConnectionModule;
{$R *.dfm}
const
COL_TASK_ITEM_ID = 0;
COL_TASK_ID = 1;
COL_APP = 2;
COL_VERSION = 3;
COL_TASK_DATE = 4;
COL_REPORTED_BY = 5;
COL_ASSIGNED_TO = 6;
COL_STATUS = 7;
COL_STATUS_DATE = 8;
COL_FIXED_VER = 9;
COL_FORM_SECTION = 10;
COL_ISSUE = 11;
COL_NOTES = 12;
procedure TFTasks.WebFormCreate(Sender: TObject);
begin
xdwcTasks.Connection := DMConnection.ApiConnection;
LoadTasks('WPR0001');
end;
[async] procedure TFTasks.LoadTasks(const AProjectId: string);
var
response: TXDataClientResponse;
resultObj: TJSObject;
tasksArray: TJSArray;
taskObj: TJSObject;
itemsArray: TJSArray;
itemObj: TJSObject;
taskIndex, itemIndex: Integer;
gridRow: Integer;
statusDateVal: JSValue;
begin
Utils.ShowSpinner('spinner');
try
try
response := await(xdwcTasks.RawInvokeAsync(
'IApiService.GetProjectTasks', [AProjectId]
));
if not Assigned(response.Result) then
Exit;
resultObj := TJSObject(response.Result);
tasksArray := TJSArray(resultObj['data']);
grdTasks.BeginUpdate;
try
grdTasks.RowCount := 1;
gridRow := 1;
for taskIndex := 0 to tasksArray.Length - 1 do
begin
taskObj := TJSObject(tasksArray[taskIndex]);
itemsArray := TJSArray(taskObj['items']);
for itemIndex := 0 to itemsArray.Length - 1 do
begin
itemObj := TJSObject(itemsArray[itemIndex]);
grdTasks.RowCount := gridRow + 1;
grdTasks.Cells[COL_TASK_ITEM_ID, gridRow] := string(itemObj['taskItemId']);
grdTasks.Cells[COL_TASK_ID, gridRow] := string(itemObj['taskId']);
grdTasks.Cells[COL_APP, gridRow] := string(itemObj['application']);
grdTasks.Cells[COL_VERSION, gridRow] := string(itemObj['version']);
grdTasks.Cells[COL_TASK_DATE, gridRow] := string(itemObj['taskDate']);
grdTasks.Cells[COL_REPORTED_BY, gridRow] := string(itemObj['reportedBy']);
grdTasks.Cells[COL_ASSIGNED_TO, gridRow] := string(itemObj['assignedTo']);
grdTasks.Cells[COL_STATUS, gridRow] := string(itemObj['status']);
statusDateVal := itemObj['statusDate'];
if JS.isNull(statusDateVal) or JS.isUndefined(statusDateVal) then
grdTasks.Cells[COL_STATUS_DATE, gridRow] := ''
else
grdTasks.Cells[COL_STATUS_DATE, gridRow] := string(statusDateVal);
grdTasks.Cells[COL_FIXED_VER, gridRow] := string(itemObj['fixedVersion']);
grdTasks.Cells[COL_FORM_SECTION, gridRow] := string(itemObj['formSection']);
grdTasks.Cells[COL_ISSUE, gridRow] := string(itemObj['issue']);
grdTasks.Cells[COL_NOTES, gridRow] := string(itemObj['notes']);
Inc(gridRow);
end;
end;
finally
grdTasks.EndUpdate;
end;
except
on E: EXDataClientRequestException do
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
end.
object FTasksDBGrid: TFTasksDBGrid
Width = 1161
Height = 725
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object dbGridTasks: TWebDBGrid
Left = 0
Top = 0
Width = 1161
Height = 725
TabStop = False
Align = alClient
Columns = <
item
ElementClassName = 'text-nowrap tasks-nowrap'
DataField = 'taskId'
Title = 'Task Id'
Width = 95
end
item
DataField = 'application'
Editor = geCombo
Title = 'Application'
Width = 150
end
item
ElementClassName = 'text-nowrap tasks-nowrap'
DataField = 'version'
Title = 'Version'
Width = 95
end
item
DataField = 'taskDate'
Editor = geDate
Title = 'Date'
Width = 120
end
item
ComboBoxItems.Strings = (
#11
''
'Elias'
'Mark'
'Mac')
DataField = 'reportedBy'
Editor = geCombo
Title = 'Reported By'
Width = 95
end
item
ComboBoxItems.Strings = (
''
'Elias'
'Mark'
'Mac')
DataField = 'assignedTo'
Editor = geCombo
Title = 'Assigned To'
Width = 95
end
item
ElementClassName = 'text-nowrap'
DataField = 'status'
Title = 'Status'
Width = 120
end
item
DataField = 'statusDate'
Editor = geDate
Title = 'Status Date'
Width = 120
end
item
ElementClassName = 'text-nowrap tasks-nowrap'
DataField = 'formSection'
Title = 'Form/Section'
Width = 150
end
item
ElementClassName = 'text-wrap'
DataField = 'issue'
Title = 'Issue'
Width = 350
end
item
ElementClassName = 'text-wrap'
DataField = 'notes'
Editor = geMemo
Title = 'Notes'
Width = 350
end>
DataSource = wdsTasks
ElementFont = efCSS
ElementId = 'db_grid_tasks'
FixedFont.Charset = DEFAULT_CHARSET
FixedFont.Color = clWindowText
FixedFont.Height = -12
FixedFont.Name = 'Segoe UI'
FixedFont.Style = []
FixedCols = 0
Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRangeSelect, goColSizing, goRowMoving, goEditing, goFixedRowDefAlign]
TabOrder = 0
HeightPercent = 100.000000000000000000
WidthStyle = ssAuto
WidthPercent = 100.000000000000000000
ColWidths = (
95
150
95
120
95
95
120
120
150
350
350)
end
object btnReload: TWebButton
Left = 864
Top = 259
Width = 96
Height = 25
Caption = 'Reload'
ChildOrder = 1
ElementID = 'btn_reload'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnReloadClick
end
object btnAddRow: TWebButton
Left = 980
Top = 259
Width = 96
Height = 25
Caption = 'Add Row'
ChildOrder = 1
ElementID = 'btn_add_row'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnAddRowClick
end
object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 1010
Top = 430
end
object wdsTasks: TWebDataSource
DataSet = xdwdsTasks
Left = 1014
Top = 574
end
object xdwdsTasks: TXDataWebDataSet
Left = 1010
Top = 504
object xdwdsTaskstaskID: TStringField
FieldName = 'taskId'
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
end
end
unit View.TasksDBGrid;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.Grids, Vcl.Controls, Vcl.Grids,
WEBLib.DBCtrls, Data.DB, WEBLib.DB, XData.Web.JsonDataset, XData.Web.Dataset,
XData.Web.Client, ConnectionModule, Vcl.StdCtrls, WEBLib.StdCtrls;
type
TFTasksDBGrid = class(TWebForm)
xdwcTasks: TXDataWebClient;
wdsTasks: TWebDataSource;
xdwdsTasks: TXDataWebDataSet;
xdwdsTaskstaskID: TStringField;
xdwdsTasksapplication: TStringField;
xdwdsTasksversion: TStringField;
xdwdsTaskstaskDate: TStringField;
xdwdsTasksreportedBy: TStringField;
xdwdsTasksassignedTo: TStringField;
xdwdsTasksstatus: TStringField;
xdwdsTasksstatusDate: TStringField;
xdwdsTasksformSection: TStringField;
xdwdsTasksissue: TStringField;
xdwdsTasksnotes: TStringField;
btnReload: TWebButton;
btnAddRow: TWebButton;
dbGridTasks: TWebDBGrid;
procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
private
FProjectId: string;
[async] procedure LoadTasks(const AProjectId: string);
procedure SetProjectTitle(const AProjectId: string);
public
{ Public declarations }
end;
var
FTasksDBGrid: TFTasksDBGrid;
implementation
{$R *.dfm}
procedure TFTasksDBGrid.WebFormCreate(Sender: TObject);
begin
wdsTasks.DataSet := xdwdsTasks;
dbGridTasks.DataSource := wdsTasks;
FProjectId := 'WPR0001';
SetProjectTitle(FProjectId);
LoadTasks(FProjectId);
end;
procedure TFTasksDBGrid.btnAddRowClick(Sender: TObject);
begin
xdwdsTasks.Append;
xdwdsTaskstaskID.AsString := FProjectId;
xdwdsTasks.Post;
end;
procedure TFTasksDBGrid.btnReloadClick(Sender: TObject);
begin
LoadTasks(FProjectId);
end;
procedure TFTasksDBgrid.SetProjectTitle(const AProjectId: string);
begin
TJSHTMLElement(document.getElementById('lbl_project_name')).innerText := AProjectId;
end;
[async] procedure TFTasksDBgrid.LoadTasks(const AProjectId: string);
var
response: TXDataClientResponse;
resultObj, taskObj: TJSObject;
tasksArray, itemsArray, flatItems: TJSArray;
taskIndex, itemIndex: Integer;
begin
response := await(xdwcTasks.RawInvokeAsync('IApiService.GetProjectTasks', [AProjectId]));
resultObj := TJSObject(response.Result);
tasksArray := TJSArray(resultObj['data']);
flatItems := TJSArray.new;
for taskIndex := 0 to tasksArray.Length - 1 do
begin
taskObj := TJSObject(tasksArray[taskIndex]);
itemsArray := TJSArray(taskObj['items']);
for itemIndex := 0 to itemsArray.Length - 1 do
flatItems.push(itemsArray[itemIndex]);
end;
xdwdsTasks.Close;
xdwdsTasks.SetJsonData(flatItems);
xdwdsTasks.Open;
end;
end.
\ No newline at end of file
object FTasksDataGrid: TFTasksDataGrid
Width = 1274
Height = 792
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object dataGridTasks: TWebDataGrid
Left = 22
Top = 96
Width = 1223
Height = 357
ElementID = 'data_grid_tasks'
Banding.Enabled = False
Banding.OddRowsColor = 16777215
Banding.EvenRowsColor = 16777215
MaxBlocksInCache = 0
TabOrder = 0
RowMultiSelectWithClick = False
EnableClickSelection = True
BidiMode = bdLeftToRight
SuppressMoveWhenColumnDragging = False
MultilevelHeaders = <>
ColumnDefs = <
item
Field = 'taskId'
CellDataType = cdtText
HeaderName = 'Task'
Editable = True
CheckboxSelection = False
Resizable = True
Width = 95
LockPinned = True
SelectOptions = <>
end
item
Field = 'application'
CellDataType = cdtText
HeaderName = 'App'
EditModeType = cetCombobox
Editable = True
CheckboxSelection = False
LockPinned = True
SelectOptions = <
item
Value = 'null'
end
item
Text = 'WebPoliceReport - Client'
Value = 'WebPoliceReport - Client'
end
item
Text = 'WebPoliceReport - Server'
Value = 'WebPoliceReport - Server'
end>
end
item
Field = 'version'
CellDataType = cdtText
HeaderName = 'Version'
Editable = True
CheckboxSelection = False
Width = 95
LockPinned = True
SelectOptions = <>
end
item
Field = 'taskDate'
CellDataType = cdtDateString
HeaderName = 'Date'
EditModeType = cetDate
Editable = True
CheckboxSelection = False
Width = 110
LockPinned = True
SelectOptions = <>
end
item
Field = 'reportedBy'
CellDataType = cdtText
HeaderName = 'Reported By'
EditModeType = cetCombobox
Editable = True
CheckboxSelection = False
Width = 150
LockPinned = True
SelectOptions = <
item
Value = 'Null'
end
item
Text = 'Elias'
Value = 'Elias'
end
item
Text = 'Mark'
Value = 'Mark'
end
item
Text = 'Mac'
Value = 'Mac'
end>
end
item
Field = 'assignedTo'
CellDataType = cdtText
HeaderName = 'Assigned To'
EditModeType = cetCombobox
Editable = True
CheckboxSelection = False
Width = 150
LockPinned = True
SelectOptions = <
item
Value = 'Null'
end
item
Text = 'Elias'
Value = 'Elias'
end
item
Text = 'Mark'
Value = 'Mark'
end
item
Text = 'Mac'
Value = 'Mac'
end>
end
item
Field = 'status'
CellDataType = cdtText
HeaderName = 'Status'
Editable = True
CheckboxSelection = False
Width = 95
LockPinned = True
SelectOptions = <
item
Value = 'Null'
end
item
Text = 'Fixed'
Value = 'Fixed'
end
item
Text = 'Fixed - Verified'
Value = 'Fixed - Verified'
end
item
Text = 'Not Fixed'
Value = 'Not Fixed'
end>
end
item
Field = 'statusDate'
CellDataType = cdtDateString
HeaderName = 'Status Date'
EditModeType = cetDate
Editable = True
CheckboxSelection = False
Width = 110
LockPinned = True
SelectOptions = <>
end
item
Field = 'formSection'
CellDataType = cdtText
HeaderName = 'Form/Section'
Editable = True
CheckboxSelection = False
LockPinned = True
SelectOptions = <>
end
item
Field = 'issue'
CellDataType = cdtText
HeaderName = 'Issue'
EditModeType = cetMemo
Editable = True
CheckboxSelection = False
Width = 300
WrapText = True
AutoHeight = True
LockPinned = True
SelectOptions = <>
end
item
Field = 'notes'
CellDataType = cdtText
HeaderName = 'Notes'
EditModeType = cetMemo
Editable = True
CheckboxSelection = False
Width = 300
WrapText = True
AutoHeight = True
LockPinned = True
SelectOptions = <>
end>
end
object btnReload: TWebButton
Left = 356
Top = 30
Width = 96
Height = 25
Caption = 'Reload'
ChildOrder = 1
ElementID = 'btn_reload'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnAddRow: TWebButton
Left = 356
Top = 61
Width = 96
Height = 25
Caption = 'Add Row'
ChildOrder = 1
ElementID = 'btn_add_row'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 150
Top = 534
end
end
unit View.TasksDataGrid;
interface
uses
System.SysUtils, System.Classes,
JS, Web, WEBLib.Forms, WEBLib.Dialogs, WEBLib.StdCtrls,
XData.Web.Client,
WEBLib.DataGrid,
Utils, WEBLib.DataGrid.Common, Vcl.StdCtrls, Vcl.Controls, Vcl.Grids,
WEBLib.DataGrid.DataAdapter.Base, WEBLib.DataGrid.DataAdapter.XData,
libdatagrid;
type
TFTasksDataGrid = class(TWebForm)
xdwcTasks: TXDataWebClient;
dataGridTasks: TWebDataGrid;
btnReload: TWebButton;
btnAddRow: TWebButton;
procedure WebFormCreate(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure btnAddRowClick(Sender: TObject);
private
FProjectId: string;
procedure SetProjectTitle(const AProjectId: string);
[async] procedure LoadTasks(const AProjectId: string);
public
end;
var
FTasksDataGrid: TFTasksDataGrid;
implementation
uses
ConnectionModule;
{$R *.dfm}
procedure TFTasksDataGrid.WebFormCreate(Sender: TObject);
begin
xdwcTasks.Connection := DMConnection.ApiConnection;
FProjectId := 'WPR0001';
SetProjectTitle(FProjectId);
LoadTasks(FProjectId);
end;
procedure TFTasksDataGrid.SetProjectTitle(const AProjectId: string);
begin
TJSHTMLElement(document.getElementById('lbl_project_name')).innerText := AProjectId;
end;
procedure TFTasksDataGrid.btnReloadClick(Sender: TObject);
begin
LoadTasks(FProjectId);
end;
procedure TFTasksDataGrid.btnAddRowClick(Sender: TObject);
begin
dataGridTasks.InsertNewRow;
dataGridTasks.EnsureLastRowVisible;
end;
[async] procedure TFTasksDataGrid.LoadTasks(const AProjectId: string);
var
response: TXDataClientResponse;
resultObj, taskObj: TJSObject;
tasksArray, itemsArray, flatItems: TJSArray;
taskIndex, itemIndex: Integer;
begin
response := await(xdwcTasks.RawInvokeAsync('IApiService.GetProjectTasks', [AProjectId]));
resultObj := TJSObject(response.Result);
tasksArray := TJSArray(resultObj['data']);
flatItems := TJSArray.new;
for taskIndex := 0 to tasksArray.Length - 1 do
begin
taskObj := TJSObject(tasksArray[taskIndex]);
itemsArray := TJSArray(taskObj['items']);
for itemIndex := 0 to itemsArray.Length - 1 do
flatItems.push(itemsArray[itemIndex]);
end;
dataGridTasks.LoadFromJSON(TJSObject(flatItems));
end;
end.
...@@ -3,7 +3,7 @@ object FTasksHTML: TFTasksHTML ...@@ -3,7 +3,7 @@ object FTasksHTML: TFTasksHTML
Height = 480 Height = 480
CSSLibrary = cssBootstrap CSSLibrary = cssBootstrap
ElementFont = efCSS ElementFont = efCSS
OnShow = WebFormShow OnCreate = WebFormCreate
object btnReload: TWebButton object btnReload: TWebButton
Left = 78 Left = 78
Top = 88 Top = 88
......
...@@ -30,7 +30,7 @@ type ...@@ -30,7 +30,7 @@ type
xdwdsTaskstaskItemId: TStringField; xdwdsTaskstaskItemId: TStringField;
procedure btnAddRowClick(Sender: TObject); procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject); procedure btnReloadClick(Sender: TObject);
procedure WebFormShow(Sender: TObject); procedure WebFormCreate(Sender: TObject);
private private
FTaskId: string; FTaskId: string;
[async] procedure LoadTasks(const ATaskId: string); [async] procedure LoadTasks(const ATaskId: string);
...@@ -49,7 +49,10 @@ type ...@@ -49,7 +49,10 @@ type
[async] procedure SaveRow(AIndex: Integer); [async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent); procedure EditorBlur(Event: TJSEvent);
procedure InitializeForm;
public public
class function CreateForm(AElementID, ATaskId: string): TWebForm;
end; end;
var var
...@@ -62,11 +65,29 @@ uses ...@@ -62,11 +65,29 @@ uses
{$R *.dfm} {$R *.dfm}
class function TFTasksHTML.CreateForm(AElementID, ATaskId: string): TWebForm;
begin
console.log('TFTasksHTML.CreateForm called, host=' + AElementID + ', taskId=' + ATaskId);
Application.CreateForm(TFTasksHTML, AElementID, Result,
procedure(AForm: TObject)
begin
console.log('TFTasksHTML.CreateForm callback fired, assigned=' + BoolToStr(Assigned(AForm), True));
procedure TFTasksHTML.WebFormShow(Sender: TObject); if Assigned(AForm) then
begin
TFTasksHTML(AForm).FTaskId := ATaskId;
TFTasksHTML(AForm).InitializeForm;
end
else
Utils.ShowErrorModal('TFTasksHTML form callback returned nil.');
end
);
end;
procedure TFTasksHTML.InitializeForm;
begin begin
console.log('TFTasksHTML.WebFormShow fired'); console.log('TFTasksHTML.InitializeForm fired');
FTaskId := window.localStorage.getItem('EMT3_TASK_ID');
console.log('The task id is: ' + FTaskId); console.log('The task id is: ' + FTaskId);
if FTaskId = '' then if FTaskId = '' then
...@@ -79,12 +100,12 @@ begin ...@@ -79,12 +100,12 @@ begin
if not DMConnection.ApiConnection.Connected then if not DMConnection.ApiConnection.Connected then
begin begin
DMConnection.ApiConnection.Open( DMConnection.ApiConnection.Open(
procedure procedure
begin begin
LoadTasks(FTaskID); LoadTasks(FTaskId);
end end
); );
end end
else else
LoadTasks(FTaskId); LoadTasks(FTaskId);
...@@ -255,6 +276,11 @@ begin ...@@ -255,6 +276,11 @@ begin
end; end;
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
begin
console.log('TFTasksHTML.WebFormCreate fired');
end;
[async] procedure TFTasksHTML.LoadTasks(const ATaskId: string); [async] procedure TFTasksHTML.LoadTasks(const ATaskId: string);
var var
response: TXDataClientResponse; response: TXDataClientResponse;
......
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>TMS Web Project</title>
<style>
</style>
</head>
<body>
</body>
</html>
\ No newline at end of file
unit View.TasksTabulator;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, Data.DB, XData.Web.JsonDataset,
XData.Web.Dataset, XData.Web.Client, ConnectionModule;
type
TFTasksTabulator = class(TWebForm)
xdwcTasks: TXDataWebClient;
xdwdsTasks: TXDataWebDataSet;
xdwdsTaskstaskID: TStringField;
xdwdsTasksapplication: TStringField;
xdwdsTasksversion: TStringField;
xdwdsTaskstaskDate: TStringField;
xdwdsTasksreportedBy: TStringField;
xdwdsTasksassignedTo: TStringField;
xdwdsTasksstatus: TStringField;
xdwdsTasksstatusDate: TStringField;
xdwdsTasksformSection: TStringField;
xdwdsTasksissue: TStringField;
xdwdsTasksnotes: TStringField;
private
{ Private declarations }
public
{ Public declarations }
end;
var
FTasksTabulator: TFTasksTabulator;
implementation
{$R *.dfm}
end.
\ No newline at end of file
program emT3web; program emT3web;
{$R *.dres}
uses uses
Vcl.Forms, Vcl.Forms,
System.SysUtils, System.SysUtils,
...@@ -14,11 +16,8 @@ uses ...@@ -14,11 +16,8 @@ uses
App.Config in 'App.Config.pas', App.Config in 'App.Config.pas',
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html}, View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.Tasks in 'View.Tasks.pas' {FTasks: TWebForm} {*.html},
View.TasksHTML in 'View.TasksHTML.pas' {FTasksHTML: TWebForm} {*.html}, View.TasksHTML in 'View.TasksHTML.pas' {FTasksHTML: TWebForm} {*.html},
View.TasksDataGrid in 'View.TasksDataGrid.pas' {FTasksDataGrid: TWebForm} {*.html}, View.Home in 'View.Home.pas' {FHome: TWebForm} {*.html};
View.TasksTabulator in 'View.TasksTabulator.pas' {FTasksTabulator: TWebForm} {*.html},
View.TasksDBGrid in 'View.TasksDBGrid.pas' {FTasksDBGrid: TWebForm} {*.html};
{$R *.res} {$R *.res}
...@@ -74,11 +73,9 @@ begin ...@@ -74,11 +73,9 @@ begin
DisplayAccessDeniedModal(AMessage); DisplayAccessDeniedModal(AMessage);
end; end;
procedure DisplayMainView; procedure DisplayMainView(const AUserId, ATaskId, ACode: string);
begin begin
if Assigned(FViewLogin) then TFViewMain.Display(@DisplayLoginView, AUserId, ATaskId, ACode);
FViewLogin.Free;
TFViewMain.Display(@DisplayLoginView);
end; end;
procedure UnauthorizedAccessProc(AMessage: string); procedure UnauthorizedAccessProc(AMessage: string);
...@@ -86,65 +83,49 @@ begin ...@@ -86,65 +83,49 @@ begin
DisplayLoginView(AMessage); DisplayLoginView(AMessage);
end; end;
procedure SaveUrlParamsToStorage(const userId, taskId, code: string);
begin
if userId <> '' then
window.localStorage.setItem('EMT3_USER_ID', userId);
if taskId <> '' then
window.localStorage.setItem('EMT3_TASK_ID', taskId);
if code <> '' then
window.localStorage.setItem('EMT3_CODE', code);
end;
procedure StartApplication; procedure StartApplication;
var var
UserIdParam: string; userIdParam: string;
TaskIdParam: string; taskIdParam: string;
CodeParam: string; codeParam: string;
begin begin
UserIdParam := Application.Parameters.Values['user_id']; userIdParam := Application.Parameters.Values['user_id'];
TaskIdParam := Application.Parameters.Values['task_id']; taskIdParam := Application.Parameters.Values['task_id'];
CodeParam := Application.Parameters.Values['code']; codeParam := Application.Parameters.Values['code'];
SaveUrlParamsToStorage(UserIdParam, TaskIdParam, CodeParam); DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
DMConnection.InitApp(
procedure
begin begin
DMConnection.SetClientConfig( if not Success then
procedure(Success: Boolean; ErrorMessage: string) begin
begin DisplayAccessDeniedModal(ErrorMessage);
if Success then 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
begin begin
if (UserIdParam <> '') and (TaskIdParam <> '') and (CodeParam <> '') then DisplayMainView(userIdParam, taskIdParam, codeParam);
begin end,
AuthService.Login( procedure(LoginError: string)
UserIdParam, TaskIdParam, CodeParam,
procedure
begin
DisplayMainView;
end,
procedure(LoginError: string)
begin
DisplayLoginView('Invalid or expired link.' + sLineBreak + LoginError);
end
);
Exit;
end;
if AuthService.Authenticated and (not AuthService.TokenExpired) then
DisplayMainView
else
DisplayLoginView;
end
else
begin begin
DisplayAccessDeniedModal(ErrorMessage); DisplayLoginView('Invalid or expired link.' + sLineBreak + LoginError);
end; end
end); );
end, Exit;
@UnauthorizedAccessProc end;
DisplayLoginView;
end
); );
end; end;
...@@ -152,6 +133,6 @@ begin ...@@ -152,6 +133,6 @@ begin
Application.Initialize; Application.Initialize;
Application.MainFormOnTaskbar := True; Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection); Application.CreateForm(TDMConnection, DMConnection);
StartApplication; DMConnection.InitApp(@StartApplication, @UnauthorizedAccessProc);
Application.Run; Application.Run;
end. end.
...@@ -99,11 +99,10 @@ ...@@ -99,11 +99,10 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release> <VerInfo_Release>8</VerInfo_Release>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebOutputPath>..\kgOrdersServer\bin\static</TMSWebOutputPath>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSURLParams>?user_id=1019&amp;task_id=4245&amp;code=749358</TMSURLParams> <TMSWebOutputPath>..\emT3XDataServer\bin</TMSWebOutputPath>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols> <DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
...@@ -140,24 +139,13 @@ ...@@ -140,24 +139,13 @@
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="Utils.pas"/> <DCCReference Include="Utils.pas"/>
<DCCReference Include="View.Tasks.pas">
<Form>FTasks</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksHTML.pas"> <DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form> <Form>FTasksHTML</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="View.TasksDataGrid.pas"> <DCCReference Include="View.Home.pas">
<Form>FTasksDataGrid</Form> <Form>FHome</Form>
<DesignClass>TWebForm</DesignClass> <FormType>dfm</FormType>
</DCCReference>
<DCCReference Include="View.TasksTabulator.pas">
<Form>FTasksTabulator</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksDBGrid.pas">
<Form>FTasksDBGrid</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<None Include="index.html"/> <None Include="index.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.7.1';
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);
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
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;
// 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
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;
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;
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;
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 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 = 42
Height = 14
Caption = 'App Title'
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 memoDebug: TWebMemo
Left = 45
Top = 361
Width = 471
Height = 83
ElementID = 'memo_debug'
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 xdwcMain: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 80
Top = 476
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">Koehler-Gibson Orders</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">
<div class="row">
<div id="pnl_main" class="col-12"></div>
</div>
<div class="row mt-3">
<div class="col-12">
<textarea class="form-control font-monospace" id="memo_debug" rows="4" placeholder="Debug output..."></textarea>
</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>
<!-- 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;
memoDebug: TWebMemo;
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 FTasksTabulator: TFTasksTabulator object FTasksHTML: TFTasksHTML
Width = 640 Width = 640
Height = 480 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
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
WidthPercent = 100.000000000000000000
OnClick = btnAddRowClick
end
object xdwcTasks: TXDataWebClient object xdwcTasks: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 506 Left = 506
Top = 92 Top = 92
end end
object xdwdsTasks: TXDataWebDataSet object xdwdsTasks: TXDataWebDataSet
Left = 468 Left = 506
Top = 182 Top = 148
object xdwdsTaskstaskID: TStringField object xdwdsTaskstaskID: TStringField
FieldName = 'taskId' FieldName = 'taskId'
end end
object xdwdsTasksitemNum: TIntegerField
FieldName = 'itemNum'
end
object xdwdsTasksapplication: TStringField object xdwdsTasksapplication: TStringField
FieldName = 'application' FieldName = 'application'
end end
...@@ -42,5 +71,8 @@ object FTasksTabulator: TFTasksTabulator ...@@ -42,5 +71,8 @@ object FTasksTabulator: TFTasksTabulator
object xdwdsTasksnotes: TStringField object xdwdsTasksnotes: TStringField
FieldName = 'notes' FieldName = 'notes'
end end
object xdwdsTaskstaskItemId: TStringField
FieldName = 'taskItemId'
end
end end
end end
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
</div> </div>
</div> </div>
<div id="data_grid_tasks" class="flex-grow-1 min-vh-0"></div> <div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
</div> </div>
object FTest: TFTest
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
object lblTest: TWebLabel
Left = 280
Top = 204
Width = 52
Height = 15
Caption = 'Test Form'
ElementID = 'lbl_test'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnTestApi: TWebButton
Left = 266
Top = 240
Width = 96
Height = 25
Caption = 'Test Api'
ChildOrder = 1
ElementID = 'btn_test_api'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnTestApiClick
end
object memoTestDebug: TWebMemo
Left = 224
Top = 286
Width = 185
Height = 89
ElementClassName = 'form-control'
ElementID = 'memo_test_debug'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
Lines.Strings = (
'')
SelLength = 0
SelStart = 2
ShowHint = False
WidthPercent = 100.000000000000000000
OnChange = memoTestDebugChange
end
object xdwcTest: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 464
Top = 208
end
end
<div class="container py-4">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<div id="lbl_test" class="h2 fw-semibold mb-1">Test Form</div>
<div class="text-muted">Quick API + JWT/version diagnostics</div>
</div>
<button id="btn_test_api" class="btn btn-primary">Test API</button>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary d-flex align-items-center justify-content-between">
<div class="fw-semibold">Debug Output</div>
</div>
<div class="card-body" style="min-height: 70vh;">
<textarea id="memo_test_debug"
class="form-control font-monospace h-100"
style="min-height: 65vh;"
rows="28"
placeholder="Click Test API to populate diagnostics..."></textarea>
</div>
</div>
</div>
unit View.Test;
interface
uses
System.SysUtils, System.Classes, JS, Web,
WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.StdCtrls,
XData.Web.Client,
App.Types, ConnectionModule, Vcl.StdCtrls, Vcl.Controls;
type
TFTest = class(TWebForm)
lblTest: TWebLabel;
btnTestApi: TWebButton;
memoTestDebug: TWebMemo;
xdwcTest: TXDataWebClient;
[async] procedure btnTestApiClick(Sender: TObject);
procedure memoTestDebugChange(Sender: TObject);
private
procedure AddLine(const s: string);
procedure DumpClaims;
public
end;
var
FTest: TFTest;
implementation
uses
Auth.Service;
procedure TFTest.AddLine(const s: string);
begin
memoTestDebug.Lines.Add(s);
end;
procedure TFTest.DumpClaims;
var
token: string;
payload: TJSObject;
begin
token := AuthService.GetToken;
AddLine('token:' + token);
//add null logic
AddLine('token present: ' + BoolToStr(token <> '', True));
if token = '' then
Exit;
AddLine('token expired: ' + BoolToStr(AuthService.TokenExpired, True));
AddLine('token exp (local): ' + DateTimeToStr(AuthService.TokenExpirationDate));
payload := TJSObject(AuthService.TokenPayload);
if not Assigned(payload) then
Exit;
asm
const p = payload;
const keys = Object.keys(p);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = p[k];
this.AddLine('claim ' + k + ': ' + String(v));
}
end;
end;
[async] procedure TFTest.btnTestApiClick(Sender: TObject);
var
response: TXDataClientResponse;
resultObj: TJSObject;
messageText: string;
requiredVersion: string;
begin
memoTestDebug.Visible := True;
memoTestDebug.Lines.Clear;
AddLine('--- request ---');
AddLine('api url: ' + DMConnection.ApiConnection.URL);
AddLine('auth url: ' + DMConnection.AuthConnection.URL);
AddLine('clientVersion const: ' + DMConnection.clientVersion);
AddLine('');
AddLine('--- token / claims ---');
DumpClaims;
messageText := 'hello from TFTest @ ' + DateTimeToStr(Now);
AddLine('');
AddLine('calling IApiService.TestApi ...');
try
response := await(xdwcTest.RawInvokeAsync('IApiService.TestApi', [messageText]));
except
on E: Exception do
begin
AddLine('');
AddLine('--- api exception ---');
AddLine(E.Message);
Exit;
end;
end;
AddLine('');
AddLine('--- api response ---');
if not Assigned(response.Result) then
begin
AddLine('response.Result is nil');
Exit;
end;
resultObj := TJSObject(response.Result);
AddLine('messageEcho: ' + JS.toString(resultObj['messageEcho']));
AddLine('serverTime: ' + JS.toString(resultObj['serverTime']));
AddLine('requiredWebClientVersion: ' + JS.toString(resultObj['requiredWebClientVersion']));
AddLine('note: ' + JS.toString(resultObj['note']));
requiredVersion := JS.toString(resultObj['requiredWebClientVersion']);
AddLine('');
AddLine('--- version compare (client-side) ---');
AddLine('clientVersion const: ' + DMConnection.clientVersion);
AddLine('requiredWebClientVersion: ' + requiredVersion);
AddLine('match: ' + BoolToStr(DMConnection.clientVersion = requiredVersion, True));
end;
procedure TFTest.memoTestDebugChange(Sender: TObject);
begin
end;
end.
[Paths]
HtmlPath=C:\Projects\emT3web\emT3XDataServer\bin\static
HtmlFile=index.html
DefaultURL=http://localhost:8000/emT3WebApp
SingleInstance=0
Debug=0
DebugManager=C:\RADTools\TMS\Products\tms.webcore\Bin\Win32\TMSDBGManager.exe
URL=http://localhost:8000/$(ProjectName)
URLParams=?user_id=1019&task_id=3997&url_code=123456
Browser=1
BrowserBin=
BrowserParams=
Electron=0
ElectronBuild=0
JSDebugger=1
{
"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%);
}
.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};
{$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>EM Systems Template App</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>
...@@ -3,10 +3,11 @@ object ApiDatabase: TApiDatabase ...@@ -3,10 +3,11 @@ object ApiDatabase: TApiDatabase
Height = 358 Height = 358
Width = 519 Width = 519
object ucETaskApi: TUniConnection object ucETaskApi: TUniConnection
AutoCommit = False
ProviderName = 'MySQL' ProviderName = 'MySQL'
Database = 'eTask' Database = 'eTask'
Username = 'root' Username = 'root'
Server = '192.168.116.132' Server = '192.168.102.129'
LoginPrompt = False LoginPrompt = False
Left = 71 Left = 71
Top = 65 Top = 65
...@@ -44,7 +45,7 @@ object ApiDatabase: TApiDatabase ...@@ -44,7 +45,7 @@ object ApiDatabase: TApiDatabase
object uqSaveTaskRow: TUniQuery object uqSaveTaskRow: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
SQL.Strings = ( SQL.Strings = (
'UPDATE web_tasks' 'UPDATE task_items'
'SET' 'SET'
' APPLICATION = :APPLICATION,' ' APPLICATION = :APPLICATION,'
' APP_VERSION = :APP_VERSION,' ' APP_VERSION = :APP_VERSION,'
...@@ -222,9 +223,10 @@ object ApiDatabase: TApiDatabase ...@@ -222,9 +223,10 @@ object ApiDatabase: TApiDatabase
object uqEnsureBlankRow: TUniQuery object uqEnsureBlankRow: TUniQuery
Connection = ucETaskApi Connection = ucETaskApi
SQL.Strings = ( SQL.Strings = (
'insert ignore into web_tasks (' 'insert ignore into task_items ('
' TASK_ITEM_ID,' ' TASK_ITEM_ID,'
' TASK_ID,' ' TASK_ID,'
' ITEM_NUM,'
' PROJECT_ID,' ' PROJECT_ID,'
' APPLICATION,' ' APPLICATION,'
' APP_VERSION,' ' APP_VERSION,'
...@@ -265,4 +267,180 @@ object ApiDatabase: TApiDatabase ...@@ -265,4 +267,180 @@ object ApiDatabase: TApiDatabase
Value = nil Value = nil
end> end>
end end
object uqTaskHeader: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' t.TASK_ID,'
' t.PARENT_ID,'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' t.PROJECT_ID,'
' t.SUBJECT,'
' p.NAME as PROJECT_NAME'
'from tasks t'
'left join project p'
' on p.PROJECT_ID = t.PROJECT_ID'
'where t.TASK_ID = :TASK_ID')
Left = 164
Top = 248
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqTaskHeaderTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqTaskHeaderPARENT_ID: TStringField
FieldName = 'PARENT_ID'
Size = 7
end
object uqTaskHeaderTASK_NUM_1: TIntegerField
FieldName = 'TASK_NUM_1'
end
object uqTaskHeaderTASK_NUM_2: TIntegerField
FieldName = 'TASK_NUM_2'
end
object uqTaskHeaderTASK_NUM_3: TIntegerField
FieldName = 'TASK_NUM_3'
end
object uqTaskHeaderTASK_NUM_4: TIntegerField
FieldName = 'TASK_NUM_4'
end
object uqTaskHeaderTASK_NUM_5: TIntegerField
FieldName = 'TASK_NUM_5'
end
object uqTaskHeaderTASK_NUM_6: TIntegerField
FieldName = 'TASK_NUM_6'
end
object uqTaskHeaderPROJECT_ID: TStringField
FieldName = 'PROJECT_ID'
Size = 7
end
object uqTaskHeaderSUBJECT: TStringField
FieldName = 'SUBJECT'
Size = 80
end
object uqTaskHeaderPROJECT_NAME: TStringField
FieldName = 'PROJECT_NAME'
ReadOnly = True
Size = 30
end
end
object uqTaskItemsForParent: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'select'
' ti.TASK_ITEM_ID,'
' ti.TASK_ID,'
' ti.ITEM_NUM,'
' ti.APPLICATION,'
' ti.APP_VERSION,'
' ti.TASK_DATE,'
' ti.STATUS_DATE,'
' ti.REPORTED_BY,'
' ti.ASSIGNED_TO,'
' ti.STATUS,'
' ti.FIXED_VERSION,'
' ti.FORM_SECTION,'
' ti.ISSUE,'
' ti.NOTES'
'from task_items ti'
'join tasks t'
' on t.TASK_ID = ti.TASK_ID'
'where ti.TASK_ID = :TASK_ID'
' or t.PARENT_ID = :TASK_ID'
'order by'
' case when ti.TASK_ID = :TASK_ID then 0 else 1 end,'
' t.TASK_NUM_1,'
' t.TASK_NUM_2,'
' t.TASK_NUM_3,'
' t.TASK_NUM_4,'
' t.TASK_NUM_5,'
' t.TASK_NUM_6,'
' ti.ITEM_NUM')
Left = 164
Top = 182
ParamData = <
item
DataType = ftUnknown
Name = 'TASK_ID'
Value = nil
end>
object uqTaskItemsForParentTASK_ITEM_ID: TStringField
FieldName = 'TASK_ITEM_ID'
Required = True
Size = 7
end
object uqTaskItemsForParentTASK_ID: TStringField
FieldName = 'TASK_ID'
Required = True
Size = 7
end
object uqTaskItemsForParentITEM_NUM: TSmallintField
FieldName = 'ITEM_NUM'
Required = True
end
object uqTaskItemsForParentAPPLICATION: TStringField
FieldName = 'APPLICATION'
Required = True
Size = 255
end
object uqTaskItemsForParentAPP_VERSION: TStringField
FieldName = 'APP_VERSION'
Required = True
Size = 50
end
object uqTaskItemsForParentTASK_DATE: TDateField
FieldName = 'TASK_DATE'
Required = True
end
object uqTaskItemsForParentSTATUS_DATE: TDateField
FieldName = 'STATUS_DATE'
Required = True
end
object uqTaskItemsForParentREPORTED_BY: TStringField
FieldName = 'REPORTED_BY'
Required = True
Size = 50
end
object uqTaskItemsForParentASSIGNED_TO: TStringField
FieldName = 'ASSIGNED_TO'
Required = True
Size = 50
end
object uqTaskItemsForParentSTATUS: TStringField
FieldName = 'STATUS'
Required = True
Size = 100
end
object uqTaskItemsForParentFIXED_VERSION: TStringField
FieldName = 'FIXED_VERSION'
Required = True
Size = 50
end
object uqTaskItemsForParentFORM_SECTION: TStringField
FieldName = 'FORM_SECTION'
Required = True
Size = 255
end
object uqTaskItemsForParentISSUE: TStringField
FieldName = 'ISSUE'
Required = True
Size = 1000
end
object uqTaskItemsForParentNOTES: TStringField
FieldName = 'NOTES'
Required = True
Size = 1000
end
end
end end
...@@ -33,6 +33,33 @@ type ...@@ -33,6 +33,33 @@ type
uqTaskItemsFORM_SECTION: TStringField; uqTaskItemsFORM_SECTION: TStringField;
uqTaskItemsISSUE: TStringField; uqTaskItemsISSUE: TStringField;
uqTaskItemsNOTES: TStringField; uqTaskItemsNOTES: TStringField;
uqTaskHeader: TUniQuery;
uqTaskHeaderTASK_ID: TStringField;
uqTaskHeaderPARENT_ID: TStringField;
uqTaskHeaderTASK_NUM_1: TIntegerField;
uqTaskHeaderTASK_NUM_2: TIntegerField;
uqTaskHeaderTASK_NUM_3: TIntegerField;
uqTaskHeaderTASK_NUM_4: TIntegerField;
uqTaskHeaderTASK_NUM_5: TIntegerField;
uqTaskHeaderTASK_NUM_6: TIntegerField;
uqTaskHeaderPROJECT_ID: TStringField;
uqTaskHeaderSUBJECT: TStringField;
uqTaskHeaderPROJECT_NAME: TStringField;
uqTaskItemsForParent: TUniQuery;
uqTaskItemsForParentTASK_ITEM_ID: TStringField;
uqTaskItemsForParentTASK_ID: TStringField;
uqTaskItemsForParentITEM_NUM: TSmallintField;
uqTaskItemsForParentAPPLICATION: TStringField;
uqTaskItemsForParentAPP_VERSION: TStringField;
uqTaskItemsForParentTASK_DATE: TDateField;
uqTaskItemsForParentSTATUS_DATE: TDateField;
uqTaskItemsForParentREPORTED_BY: TStringField;
uqTaskItemsForParentASSIGNED_TO: TStringField;
uqTaskItemsForParentSTATUS: TStringField;
uqTaskItemsForParentFIXED_VERSION: TStringField;
uqTaskItemsForParentFORM_SECTION: TStringField;
uqTaskItemsForParentISSUE: TStringField;
uqTaskItemsForParentNOTES: TStringField;
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure uqUsersCalcFields(DataSet: TDataSet); procedure uqUsersCalcFields(DataSet: TDataSet);
private private
......
...@@ -20,39 +20,38 @@ type ...@@ -20,39 +20,38 @@ type
taskItemId: string; taskItemId: string;
taskId: string; taskId: string;
itemNum: integer; itemNum: integer;
application: string; application: string;
version: string; version: string;
taskDate: TDateTime; taskDate: TDateTime;
reportedBy: string; reportedBy: string;
assignedTo: string; assignedTo: string;
status: string; status: string;
statusDate: Variant; statusDate: Variant;
fixedVersion: string; fixedVersion: string;
formSection: string; formSection: string;
issue: string; issue: string;
notes: string; notes: string;
end; end;
TTask = class type
public TTaskHeader = class
taskId: string; public
items: TList<TTaskItem>; taskId: string;
constructor Create; parentId: string;
destructor Destroy; override; taskNumber: string;
end; projectName: string;
subject: string;
TTasksList = class title: string;
public end;
count: integer;
data: TList<TTask>; TTaskItemsResponse = class
constructor Create; public
destructor Destroy; override; count: integer;
end; task: TTaskHeader;
items: TList<TTaskItem>;
constructor Create;
destructor Destroy; override;
end;
TTaskRowSave = class TTaskRowSave = class
public public
...@@ -76,39 +75,26 @@ type ...@@ -76,39 +75,26 @@ type
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable) IApiService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}'] ['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
function GetTaskItems(taskId: string): TTasksList; function GetTaskItems(taskId: string): TTaskItemsResponse;
[HttpPost] function SaveTaskRow(const Item: TTaskRowSave): Boolean; [HttpPost] function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
end; end;
implementation implementation
constructor TTask.Create; constructor TTaskItemsResponse.Create;
begin begin
inherited; inherited;
items := TList<TTaskItem>.Create; items := TList<TTaskItem>.Create;
end; end;
destructor TTask.Destroy; destructor TTaskItemsResponse.Destroy;
begin begin
items.Free; items.Free;
inherited; inherited;
end; end;
constructor TTasksList.Create;
begin
inherited;
data := TList<TTask>.Create;
end;
destructor TTasksList.Destroy;
begin
data.Free;
inherited;
end;
initialization initialization
RegisterServiceType(TypeInfo(IApiService)); RegisterServiceType(TypeInfo(IApiService));
end. end.
...@@ -147,7 +147,7 @@ var ...@@ -147,7 +147,7 @@ var
userState: Integer; userState: Integer;
jwt: TJWT; jwt: TJWT;
begin begin
Logger.Log(3, Format('AuthService.Login - UserID: "%s", TaskID: "%s"', [userId, taskId])); Logger.Log(3, Format('AuthService.Login - UserID: "%s", TaskID: "%s", Code: "%s"', [userId, taskId, urlCode]));
try try
userState := CheckUrlLogin(userId, taskId, urlCode); userState := CheckUrlLogin(userId, taskId, urlCode);
......
<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>
<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>
<div id="wrapper" class="d-flex flex-column vh-100">
<nav class="navbar navbar-expand navbar-light bg-light" style="margin-bottom: 0px">
<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>
<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">
<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>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownMenuLink">
<li>
<a class="dropdown-item" id="dropdown.menu.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">
<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>
</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>
</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>
</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>
</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>
</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>
</div>
</div>
</div>
</div>
<div class="container h-100 d-flex flex-column mt-0 py-0" style="max-width: 100%;"> <div class="container-fluid p-2 d-flex flex-column h-100">
<div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0"> <div class="d-flex align-items-center justify-content-between mb-2 flex-shrink-0">
<h5 class="mb-0" id="lbl_project_name"></h5> <h5 class="mb-0" id="lbl_project_name"></h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button id="btn_add_row" class="btn btn-sm btn-outline-success">Add Row</button> <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> <button id="btn_reload" class="btn btn-sm btn-outline-primary">Reload</button>
</div> </div>
</div> </div>
<div id="db_grid_tasks" class="flex-grow-1" style="min-height:0;"></div> <div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
</div> </div>
{
"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] [Settings]
MemoLogLevel=4 MemoLogLevel=4
FileLogLevel=4 FileLogLevel=4
webClientVersion=0.0.1 webClientVersion=0.7.1
LogFileNum=175 LogFileNum=175
[Database] [Database]
Server=192.168.116.128 --Server=192.168.116.128
--Server=192.168.102.129 Server=192.168.102.129
--Server=192.168.75.133 --Server=192.168.75.133
--Server=192.168.159.10 --Server=192.168.159.10
Database=eTask Database=eTask
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!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="emT3web.js" type="text/javascript"></script>
</head>
<body>
</body>
<script type="text/javascript">rtl.run();</script>
</html>
<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">Koehler-Gibson Orders</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">
<div class="row">
<div id="pnl_main" class="col-12"></div>
</div>
<div class="row mt-3">
<div class="col-12">
<textarea class="form-control font-monospace" id="memo_debug" rows="4" placeholder="Debug output..."></textarea>
</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>
<!-- 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>
<div class="container-fluid p-2 d-flex flex-column h-100">
<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>
</div>
<div id="tasks_table_host" class="flex-grow-1 min-vh-0"></div>
</div>
<div class="container py-4">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<div id="lbl_test" class="h2 fw-semibold mb-1">Test Form</div>
<div class="text-muted">Quick API + JWT/version diagnostics</div>
</div>
<button id="btn_test_api" class="btn btn-primary">Test API</button>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary d-flex align-items-center justify-content-between">
<div class="fw-semibold">Debug Output</div>
</div>
<div class="card-body" style="min-height: 70vh;">
<textarea id="memo_test_debug"
class="form-control font-monospace h-100"
style="min-height: 65vh;"
rows="28"
placeholder="Click Test API to populate diagnostics..."></textarea>
</div>
</div>
</div>
{
"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%);
}
.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);
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!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>EM Systems Template App</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="emT3WebApp.js"></script>
</head>
<body>
<noscript>Your browser does not support JavaScript!</noscript>
<script>rtl.run();</script>
</body>
</html>
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