Commit 95158366 by Mac Stephens

WIP building new client and adding item_num system

parent ba563f22
......@@ -16,3 +16,7 @@ emT3XDataServer/Win32/
*.txt
emT3Web/Win32/Debug/
emT3Web/__recovery/
emT3WebApp/__history/
......@@ -110,6 +110,6 @@ String TfMain::BuildLaunchUrl(const String &baseUrl, const String &userId, const
String qTaskId = TNetEncoding::URL->Encode(taskId);
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
Width = 337
Height = 23
TabOrder = 4
Text = 'http://127.0.0.1:8000/emT3webClient/index.html'
Text = 'http://localhost:8000/emT3webApp/index.html'
end
object edtExpSeconds: TEdit
Left = 500
......
......@@ -120,10 +120,6 @@ end;
procedure TAuthService.Logout;
begin
DeleteToken;
window.localStorage.removeItem('EMT3_USER_ID');
window.localStorage.removeItem('EMT3_TASK_ID');
window.localStorage.removeItem('EMT3_CODE');
end;
procedure TAuthService.SetToken(AToken: string);
......
......@@ -17,9 +17,6 @@ type
procedure AuthConnectionError(Error: TXDataWebConnectionError);
private
FUnauthorizedAccessProc: TUnauthorizedAccessProc;
FUserIdParam: string;
FTaskIdParam: string;
FCodeParam: string;
public
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
'Close')
Opacity = 0.200000000000000000
end
object edtTaskIdMain: TWebEdit
Left = 220
Top = 170
Width = 121
Height = 22
ChildOrder = 6
ElementClassName = 'form-control'
ElementID = 'edt_task_id_main'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 44
......
......@@ -5,6 +5,9 @@
<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">
......
......@@ -6,7 +6,7 @@ uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils;
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils, View.Home;
type
TFViewMain = class(TWebForm)
......@@ -17,25 +17,23 @@ type
XDataWebClient: TXDataWebClient;
lblVersion: TWebLabel;
lblAppTitle: TWebLabel;
edtTaskIdMain: TWebEdit;
procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject);
procedure wllblLogoutClick(Sender: TObject);
private
{ Private declarations }
FUserInfo: string;
FSearchSettings: string;
FChildForm: TWebForm;
FTasksHtmlForm: TWebForm;
FLogoutProc: TLogoutProc;
FSearchProc: TSearchProc;
procedure ShowCrudForm( AFormClass: TWebFormClass );
procedure ConfirmLogout;
procedure LoadTasksHtmlForm;
procedure LoadHomeForm;
public
{ Public declarations }
class procedure Display(LogoutProc: TLogoutProc);
procedure ShowForm( AFormClass: TWebFormClass );
var
search: string;
change: boolean;
FUserId: string;
FTaskId: string;
FCode: string;
class procedure Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
end;
var
......@@ -45,30 +43,40 @@ implementation
uses
Auth.Service,
View.Tasks,
View.TasksHTML,
View.TasksDataGrid,
View.TasksDBGrid;
View.TasksHTML;
{$R *.dfm}
class procedure TFViewMain.Display(LogoutProc: TLogoutProc);
class procedure TFViewMain.Display(LogoutProc: TLogoutProc; const AUserId, ATaskId, ACode: string);
begin
if Assigned(FViewMain) then
FViewMain.Free;
FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := LogoutProc;
FViewMain.FUserId := AUserId;
FViewMain.FTaskId := ATaskId;
FViewMain.FCode := ACode;
console.log('Main form values assigned after create');
console.log('UserId=' + FViewMain.FUserId);
console.log('TaskId=' + FViewMain.FTaskId);
console.log('Code=' + FViewMain.FCode);
end;
procedure TFViewMain.WebFormCreate(Sender: TObject);
begin
console.log('TFViewMain.WebFormCreate fired');
// FChildForm := nil;
console.log('About to ShowForm(TFTasksHTML), host=' + WebPanel1.ElementID);
ShowForm(TFTasksHTML);
lblAppTitle.Caption := 'emT3web';
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;
......@@ -99,26 +107,28 @@ begin
);
end;
procedure TFViewMain.ShowCrudForm(AFormClass: TWebFormClass);
procedure TFViewMain.LoadTasksHtmlForm;
begin
ShowForm(AFormClass);
end;
if Assigned(FTasksHtmlForm) then
FTasksHtmlForm.Free;
console.log('About to create TFTasksHTML, host=' + WebPanel1.ElementID);
console.log('Main form task id is: ' + FTaskId);
procedure TFViewMain.ShowForm(AFormClass: TWebFormClass);
begin
if Assigned(FChildForm) then
FChildForm.Free;
Application.CreateForm(AFormClass, WebPanel1.ElementID, FChildForm);
console.log('CreateForm called, FChildForm assigned: ' + BoolToStr(Assigned(FChildForm)));
FTasksHtmlForm := TFTasksHTML.CreateForm(WebPanel1.ElementID, FTaskId);
end;
//procedure TFViewMain.ShowTasksForm(Info: string);
//begin
// if Assigned(FChildForm) then
// FChildForm.Free;
// FChildForm := TFViewUsers.CreateForm(WebPanel1.ElementID, Info);
//end;
procedure TFViewMain.LoadHomeForm;
begin
if Assigned(FTasksHtmlForm) then
FTasksHtmlForm.Free;
console.log('About to create TFHome, host=' + WebPanel1.ElementID);
console.log('Main form task id is: ' + FTaskId);
console.log('Main form user id is: ' + FUserId);
console.log('Main form code is: ' + FCode);
FTasksHtmlForm := TFHome.CreateForm(WebPanel1.ElementID, FTaskId, FUserId, FCode);
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
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnShow = WebFormShow
OnCreate = WebFormCreate
object btnReload: TWebButton
Left = 78
Top = 88
......
......@@ -30,7 +30,7 @@ type
xdwdsTaskstaskItemId: TStringField;
procedure btnAddRowClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure WebFormShow(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
private
FTaskId: string;
[async] procedure LoadTasks(const ATaskId: string);
......@@ -49,7 +49,10 @@ type
[async] procedure SaveRow(AIndex: Integer);
procedure EditorBlur(Event: TJSEvent);
procedure InitializeForm;
public
class function CreateForm(AElementID, ATaskId: string): TWebForm;
end;
var
......@@ -62,11 +65,29 @@ uses
{$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
console.log('TFTasksHTML.WebFormShow fired');
FTaskId := window.localStorage.getItem('EMT3_TASK_ID');
console.log('TFTasksHTML.InitializeForm fired');
console.log('The task id is: ' + FTaskId);
if FTaskId = '' then
......@@ -82,7 +103,7 @@ begin
DMConnection.ApiConnection.Open(
procedure
begin
LoadTasks(FTaskID);
LoadTasks(FTaskId);
end
);
end
......@@ -255,6 +276,11 @@ begin
end;
procedure TFTasksHTML.WebFormCreate(Sender: TObject);
begin
console.log('TFTasksHTML.WebFormCreate fired');
end;
[async] procedure TFTasksHTML.LoadTasks(const ATaskId: string);
var
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;
{$R *.dres}
uses
Vcl.Forms,
System.SysUtils,
......@@ -14,11 +16,8 @@ uses
App.Config in 'App.Config.pas',
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
Utils in 'Utils.pas',
View.Tasks in 'View.Tasks.pas' {FTasks: TWebForm} {*.html},
View.TasksHTML in 'View.TasksHTML.pas' {FTasksHTML: TWebForm} {*.html},
View.TasksDataGrid in 'View.TasksDataGrid.pas' {FTasksDataGrid: TWebForm} {*.html},
View.TasksTabulator in 'View.TasksTabulator.pas' {FTasksTabulator: TWebForm} {*.html},
View.TasksDBGrid in 'View.TasksDBGrid.pas' {FTasksDBGrid: TWebForm} {*.html};
View.Home in 'View.Home.pas' {FHome: TWebForm} {*.html};
{$R *.res}
......@@ -74,11 +73,9 @@ begin
DisplayAccessDeniedModal(AMessage);
end;
procedure DisplayMainView;
procedure DisplayMainView(const AUserId, ATaskId, ACode: string);
begin
if Assigned(FViewLogin) then
FViewLogin.Free;
TFViewMain.Display(@DisplayLoginView);
TFViewMain.Display(@DisplayLoginView, AUserId, ATaskId, ACode);
end;
procedure UnauthorizedAccessProc(AMessage: string);
......@@ -86,44 +83,38 @@ begin
DisplayLoginView(AMessage);
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;
var
UserIdParam: string;
TaskIdParam: string;
CodeParam: string;
userIdParam: string;
taskIdParam: string;
codeParam: string;
begin
UserIdParam := Application.Parameters.Values['user_id'];
TaskIdParam := Application.Parameters.Values['task_id'];
CodeParam := Application.Parameters.Values['code'];
SaveUrlParamsToStorage(UserIdParam, TaskIdParam, CodeParam);
userIdParam := Application.Parameters.Values['user_id'];
taskIdParam := Application.Parameters.Values['task_id'];
codeParam := Application.Parameters.Values['code'];
DMConnection.InitApp(
procedure
begin
DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
begin
if Success then
if not Success then
begin
DisplayAccessDeniedModal(ErrorMessage);
Exit;
end;
if AuthService.Authenticated and (not AuthService.TokenExpired) then
begin
if (UserIdParam <> '') and (TaskIdParam <> '') and (CodeParam <> '') then
DisplayMainView(userIdParam, taskIdParam, codeParam);
Exit;
end;
if (userIdParam <> '') and (taskIdParam <> '') and (codeParam <> '') then
begin
AuthService.Login(
UserIdParam, TaskIdParam, CodeParam,
userIdParam, taskIdParam, codeParam,
procedure
begin
DisplayMainView;
DisplayMainView(userIdParam, taskIdParam, codeParam);
end,
procedure(LoginError: string)
begin
......@@ -133,18 +124,8 @@ begin
Exit;
end;
if AuthService.Authenticated and (not AuthService.TokenExpired) then
DisplayMainView
else
DisplayLoginView;
end
else
begin
DisplayAccessDeniedModal(ErrorMessage);
end;
end);
end,
@UnauthorizedAccessProc
);
end;
......@@ -152,6 +133,6 @@ begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection);
StartApplication;
DMConnection.InitApp(@StartApplication, @UnauthorizedAccessProc);
Application.Run;
end.
......@@ -99,11 +99,10 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>8</VerInfo_Release>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebBrowser>1</TMSWebBrowser>
<TMSWebOutputPath>..\kgOrdersServer\bin\static</TMSWebOutputPath>
<TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSURLParams>?user_id=1019&amp;task_id=4245&amp;code=749358</TMSURLParams>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<TMSWebOutputPath>..\emT3XDataServer\bin</TMSWebOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
......@@ -140,24 +139,13 @@
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="Utils.pas"/>
<DCCReference Include="View.Tasks.pas">
<Form>FTasks</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksHTML.pas">
<Form>FTasksHTML</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksDataGrid.pas">
<Form>FTasksDataGrid</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksTabulator.pas">
<Form>FTasksTabulator</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.TasksDBGrid.pas">
<Form>FTasksDBGrid</Form>
<DCCReference Include="View.Home.pas">
<Form>FHome</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<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
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
Connection = DMConnection.ApiConnection
Left = 506
Top = 92
end
object xdwdsTasks: TXDataWebDataSet
Left = 468
Top = 182
Left = 506
Top = 148
object xdwdsTaskstaskID: TStringField
FieldName = 'taskId'
end
object xdwdsTasksitemNum: TIntegerField
FieldName = 'itemNum'
end
object xdwdsTasksapplication: TStringField
FieldName = 'application'
end
......@@ -42,5 +71,8 @@ object FTasksTabulator: TFTasksTabulator
object xdwdsTasksnotes: TStringField
FieldName = 'notes'
end
object xdwdsTaskstaskItemId: TStringField
FieldName = 'taskItemId'
end
end
end
......@@ -7,7 +7,8 @@
</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>
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
Height = 358
Width = 519
object ucETaskApi: TUniConnection
AutoCommit = False
ProviderName = 'MySQL'
Database = 'eTask'
Username = 'root'
Server = '192.168.116.132'
Server = '192.168.102.129'
LoginPrompt = False
Left = 71
Top = 65
......@@ -44,7 +45,7 @@ object ApiDatabase: TApiDatabase
object uqSaveTaskRow: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'UPDATE web_tasks'
'UPDATE task_items'
'SET'
' APPLICATION = :APPLICATION,'
' APP_VERSION = :APP_VERSION,'
......@@ -222,9 +223,10 @@ object ApiDatabase: TApiDatabase
object uqEnsureBlankRow: TUniQuery
Connection = ucETaskApi
SQL.Strings = (
'insert ignore into web_tasks ('
'insert ignore into task_items ('
' TASK_ITEM_ID,'
' TASK_ID,'
' ITEM_NUM,'
' PROJECT_ID,'
' APPLICATION,'
' APP_VERSION,'
......@@ -265,4 +267,180 @@ object ApiDatabase: TApiDatabase
Value = nil
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
......@@ -33,6 +33,33 @@ type
uqTaskItemsFORM_SECTION: TStringField;
uqTaskItemsISSUE: 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 uqUsersCalcFields(DataSet: TDataSet);
private
......
......@@ -20,36 +20,35 @@ type
taskItemId: string;
taskId: string;
itemNum: integer;
application: string;
version: string;
taskDate: TDateTime;
reportedBy: string;
assignedTo: string;
status: string;
statusDate: Variant;
fixedVersion: string;
formSection: string;
issue: string;
notes: string;
end;
TTask = class
type
TTaskHeader = class
public
taskId: string;
items: TList<TTaskItem>;
constructor Create;
destructor Destroy; override;
parentId: string;
taskNumber: string;
projectName: string;
subject: string;
title: string;
end;
TTasksList = class
TTaskItemsResponse = class
public
count: integer;
data: TList<TTask>;
task: TTaskHeader;
items: TList<TTaskItem>;
constructor Create;
destructor Destroy; override;
end;
......@@ -76,39 +75,26 @@ type
[ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable)
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
function GetTaskItems(taskId: string): TTasksList;
[HttpPost] function SaveTaskRow(const Item: TTaskRowSave): Boolean;
function GetTaskItems(taskId: string): TTaskItemsResponse;
[HttpPost] function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
end;
implementation
constructor TTask.Create;
constructor TTaskItemsResponse.Create;
begin
inherited;
items := TList<TTaskItem>.Create;
end;
destructor TTask.Destroy;
destructor TTaskItemsResponse.Destroy;
begin
items.Free;
inherited;
end;
constructor TTasksList.Create;
begin
inherited;
data := TList<TTask>.Create;
end;
destructor TTasksList.Destroy;
begin
data.Free;
inherited;
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
end.
......@@ -10,7 +10,7 @@ uses
Api.Service,
Api.Database,
Common.Logging,
System.SysUtils;
System.SysUtils, JSON, System.IniFiles;
type
[ServiceImplementation]
......@@ -19,11 +19,15 @@ type
apiDB: TApiDatabase;
private
procedure EnsureBlankWebTaskRow(const taskId: string);
function SaveTaskRow(const Item: TTaskRowSave): Boolean;
function BuildTaskNumber: string;
function BuildTaskTitle(const taskNumber, projectName, subject: string): string;
function SaveTaskRow(Item: TTaskRowSave): Boolean;
function TestApi(messageText: string): TJSONObject;
function GetWebClientVersion: string;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function GetTaskItems(taskId: string): TTasksList;
function GetTaskItems(taskId: string): TTaskItemsResponse;
end;
implementation
......@@ -42,17 +46,102 @@ begin
inherited;
end;
function TApiService.GetTaskItems(taskId: string): TTasksList;
function TApiService.GetTaskItems(taskId: string): TTaskItemsResponse;
var
task: TTask;
taskHeader: TTaskHeader;
item: TTaskItem;
useParentView: Boolean;
begin
Logger.Log(4, Format('ApiService.GetTaskItems - TASK_ID="%s"', [taskId]));
Result := TTasksList.Create;
Result := TTaskItemsResponse.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
try
apiDB.uqTaskHeader.Close;
apiDB.uqTaskHeader.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskHeader.Open;
if apiDB.uqTaskHeader.IsEmpty then
begin
Logger.Log(2, Format('ApiService.GetTaskItems - no task header found for TASK_ID="%s"', [taskId]));
Result.count := 0;
Exit;
end;
taskHeader := TTaskHeader.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(taskHeader);
taskHeader.taskId := apiDB.uqTaskHeaderTASK_ID.AsString;
taskHeader.parentId := apiDB.uqTaskHeaderPARENT_ID.AsString;
taskHeader.taskNumber := BuildTaskNumber;
taskHeader.projectName := apiDB.uqTaskHeaderPROJECT_NAME.AsString;
taskHeader.subject := apiDB.uqTaskHeaderSUBJECT.AsString;
taskHeader.title := BuildTaskTitle(
taskHeader.taskNumber,
taskHeader.projectName,
taskHeader.subject
);
Result.task := taskHeader;
useParentView := Trim(Result.task.parentId) = '';
useParentView := useParentView or (Trim(Result.task.parentId) = '0');
if useParentView then
begin
apiDB.uqTaskItemsForParent.Close;
apiDB.uqTaskItemsForParent.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskItemsForParent.Open;
if apiDB.uqTaskItemsForParent.IsEmpty then
begin
Logger.Log(4, Format('ApiService.GetTaskItems - no rows for TASK_ID="%s", ensuring blank row', [taskId]));
EnsureBlankWebTaskRow(taskId);
apiDB.uqTaskItemsForParent.Close;
apiDB.uqTaskItemsForParent.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskItemsForParent.Open;
end;
while not apiDB.uqTaskItemsForParent.Eof do
begin
item := TTaskItem.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(item);
item.taskItemId := apiDB.uqTaskItemsForParentTASK_ITEM_ID.AsString;
item.taskId := apiDB.uqTaskItemsForParentTASK_ID.AsString;
item.itemNum := apiDB.uqTaskItemsForParentITEM_NUM.AsInteger;
item.application := apiDB.uqTaskItemsForParentAPPLICATION.AsString;
item.version := apiDB.uqTaskItemsForParentAPP_VERSION.AsString;
if apiDB.uqTaskItemsForParentTASK_DATE.IsNull then
item.taskDate := 0
else
item.taskDate := apiDB.uqTaskItemsForParentTASK_DATE.AsDateTime;
item.reportedBy := apiDB.uqTaskItemsForParentREPORTED_BY.AsString;
item.assignedTo := apiDB.uqTaskItemsForParentASSIGNED_TO.AsString;
item.status := apiDB.uqTaskItemsForParentSTATUS.AsString;
if apiDB.uqTaskItemsForParentSTATUS_DATE.IsNull then
item.statusDate := Null
else
item.statusDate := apiDB.uqTaskItemsForParentSTATUS_DATE.AsDateTime;
item.fixedVersion := apiDB.uqTaskItemsForParentFIXED_VERSION.AsString;
item.formSection := apiDB.uqTaskItemsForParentFORM_SECTION.AsString;
item.issue := apiDB.uqTaskItemsForParentISSUE.AsString;
item.notes := apiDB.uqTaskItemsForParentNOTES.AsString;
Result.items.Add(item);
apiDB.uqTaskItemsForParent.Next;
end;
end
else
begin
apiDB.uqTaskItems.Close;
apiDB.uqTaskItems.ParamByName('TASK_ID').AsString := taskId;
apiDB.uqTaskItems.Open;
......@@ -67,20 +156,6 @@ begin
apiDB.uqTaskItems.Open;
end;
if apiDB.uqTaskItems.IsEmpty then
begin
Logger.Log(2, Format('ApiService.GetTaskItems - still no rows after ensure blank for TASK_ID="%s"', [taskId]));
Result.count := 0;
Exit;
end;
task := TTask.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(task);
task.taskId := taskId;
Result.data.Add(task);
while not apiDB.uqTaskItems.Eof do
begin
item := TTaskItem.Create;
......@@ -100,7 +175,6 @@ begin
item.reportedBy := apiDB.uqTaskItemsREPORTED_BY.AsString;
item.assignedTo := apiDB.uqTaskItemsASSIGNED_TO.AsString;
item.status := apiDB.uqTaskItemsSTATUS.AsString;
if apiDB.uqTaskItemsSTATUS_DATE.IsNull then
......@@ -113,13 +187,14 @@ begin
item.issue := apiDB.uqTaskItemsISSUE.AsString;
item.notes := apiDB.uqTaskItemsNOTES.AsString;
task.items.Add(item);
Result.items.Add(item);
apiDB.uqTaskItems.Next;
end;
end;
Result.count := Result.data.Count;
Logger.Log(4, Format('ApiService.GetTaskItems - returned %d task(s)', [Result.count]));
Result.count := Result.items.Count;
Logger.Log(4, Format('ApiService.GetTaskItems - returned %d item(s)', [Result.count]));
except
on E: Exception do
begin
......@@ -129,6 +204,7 @@ begin
end;
end;
procedure TApiService.EnsureBlankWebTaskRow(const taskId: string);
begin
Logger.Log(4, Format('ApiService.EnsureBlankWebTaskRow - TASK_ID="%s"', [taskId]));
......@@ -145,7 +221,51 @@ begin
end;
end;
function TApiService.SaveTaskRow(const Item: TTaskRowSave): Boolean;
function TApiService.BuildTaskNumber: string;
procedure AddPart(const value: string);
var
s: string;
begin
s := Trim(value);
if s = '' then
Exit;
if Result = '' then
Result := s
else
Result := Result + '.' + s;
end;
begin
Result := '';
AddPart(apiDB.uqTaskHeaderTASK_NUM_1.AsString);
AddPart(apiDB.uqTaskHeaderTASK_NUM_2.AsString);
AddPart(apiDB.uqTaskHeaderTASK_NUM_3.AsString);
AddPart(apiDB.uqTaskHeaderTASK_NUM_4.AsString);
AddPart(apiDB.uqTaskHeaderTASK_NUM_5.AsString);
AddPart(apiDB.uqTaskHeaderTASK_NUM_6.AsString);
end;
function TApiService.BuildTaskTitle(const taskNumber, projectName, subject: string): string;
begin
Result := 'Task';
if Trim(taskNumber) <> '' then
Result := Result + ' - ' + Trim(taskNumber);
if Trim(projectName) <> '' then
Result := Result + ' | ' + Trim(projectName);
if Trim(subject) <> '' then
Result := Result + ' | ' + Trim(subject);
end;
function TApiService.SaveTaskRow(Item: TTaskRowSave): Boolean;
function ParseDateOrZero(const S: string; out D: TDateTime): Boolean;
begin
......@@ -200,6 +320,37 @@ begin
end;
end;
function TApiService.TestApi(messageText: string): TJSONObject;
var
requiredVersion: string;
begin
Logger.Log(3, 'IApiService.TestApi called');
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
requiredVersion := GetWebClientVersion;
Result.AddPair('messageEcho', messageText);
Result.AddPair('serverTime', DateTimeToStr(Now));
Result.AddPair('requiredWebClientVersion', requiredVersion);
Result.AddPair('note', 'If this endpoint is reachable, JWT auth passed. Version enforcement on every API call is a separate step (middleware).');
end;
function TApiService.GetWebClientVersion: string;
var
iniFile: TIniFile;
begin
iniFile := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
try
Result := iniFile.ReadString('Settings', 'webClientVersion', '');
finally
iniFile.Free;
end;
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
RegisterServiceType(TApiService);
......
......@@ -147,7 +147,7 @@ var
userState: Integer;
jwt: TJWT;
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
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">
<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="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>
{
"AuthUrl" : "http://localhost:2001/emsys/emt3/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emt3/api/"
}
/* Note: Base layout */
html, body{
height:100%;
margin:0;
}
#wrapper{
height:100vh;
display:flex;
flex-direction:column;
min-height:0;
}
/* Note: Embedded forms must be allowed to shrink inside flex containers */
#main\.webpanel{
min-height:0;
flex:1 1 auto;
display:flex;
flex-direction:column;
}
#main\.webpanel > *{
min-height:0;
}
/* Note: Primary button color */
.btn-primary{
background-color:#286090 !important;
border-color:#286090 !important;
color:#fff !important;
}
.btn-primary:hover{
background-color:#204d74 !important;
border-color:#204d74 !important;
}
/* Note: Navbar tweaks */
#view\.main\.apptitle{
display:flex;
align-items:center;
}
.navbar-nav .nav-link.active{
color:#fff !important;
background-color:#004F84 !important;
font-weight:700;
}
.navbar-nav .nav-link:hover{
color:#fff !important;
background-color:#286090 !important;
}
.navbar-toggler{
display:none;
}
/* Note: Dropdown menu items */
.dropdown-menu a{
display:flex;
align-items:center;
width:100%;
padding:.5rem 1rem;
color:#000;
text-decoration:none;
}
.dropdown-menu a:hover{
background-color:#204d74;
color:#fff;
}
.dropdown-menu a span{
flex-grow:1;
}
/* Note: Login card (used on login view) */
.login-card{
display:inline-block;
width:300px;
padding:0;
border-radius:10px;
box-shadow:0 4px 8px rgba(0,0,0,.1);
background-color:#fff;
}
/* Note: Validation helpers */
.is-invalid .form-check-input{
border:1px solid #dc3545 !important;
}
.is-invalid .form-check-label{
color:#dc3545 !important;
}
/* Note: Toast animation */
@keyframes slideInLeft{
from{transform:translateX(-120%);opacity:0;}
to{transform:translateX(0);opacity:1;}
}
.toast.slide-in{
animation:slideInLeft .4s ease-out forwards;
}
/* Note: Spinner overlay */
#spinner{
position:fixed !important;
z-index:9999 !important;
top:50%;
left:50%;
transform:translate(-50%,-50%);
}
/* Note: TasksHTML (table experiment) */
#tasks_table_host{
height:100%;
min-height:0;
}
#tasks_table_host .tasks-vscroll{
height:100%;
overflow-y:auto;
overflow-x:hidden;
}
#tasks_table_host .tasks-hscroll{
overflow-x:auto;
}
#tasks_table_host .tasks-hscroll table{
width:max-content;
min-width:100%;
table-layout:fixed;
}
#tasks_table_host thead th{
position:sticky;
top:0;
z-index:2;
background:var(--bs-body-bg);
}
#tasks_table_host td,
#tasks_table_host th{
padding:.25rem;
}
#tasks_table_host .nowrap-cell{white-space:nowrap;}
#tasks_table_host .wrap-cell{white-space:normal;word-break:break-word;}
#tasks_table_host .cell-input,
#tasks_table_host .cell-textarea{
border:0;
background:transparent;
border-radius:0;
padding:0;
margin:0;
box-shadow:none;
}
#tasks_table_host .cell-input:focus,
#tasks_table_host .cell-textarea:focus{
outline:0;
box-shadow:inset 0 -2px 0 var(--bs-primary);
}
#tasks_table_host .cell-textarea{
resize:none;
overflow:hidden;
white-space:pre-wrap;
}
/* Note: TasksDataGrid (TWebDataGrid experiment) */
#data_grid_tasks{
height:100%;
min-height:0;
}
#data_grid_tasks .ag-cell{
line-height:1.25;
padding-top:4px;
padding-bottom:4px;
}
#data_grid_tasks .ag-cell-inline-editing textarea{
line-height:1.25;
padding:4px 6px;
resize:none;
height:100%;
box-sizing:border-box;
}
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #204d74;
margin: -5px 0 0 -5px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
[Settings]
MemoLogLevel=4
FileLogLevel=4
webClientVersion=0.0.1
webClientVersion=0.7.1
LogFileNum=175
[Database]
Server=192.168.116.128
--Server=192.168.102.129
--Server=192.168.116.128
Server=192.168.102.129
--Server=192.168.75.133
--Server=192.168.159.10
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