Commit ccf05c5a by Mac Stephens

Added twebpanel still working on sizing

parent 60a81849
......@@ -8,3 +8,13 @@ emT3webServer/Source/__history/
emT3webServer/Win32/Debug/
emT3webServer/bin/static/
emT3webClient/__history/
emT3webClient/css/__history/
emT3webClient/Win32/Debug/
*.local
*.identcache
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 = 'KG_ORDERS_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: 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: 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],
@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
OnError = ApiConnectionError
OnRequest = ApiConnectionRequest
OnResponse = ApiConnectionResponse
Left = 48
Top = 80
end
object AuthConnection: TXDataWebConnection
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;
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.0.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,
Utils;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
procedure TDMConnection.ApiConnectionError(Error: TXDataWebConnectionError);
begin
ShowErrorModal(Error.ToString);
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);
begin
ShowErrorModal(Error.ToString);
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.
This source diff could not be displayed because it is too large. You can view the blob instead.
<nav class="navbar navbar-light bg-light login-navbar">
<div class="container-fluid">
<a class="navbar-brand" href="#">Koehler-Gibson Orders</a>
</div>
</nav>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-auto">
<img id="kgpicture" style="width: 250px; height: 250px;">
</div>
<div class="col-md-6 col-lg-4">
<div class="card login-card">
<div class="card-header">
<h3 id="view.login.title" class="fs-6 card-title">Please Sign In</h3>
</div>
<div class="card-body">
<div role="form">
<div id="view.login.message" class="alert alert-danger">
<button id="view.login.message.button" type="button" class="btn-close" aria-label="Close"></button>
<span id="view.login.message.label"></span>
</div>
<fieldset>
<div class="mb-3">
<input id="view.login.edtusername" class="form-control" type="text" autofocus placeholder="Username">
</div>
<div class="mb-3">
<input id="view.login.edtpassword" class="form-control" type="password" placeholder="Password">
</div>
<div class="mb-3">
<button id="view.login.btnlogin" class="btn btn-primary w-100">Login</button>
</div>
<div class="text-end text-muted small mt-1">
<span id="lbl_client_version"></span>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
</div>
unit View.Login;
interface
uses
System.SysUtils, System.Classes, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
JS, XData.Web.Connection, WEBLib.ExtCtrls,
App.Types, ConnectionModule, XData.Web.Client, Vcl.Imaging.pngimage;
type
TFViewLogin = class(TWebForm)
WebLabel1: TWebLabel;
edtUsername: TWebEdit;
edtPassword: TWebEdit;
btnLogin: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
btnCloseNotification: TWebButton;
XDataWebClient: TXDataWebClient;
WebImageControl1: TWebImageControl;
lblClientVersion: TWebLabel;
procedure btnLoginClick(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormShow(Sender: TObject);
private
FLoginProc: TSuccessProc;
FMessage: string;
procedure ShowNotification(Notification: string);
procedure HideNotification;
public
class procedure Display(LoginProc: TSuccessProc); overload;
class procedure Display(LoginProc: TSuccessProc; AMsg: string); overload;
end;
var
FViewLogin: TFViewLogin;
implementation
uses
Auth.Service;
{$R *.dfm}
procedure TFViewLogin.btnLoginClick(Sender: TObject);
procedure LoginSuccess;
begin
FLoginProc;
end;
procedure LoginError(AMsg: string);
begin
ShowNotification('Login Error: ' + AMsg);
end;
begin
AuthService.Login(
edtUsername.Text, edtPassword.Text,
@LoginSuccess,
@LoginError
);
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc);
begin
TFViewLogin.Display(LoginProc, '');
end;
class procedure TFViewLogin.Display(LoginProc: TSuccessProc; AMsg: string);
procedure FormCreate(AForm: TObject);
begin
TFViewLogin(AForm).FMessage := AMsg;
end;
begin
if Assigned(FViewLogin) then
FViewLogin.Free;
FViewLogin := TFViewLogin.CreateNew(@FormCreate);
FViewLogin.FLoginProc := LoginProc;
end;
procedure TFViewLogin.HideNotification;
begin
pnlMessage.ElementHandle.classList.add('d-none');
pnlMessage.Visible := False;
end;
procedure TFViewLogin.ShowNotification(Notification: string);
begin
if Notification <> '' then
begin
lblMessage.Caption := Notification;
pnlMessage.ElementHandle.classList.remove('d-none');
pnlMessage.Visible := True;
end;
end;
procedure TFViewLogin.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
procedure TFViewLogin.WebFormShow(Sender: TObject);
begin
console.log(DMConnection.clientVersion);
FViewLogin.lblClientVersion.Caption := 'v' + DMConnection.clientVersion;
if FMessage <> '' then
ShowNotification(FMessage)
else
HideNotification;
end;
end.
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 wllblLogout: TWebLinkLabel
Left = 501
Top = 33
Width = 36
Height = 14
ElementID = 'dropdown.menu.logout'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = wllblLogoutClick
Caption = ' Logout'
end
object lblVersion: TWebLabel
Left = 396
Top = 33
Width = 47
Height = 14
Caption = 'lblVersion'
ElementID = 'view.main.version'
ElementFont = efCSS
ElementPosition = epRelative
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object lblAppTitle: TWebLabel
Left = 57
Top = 33
Width = 48
Height = 14
Caption = 'emT3web'
ElementID = 'view.main.apptitle'
ElementFont = efCSS
ElementPosition = epRelative
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
Transparent = False
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel
Left = 77
Top = 112
Width = 1322
Height = 0
ElementID = 'main.webpanel'
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 WebMemo1: TWebMemo
Left = 77
Top = 479
Width = 471
Height = 83
ElementID = 'main.debugmemo'
ElementPosition = epRelative
Enabled = False
HeightPercent = 100.000000000000000000
Lines.Strings = (
'WebMemo1')
Role = 'null'
SelLength = 0
SelStart = 0
ShowFocus = False
Visible = False
WidthPercent = 100.000000000000000000
end
object WebMessageDlg1: TWebMessageDlg
Left = 47
Top = 232
Width = 24
Height = 24
Buttons = []
CustomButtons = <>
DialogText.Strings = (
'Warning'
'Error'
'Information'
'Confirm'
'Custom'
'OK'
'Cancel'
'Yes'
'No'
'Abort'
'Retry'
'Ignore'
'All'
'Yes to all'
'No to all'
'Help'
'Close')
Opacity = 0.200000000000000000
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 44
Top = 280
end
end
<div id="wrapper">
<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>
<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 vh-100">
<div class="row flex-grow-1">
<div id="main.webpanel" class="col-12 d-flex flex-column flex-grow-1"></div>
</div>
<div class="row">
<div class="col-12">
<div class="form-outline">
<textarea class="form-control" id="main.debugmemo" rows="4"></textarea>
</div>
</div>
</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">Back to Orders</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>
unit View.Main;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls,
WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset,
App.Types, ConnectionModule, XData.Web.Client, WEBLib.Menus, Utils;
type
TFViewMain = class(TWebForm)
wllblLogout: TWebLinkLabel;
WebPanel1: TWebPanel;
WebMessageDlg1: TWebMessageDlg;
WebMemo1: TWebMemo;
XDataWebClient: TXDataWebClient;
lblVersion: TWebLabel;
lblAppTitle: TWebLabel;
procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject);
procedure wllblLogoutClick(Sender: TObject);
private
{ Private declarations }
FUserInfo: string;
FSearchSettings: string;
FChildForm: TWebForm;
FLogoutProc: TLogoutProc;
FSearchProc: TSearchProc;
procedure ShowCrudForm( AFormClass: TWebFormClass );
procedure ConfirmLogout;
public
{ Public declarations }
class procedure Display(LogoutProc: TLogoutProc);
procedure ShowForm( AFormClass: TWebFormClass );
var
search: string;
change: boolean;
end;
var
FViewMain: TFViewMain;
implementation
uses
Auth.Service,
View.Login,
View.Tasks;
{$R *.dfm}
class procedure TFViewMain.Display(LogoutProc: TLogoutProc);
begin
if Assigned(FViewMain) then
FViewMain.Free;
FViewMain := TFViewMain.CreateNew;
FViewMain.FLogoutProc := LogoutProc;
end;
procedure TFViewMain.WebFormCreate(Sender: TObject);
var
userName: string;
test: boolean;
begin
FChildForm := nil;
ShowForm(TFTasks);
lblAppTitle.Caption := 'emT3web';
lblVersion.Caption := 'v' + DMConnection.clientVersion;
end;
procedure TFViewMain.mnuLogoutClick(Sender: TObject);
begin
ConfirmLogout;
end;
procedure TFViewMain.wllblLogoutClick(Sender: TObject);
begin
ConfirmLogout;
end;
procedure TFViewMain.ConfirmLogout;
begin
ShowConfirmationModal(
'Are you sure you want to log out?',
'Yes',
'No',
procedure(confirmed: Boolean)
begin
if confirmed and Assigned(FLogoutProc) then
FLogoutProc('');
end
);
end;
procedure TFViewMain.ShowCrudForm(AFormClass: TWebFormClass);
begin
ShowForm(AFormClass);
end;
procedure TFViewMain.ShowForm(AFormClass: TWebFormClass);
begin
if Assigned(FChildForm) then
FChildForm.Free;
Application.CreateForm(AFormClass, WebPanel1.ElementID, FChildForm);
end;
//procedure TFViewMain.ShowTasksForm(Info: string);
//begin
// if Assigned(FChildForm) then
// FChildForm.Free;
// FChildForm := TFViewUsers.CreateForm(WebPanel1.ElementID, Info);
//end;
end.
<div class="d-flex flex-column flex-grow-1 h-100">
<div class="row flex-grow-1 h-100">
<div class="col-12 flex-grow-1 h-100">
<div id="pnl_grid" class="h-100"></div>
</div>
</div>
</div>
unit View.Tasks;
interface
uses
System.SysUtils, System.Classes, System.Variants,
JS, Web, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, WebLib.JSON,
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, System.Rtti, WEBLib.ExtCtrls;
type
TFTasks = class(TWebForm)
xdwcTasks: TXDataWebClient;
pnlGrid: TWebPanel;
grdTasks: TTMSFNCDataGrid;
procedure WebFormCreate(Sender: TObject);
private
procedure SetupGrid;
[async] procedure LoadTasks(const AProjectId: string);
procedure AddComboColumn(ACol: Integer; const Items: array of string);
procedure AddDateColumn(ACol: Integer);
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;
SetupGrid;
LoadTasks('WPR0001');
end;
procedure TFTasks.SetupGrid;
begin
grdTasks.BeginUpdate;
try
grdTasks.Options.IO.StartColumn := 1;
grdTasks.Options.IO.StartRow := 1;
grdTasks.RowCount := 1;
grdTasks.ColumnCount := 13;
grdTasks.DefaultRowHeight := 100;
grdTasks.Options.Keyboard.TabKeyDirectEdit := True;
grdTasks.Options.Keyboard.ArrowKeyDirectEdit := True;
grdTasks.Options.Keyboard.EnterKeyDirectEdit := True;
grdTasks.Options.Editing.Enabled := True;
grdTasks.Columns[COL_TASK_ITEM_ID].Width := 110;
grdTasks.Columns[COL_TASK_ID].Width := 90;
grdTasks.Columns[COL_APP].Width := 220;
grdTasks.Columns[COL_VERSION].Width := 80;
grdTasks.Columns[COL_TASK_DATE].Width := 110;
grdTasks.Columns[COL_REPORTED_BY].Width := 110;
grdTasks.Columns[COL_ASSIGNED_TO].Width := 110;
grdTasks.Columns[COL_STATUS].Width := 140;
grdTasks.Columns[COL_STATUS_DATE].Width := 110;
grdTasks.Columns[COL_FIXED_VER].Width := 110;
grdTasks.Columns[COL_FORM_SECTION].Width := 160;
grdTasks.Columns[COL_ISSUE].Width := 650;
grdTasks.Columns[COL_NOTES].Width := 450;
grdTasks.Cells[COL_TASK_ITEM_ID, 0] := 'Task Item ID';
grdTasks.Cells[COL_TASK_ID, 0] := 'Task ID';
grdTasks.Cells[COL_APP, 0] := 'Application';
grdTasks.Cells[COL_VERSION, 0] := 'Version';
grdTasks.Cells[COL_TASK_DATE, 0] := 'Date';
grdTasks.Cells[COL_REPORTED_BY, 0] := 'Reported By';
grdTasks.Cells[COL_ASSIGNED_TO, 0] := 'Assigned To';
grdTasks.Cells[COL_STATUS, 0] := 'Status';
grdTasks.Cells[COL_STATUS_DATE, 0] := 'Status Date';
grdTasks.Cells[COL_FIXED_VER, 0] := 'Fixed Version';
grdTasks.Cells[COL_FORM_SECTION, 0] := 'Form (Section)';
grdTasks.Cells[COL_ISSUE, 0] := 'Issue';
grdTasks.Cells[COL_NOTES, 0] := 'Notes';
grdTasks.Columns[COL_TASK_ITEM_ID].ReadOnly := True;
grdTasks.Columns[COL_TASK_ID].ReadOnly := True;
AddComboColumn(COL_APP, ['webPoliceReports - Client', 'webPoliceReports - Server']);
AddComboColumn(COL_VERSION, ['0.9.4', '0.9.5']);
AddComboColumn(COL_REPORTED_BY, ['Mark', 'Mac']);
AddComboColumn(COL_ASSIGNED_TO, ['Mac', 'Mark']);
AddComboColumn(COL_STATUS, ['Open', 'In Progress', 'Blocked', 'Fixed', 'Fixed - Verified']);
AddComboColumn(COL_FIXED_VER, ['0.9.4', '0.9.5']);
AddDateColumn(COL_TASK_DATE);
AddDateColumn(COL_STATUS_DATE);
finally
grdTasks.EndUpdate;
end;
end;
procedure TFTasks.AddComboColumn(ACol: Integer; const Items: array of string);
var
I: Integer;
begin
grdTasks.Columns[ACol].AddSetting(gcsEditor);
grdTasks.Columns[ACol].AddSetting(gcsEditorItems);
grdTasks.Columns[ACol].Editor := getComboBox;
grdTasks.Columns[ACol].EditorItems.Clear;
for I := Low(Items) to High(Items) do
grdTasks.Columns[ACol].EditorItems.Add(Items[I]);
end;
procedure TFTasks.AddDateColumn(ACol: Integer);
begin
grdTasks.Columns[ACol].AddSetting(gcsEditor);
grdTasks.Columns[ACol].Editor := getDatePicker;
grdTasks.Columns[ACol].AddSetting(gcsFormatting);
grdTasks.Columns[ACol].Formatting.&Type := gdftDateTime;
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;
r: Integer;
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;
grdTasks.Height := Trunc((grdTasks.RowCount * grdTasks.DefaultRowHeight) + 2);
except
on E: EXDataClientRequestException do
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
finally
Utils.HideSpinner('spinner');
end;
end;
end.
{
"AuthUrl" : "http://localhost:2004/kgOrders/auth/",
"ApiUrl" : "http://localhost:2004/kgOrders/api/"
}
.login-card {
display: inline-block;
width: 300px; /* Adjust width as needed */
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
input[type="text"] {
min-width: 50px;
max-width: 100%;
width: auto;
padding-left: 5px;
}
is-invalid .form-check-input {
border: 1px solid #dc3545 !important;
}
.is-invalid .form-check-label {
color: #dc3545 !important;
}
.input-search input {
width: 100px; /* Adjust the width of the input */
height: 35px; /* Set the height to match label height */
padding: 5px; /* Padding for input text */
font-size: 14px; /* Font size for input text */
border: 1px solid #ced4da; /* Border style */
border-radius: 5px; /* Rounded corners for the input */
box-sizing: border-box; /* Ensures padding and border are included in the element's total width and height */
}
.card-header {
width: 100%;
text-align: left; /* Align text to the left */
background-color: #f8f9fa; /* Match the card background */
padding: 0.75rem 1.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
margin: 0; /* Remove any margin */
box-sizing: border-box; /* Ensure padding is included in the element's total width and height */
}
/* Ensure that the title does not affect the navbar layout */
#view.main.apptitle {
display: flex;
align-items: center;
justify-content: flex-start; /* Align title to the left */
width: auto; /* Ensure it doesn't stretch the container */
margin-right: 20px; /* Optional: add space between title and navbar items */
}
/* Fixed width for title area to prevent shifting */
#title {
white-space: nowrap; /* Prevent the title text from wrapping */
width: 200px; /* Fixed width for the title */
text-overflow: ellipsis; /* Truncate text with ellipsis if it overflows */
overflow: hidden;
font-weight: bold; /* Optional: make the title text bold */
}
/* Navbar items - keep them aligned and spaced out */
.navbar-nav .nav-item {
padding: 0 15px; /* Adjust spacing between navbar items */
z-index: 9999;
}
/* Flexbox for the entire navbar */
.navbar-nav {
display: flex;
justify-content: flex-end; /* Align navbar items to the right */
width: 100%;
}
/* Additional mobile responsiveness (optional) */
@media (max-width: 1200px) {
.navbar-nav {
flex-direction: column; /* Stack items vertically on smaller screens */
align-items: flex-start; /* Align items to the left */
}
.navbar-nav-spaced .nav-item {
padding: 10px 0; /* Adjust vertical spacing between items */
}
}
/* Make sure active navbar item color gets applied */
.navbar-nav .nav-item a.active {
color: #fff !important; /* Set text color to white for active item */
background-color: #004F84 !important; /* Darker blue for active background */
font-weight: bold;
}
/* Default navbar item color */
.navbar-nav .nav-item a {
color: #000 !important; /* Default color for links */
transition: color 0.3s ease;
}
/* Navbar item hover state */
.navbar-nav .nav-item a:hover {
color: #fff !important; /* Set hover text color to white */
background-color: #286090 !important; /* Light blue on hover */
}
.nav-item {
padding: 0 20px; /* Adjust this value for desired spacing between labels */
}
.mr-2 {
margin-right: 0.5rem;
}
.custom-h4 {
margin-bottom: 5px;
}
.custom-hr {
margin-top: 5px;
margin-bottom: 10px;
}
.custom-select-large {
font-size: 1.25rem;
padding: 0.5rem;
height: 2.5rem;
}
.player-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.player-container audio {
width: 100%;
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
}
.card-title {
margin: 0;
font-size: 1.25rem; /* Adjust font size as needed */
}
.card-body {
padding: 2rem;
}
.table tbody tr:hover {
background-color: #d1e7fd; /* Light blue color for hover effect */
cursor: pointer;
margin-top: 5px;
}
.form-input{
display: table;
}
.form-cells{
display: table-cell
}
.table tbody tr {
transition: background-color 0.3s ease;
}
.table thead th{
border: 2px;
background-color: #e6e6e6;
}
.table thead {
border-color: #e6e6e6;
}
.min-w-80 {
min-width: 80px;
}
.table {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
}
.navbar-nav {
margin-left: auto; /* Align the dropdown to the right */
}
.container {
margin-top: 50px; /* Adjust the top margin as needed */
}
@media (max-width: 1200px) {
.table-responsive {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table thead {
display: none;
}
.table tbody, .table tr, .table td {
display: block;
width: 100%;
}
.table tr {
margin-bottom: 1rem;
}
.table td {
text-align: right;
padding-left: 50%; /* Adjust padding to accommodate the data-label */
position: relative;
}
.table td::before {
content: attr(data-label);
position: absolute;
left: 0;
width: 50%;
padding-left: 15px; /* Adjust as necessary */
font-weight: bold;
text-align: left;
}
.table td .transcript {
margin-top: 20px; /* Set top margin to 20px */
text-align: left; /* Ensure text alignment is left */
margin-left: 8px;
white-space: normal; /* Prevent text from being cut off */
}
}
.btn-primary {
background-color: #286090 !important;
border-color: #286090 !important;
color: #fff !important;
}
.btn-primary:hover {
background-color: #204d74 !important;
border-color: #204d74 !important;
}
.login-navbar {
max-width: 1200px; /* Set the max-width to match a medium screen */
margin: auto;
border-bottom-left-radius: 10px; /* Round the bottom left corner */
border-bottom-right-radius: 10px; /* Round the bottom right corner */
border: 1px solid #d3d3d3;
}
.navbar-toggler {
display: none;
}
.dropdown-menu a {
display: flex; /* Use flexbox for alignment */
align-items: center; /* Vertically center the content */
width: 100%; /* Ensure they take up the full width */
padding: 0.5rem 1rem; /* Add padding to make them clickable */
color: #000; /* Adjust the text color if necessary */
text-decoration: none; /* Remove underlines */
}
.dropdown-menu a:hover {
background-color: #204d74;
color: #fff;
}
.dropdown-menu a span {
flex-grow: 1; /* Make the span take up the remaining space */
}
/* Style for the selected number */
.selected-number .page-link {
background-color: #204d74;
color: #fff !important;
}
/* Style for the unselected numbers and text (previous/next) */
.pagination .page-item a,
.pagination .page-item span {
color: #204d74;
}
.pagination .page-item.active .page-link,
.pagination .page-item.active .page-link:hover,
.pagination .page-item.active .page-link:focus {
background-color: #204d74;
border-color: #204d74;
color: #fff !important;
}
.card {
border: none !important;
box-shadow: none !important; /* If shadow is causing the issue */
}
.color-column {
width: 200px; /* Fixed width for the column */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; /* Adds ellipsis */
}
.grid-container {
position: relative; /* Ensure the container is the reference for child positioning */
height: 400px; /* Set the height for the grid */
width: 100%; /* Full width of the parent container */
overflow: hidden; /* Prevent unintended overflow */
}
#TFAddOrder_TMSFNCGrid2 {
top: 0px !important; /* Reset the top offset */
left: 0px !important; /* Reset the left offset */
position: absolute !important; /* Use relative positioning to align with the grid container */
}
#TFAddOrder_TMSFNCGrid2_Canvas {
position: relative !important; /* Ensure absolute positioning within the parent */
width: 100% !important; /* Make the canvas fill the container width */
height: 100% !important; /* Make the canvas fill the container height */
left: 0px !important; /* Align canvas to the left */
}
#TFAddOrder_ScrollBar5 {
top: 0px !important; /* Align scrollbar to the top */
right: 0px !important; /* Align scrollbar to the right edge */
position: absolute !important; /* Position within the grid container */
height: 400px !important; /* Match the height of the grid container */
box-sizing: border-box !important; /* Prevent borders from affecting dimensions */
}
#tblPhoneGrid {
--bs-table-bg: #fff;
--bs-table-border-color: #dee2e6; /* or whatever border tone you like */
}
/* and make sure the table collapses borders so there are no gaps */
#tblPhoneGrid {
border-collapse: collapse !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%);
}
.tbl-min-160 {
min-width: 160px;
}
.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 emT3webClient;
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},
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
App.Config in 'App.Config.pas',
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
Utils in 'Utils.pas',
View.Tasks in 'View.Tasks.pas' {FTasks: TWebForm} {*.html};
{$R *.res}
procedure DisplayLoginView(AMessage: string = ''); forward;
procedure DisplayMainView;
procedure ConnectProc;
begin
if Assigned(FViewLogin) then
FViewLogin.Free;
TFViewMain.Display(@DisplayLoginView);
end;
begin
if not DMConnection.ApiConnection.Connected then
DMConnection.ApiConnection.Open(@ConnectProc)
else
ConnectProc;
end;
procedure DisplayLoginView(AMessage: string);
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewMain) then
FViewMain.Free;
TFViewLogin.Display(@DisplayMainView, AMessage);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
DisplayLoginView(AMessage);
end;
procedure StartApplication;
begin
DMConnection.InitApp(
procedure
begin
DMConnection.SetClientConfig(
procedure(Success: Boolean; ErrorMessage: string)
begin
if Success then
begin
if (not AuthService.Authenticated) or AuthService.TokenExpired then
DisplayLoginView
else
DisplayMainView;
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 () {
location.reload(true); // Hard refresh
});
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"/>
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>EM Systems webKGOrders 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" type="text/css"/>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<script crossorigin="anonymous" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" src="https://code.jquery.com/jquery-3.7.1.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" type="text/javascript"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="$(ProjectName).js" type="text/javascript"></script>
</head>
<body>
</body>
<script type="text/javascript">rtl.run();</script>
</html>
......@@ -10,21 +10,23 @@ object ApiServerModule: TApiServerModule
ModelName = 'Api'
EntitySetPermissions = <>
SwaggerOptions.Enabled = True
SwaggerOptions.AuthMode = Jwt
SwaggerUIOptions.Enabled = True
SwaggerUIOptions.ShowFilter = True
SwaggerUIOptions.TryItOutEnabled = True
Left = 89
Top = 112
object XDataServerLogging: TSparkleGenericMiddleware
OnMiddlewareCreate = XDataServerLoggingMiddlewareCreate
end
object XDataServerCORS: TSparkleCorsMiddleware
Left = 80
Top = 142
object XDataServerJWT: TSparkleJwtMiddleware
OnGetSecret = XDataServerJWTGetSecret
end
object XDataServerCompress: TSparkleCompressMiddleware
end
object XDataServerJWT: TSparkleJwtMiddleware
OnGetSecret = XDataServerJWTGetSecret
object XDataServerCORS: TSparkleCorsMiddleware
end
object XDataServerLogging: TSparkleLoggingMiddleware
FormatString = ':method :url :statuscode - :responsetime ms'
ExceptionFormatString = '(%1:s: %4:s) %0:s - %2:s'
ErrorResponseOptions.ErrorCode = 'ServerError'
ErrorResponseOptions.ErrorMessageFormat = 'Internal server error: %4:s'
end
end
end
......@@ -15,16 +15,16 @@ uses
Sparkle.HttpServer.Module, Sparkle.HttpServer.Context,
Sparkle.Comp.CompressMiddleware, Sparkle.Comp.CorsMiddleware,
Sparkle.Comp.GenericMiddleware, Aurelius.Drivers.UniDac, UniProvider,
Data.DB, DBAccess, Uni;
Data.DB, DBAccess, Uni, Sparkle.Comp.LoggingMiddleware;
type
TApiServerModule = class(TDataModule)
SparkleHttpSysDispatcher: TSparkleHttpSysDispatcher;
XDataServer: TXDataServer;
XDataServerLogging: TSparkleGenericMiddleware;
XDataServerCORS: TSparkleCorsMiddleware;
XDataServerCompress: TSparkleCompressMiddleware;
XDataServerJWT: TSparkleJwtMiddleware;
XDataServerCompress: TSparkleCompressMiddleware;
XDataServerCORS: TSparkleCorsMiddleware;
XDataServerLogging: TSparkleLoggingMiddleware;
procedure XDataServerLoggingMiddlewareCreate(Sender: TObject;
var Middleware: IHttpServerMiddleware);
procedure XDataServerJWTGetSecret(Sender: TObject; var Secret: string);
......@@ -51,7 +51,7 @@ uses
XData.Sys.Exceptions,
Common.Logging,
Common.Middleware.Logging,
Common.Config, Vcl.Forms, IniFiles;
Common.Config, Vcl.Forms, IniFiles, Api.Service;
{%CLASSGROUP 'Vcl.Controls.TControl'}
......@@ -80,6 +80,7 @@ begin
XDataServer.BaseUrl := Url;
XDataServer.ModelName := AModelName;
Logger.Log(1, 'API XDataServer.ModelName=' + XDataServer.ModelName);
SparkleHttpSysDispatcher.Start;
Logger.Log(1, Format('Api server module listening at "%s"', [XDataServer.BaseUrl]));
......
......@@ -6,7 +6,10 @@ uses
XData.Service.Common,
System.Generics.Collections,
System.Classes,
System.SysUtils;
System.SysUtils,
Aurelius.Mapping.Attributes,
System.JSON,
Common.Logging;
const
API_MODEL = 'Api';
......@@ -26,7 +29,7 @@ type
assignedTo: string;
status: string;
statusDate: TDateTime;
statusDate: Variant;
fixedVersion: string;
formSection: string;
......@@ -52,15 +55,11 @@ type
destructor Destroy; override;
end;
[ServiceContract]
type
[ServiceContract, Model(API_MODEL)]
IApiService = interface(IInvokable)
['{A8CBF627-BB64-4A53-821C-9A84C2752248}']
end;
[ServiceContract]
ITasksService = interface(IInvokable)
['{D5E1B7A2-6A9D-4D9A-9F7F-9A3E8A9E3B11}']
['{0EFB33D7-8C4C-4F3C-9BC3-8B4D444B5F69}']
[HttpGet] function Ping: string;
[HttpGet] function GetProjectTasks(projectId: string): TTasksList;
end;
......@@ -91,6 +90,8 @@ begin
end;
initialization
RegisterServiceType(TypeInfo(ITasksService));
RegisterServiceType(TypeInfo(IApiService));
end.
......@@ -4,23 +4,24 @@ interface
uses
XData.Server.Module, System.Generics.Collections,
XData.Service.Common,
Api.Service, Api.Database;
XData.Service.Common, System.Variants,
Api.Service, Api.Database, Common.Logging;
type
[ServiceImplementation]
TApiService = class(TInterfacedObject, IApiService)
private
ApiDB: TApiDatabase;
strict private
ApiDB: TApiDatabase;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function GetProjectTasks(projectId: string): TTasksList;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function GetProjectTasks(projectId: string): TTasksList;
function Ping: string;
end;
implementation
procedure TApiService.AfterConstruction;
begin
inherited;
......@@ -35,6 +36,12 @@ begin
end;
function TApiService.Ping: string;
begin
Result := 'pong';
end;
function TApiService.GetProjectTasks(projectId: string): TTasksList;
var
taskMap: TDictionary<string, TTask>;
......@@ -88,7 +95,7 @@ begin
item.status := ApiDB.uqProjectTasksSTATUS.AsString;
if ApiDB.uqProjectTasksSTATUS_DATE.IsNull then
item.statusDate := 0
item.statusDate := Null
else
item.statusDate := ApiDB.uqProjectTasksSTATUS_DATE.AsDateTime;
......@@ -109,9 +116,8 @@ begin
end;
initialization
RegisterServiceType(TypeInfo(IApiService));
RegisterServiceType(TApiService);
end.
......@@ -18,9 +18,13 @@ object AuthDatabase: TAuthDatabase
end
object ucKG: TUniConnection
ProviderName = 'MySQL'
Database = 'emt3_web_db'
Username = 'root'
Server = '192.168.102.129'
LoginPrompt = False
Left = 67
Top = 131
EncryptedPassword = '9AFF92FF8CFF86FF8CFFCFFFCEFF'
end
object MySQLUniProvider1: TMySQLUniProvider
Left = 230
......
......@@ -14,7 +14,7 @@ object AuthServerModule: TAuthServerModule
SwaggerUIOptions.ShowFilter = True
SwaggerUIOptions.TryItOutEnabled = True
Left = 91
Top = 92
Top = 94
object XDataServerLogging: TSparkleGenericMiddleware
OnMiddlewareCreate = XDataServerLoggingMiddlewareCreate
end
......
// Implementation of Auth Serice that will eventually retrieve login information
// from the auth database.
unit Auth.ServiceImpl;
interface
......
......@@ -162,6 +162,7 @@ begin
AppServerModule := TAppServerModule.Create(Self);
AppServerModule.StartAppServer( serverConfig.url );
Logger.Log(1, 'Exe=' + Application.ExeName);
UpdateGUI;
end;
......
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