Commit be5e30ed by Mac Stephens

Added maintenance mode in dpr for client to ignore xdata connection on startup -…

Added maintenance mode in dpr for client to ignore xdata connection on startup - can be removed when site is developed
parent b581c1da
*.exe
*.bpl
*.bpi
*.dcp
*.so
*.apk
*.drc
*.map
*.dres
*.rsm
*.tds
*.dcu
*.lib
*.a
*.o
*.ocx
*.cfg
*.hpp
*Resource.rc
*.local
*.identcache
*.projdata
*.tvsconfig
*.dsk
__history/*
__recovery/*
envoyInternationalWeb/TMSWeb*
envoyInternationalWeb/Win32*
*.~*
*.stat
*.dxsettings
*.skincfg
envoyInternationalServer/source/__recovery/
*.zip
*.res
*.log
[Settings]
memoLogLevel=3
fileLogLevel=4
LogFileNum=53
webClientVersion=1.0.3
[SMTP]
Host=mail.em-sys.net
Port=587
Username=em-sys\emsys
Password=Ridge!4043
EmailWebsiteUses=emsys@em-sys.net
RecipientEmail=mac@em-sys.net
[Options]
LogFileNum=22
{
"Url": "http://localhost:2002/website",
"JwtTokenSecret": "super_secret0123super_secret4567",
"AdminPassword": "whatisthisusedfor",
"WebAppFolder": "static",
"ConsoleLogLevel": 3,
"FileLogLevel": 4
}
program emSystemsWebServer;
uses
System.SyncObjs,
System.SysUtils,
Vcl.StdCtrls,
IniFiles,
Vcl.Forms,
Api.Database in 'source\Api.Database.pas' {ApiDatabase: TDataModule},
Api.Server.Module in 'source\Api.Server.Module.pas' {ApiServerModule: TDataModule},
Common.Logging in 'source\Common.Logging.pas',
Api.Service in 'source\Api.Service.pas',
Api.ServiceImpl in 'source\Api.ServiceImpl.pas',
Main in 'source\Main.pas' {FMain},
App.Server.Module in 'source\App.Server.Module.pas' {AppServerModule: TDataModule},
Common.Config in 'source\Common.Config.pas',
Common.Middleware.Logging in 'source\Common.Middleware.Logging.pas';
type
TMemoLogAppender = class( TInterfacedObject, ILogAppender )
private
FLogLevel: Integer;
FLogMemo: TMemo;
FCriticalSection: TCriticalSection;
public
constructor Create(ALogLevel: Integer; ALogMemo: TMemo);
destructor Destroy; override;
procedure Send(logLevel: Integer; Log: ILog);
end;
TFileLogAppender = class( TInterfacedObject, ILogAppender )
private
FLogLevel: Integer;
FFilename: string;
FLogDirectory: string;
FCriticalSection: TCriticalSection;
public
constructor Create(ALogLevel: Integer; AFilename: string);
destructor Destroy; override;
procedure Send(logLevel: Integer; Log: ILog);
end;
TTaggedFileLogAppender = class( TInterfacedObject, ILogAppender )
private
FLogLevel: Integer;
FFilename: string;
FLogDirectory: string;
FTagPrefix: string;
FCriticalSection: TCriticalSection;
public
constructor Create(ALogLevel: Integer; AFilename: string; ATagPrefix: string);
destructor Destroy; override;
procedure Send(logLevel: Integer; Log: ILog);
end;
{ TMemoLogAppender }
constructor TMemoLogAppender.Create(ALogLevel: Integer; ALogMemo: TMemo);
begin
FLogLevel := ALogLevel;
FLogMemo := ALogMemo;
FCriticalSection := TCriticalSection.Create;
end;
destructor TMemoLogAppender.Destroy;
begin
FCriticalSection.Free;
inherited;
end;
procedure TMemoLogAppender.Send(logLevel: Integer; Log: ILog);
var
FormattedMessage: string;
LogTime: TDateTime;
LogMsg: string;
begin
FCriticalSection.Acquire;
try
LogTime := Now;
FormattedMessage := FormatDateTime('[yyyy-mm-dd HH:nn:ss.zzz]', LogTime);
LogMsg := Log.GetMessage;
if LogMsg.IsEmpty then
FormattedMessage := ''
else
FormattedMessage := FormattedMessage + '[' + IntToStr(logLevel) +'] ' + LogMsg;
if logLevel <= FLogLevel then
FLogMemo.Lines.Add(FormattedMessage);
finally
FCriticalSection.Release;
end;
end;
{ TFileLogAppender }
constructor TFileLogAppender.Create(ALogLevel: integer; AFilename: string);
var
iniFile: TIniFile;
fileNum: integer;
begin
FLogLevel := ALogLevel;
FCriticalSection := TCriticalSection.Create;
FLogDirectory := ExtractFilePath(Application.ExeName) + 'logs\';
if not DirectoryExists(FLogDirectory) then
CreateDir(FLogDirectory);
iniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
fileNum := iniFile.ReadInteger('Settings', 'LogFileNum', 0);
FFilename := AFilename + Format('%.4d', [fileNum]);
iniFile.WriteInteger('Settings', 'LogFileNum', fileNum + 1);
finally
iniFile.Free;
end;
end;
destructor TFileLogAppender.Destroy;
begin
FCriticalSection.Free;
inherited;
end;
procedure TFileLogAppender.Send(logLevel: integer; Log: ILog);
var
FormattedMessage: string;
LogFile: string;
LogTime: TDateTime;
LogMsg: string;
FLogFile: TextFile;
begin
FCriticalSection.Acquire;
try
LogTime := Now;
LogFile := FLogDirectory + FFilename + '.log';
FormattedMessage := FormatDateTime('[yyyy-mm-dd HH:nn:ss.zzz]', LogTime);
LogMsg := Log.GetMessage;
if LogMsg.IsEmpty then
FormattedMessage := ''
else
FormattedMessage := FormattedMessage + '[' + IntToStr(logLevel) +'] ' + LogMsg;
try
AssignFile(FLogFile, LogFile);
if logLevel <= FLogLevel then
begin
if FileExists(LogFile) then
Append(FLogFile)
else
Rewrite(FLogFile);
WriteLn(FLogFile, FormattedMessage);
end;
finally
CloseFile(FLogFile);
end;
finally
FCriticalSection.Release;
end;
end;
{ TTaggedFileLogAppender }
constructor TTaggedFileLogAppender.Create(ALogLevel: Integer; AFilename: string; ATagPrefix: string);
begin
FLogLevel := ALogLevel;
FTagPrefix := ATagPrefix;
FCriticalSection := TCriticalSection.Create;
FLogDirectory := ExtractFilePath(Application.ExeName) + 'logs\';
if not DirectoryExists(FLogDirectory) then
CreateDir(FLogDirectory);
FFilename := AFilename;
end;
destructor TTaggedFileLogAppender.Destroy;
begin
FCriticalSection.Free;
inherited;
end;
procedure TTaggedFileLogAppender.Send(logLevel: Integer; Log: ILog);
var
FormattedMessage: string;
LogFile: string;
LogTime: TDateTime;
LogMsg: string;
FLogFile: TextFile;
begin
if logLevel > FLogLevel then
Exit;
LogMsg := Log.GetMessage;
if LogMsg.IsEmpty then
Exit;
if (FTagPrefix <> '') and (Copy(LogMsg, 1, Length(FTagPrefix)) <> FTagPrefix) then
Exit;
FCriticalSection.Acquire;
try
LogTime := Now;
LogFile := FLogDirectory + FFilename + '.log';
FormattedMessage := FormatDateTime('[yyyy-mm-dd HH:nn:ss.zzz]', LogTime);
FormattedMessage := FormattedMessage + '[' + IntToStr(logLevel) +'] ' + LogMsg;
try
AssignFile(FLogFile, LogFile);
if FileExists(LogFile) then
Append(FLogFile)
else
Rewrite(FLogFile);
WriteLn(FLogFile, FormattedMessage);
finally
CloseFile(FLogFile);
end;
finally
FCriticalSection.Release;
end;
end;
{$R *.res}
var
iniFile: TIniFile;
memoLogLevel: Integer;
fileLogLevel: Integer;
begin
ReportMemoryLeaksOnShutdown := False;
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TFMain, FMain);
IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
memoLogLevel := IniFile.ReadInteger('Settings', 'memoLogLevel', 3);
fileLogLevel := IniFile.ReadInteger('Settings', 'fileLogLevel', 4);
finally
IniFile.Free;
end;
Logger.AddAppender(TMemoLogAppender.Create(memoLogLevel, FMain.memoInfo));
Logger.AddAppender(TFileLogAppender.Create(fileLogLevel, 'emSystemsWebServer'));
Logger.AddAppender(TTaggedFileLogAppender.Create(fileLogLevel, 'contact', '[CONTACT]'));
Logger.AddAppender(TTaggedFileLogAppender.Create(fileLogLevel, 'demo', '[DEMO]'));
Application.Run;
end.
object ApiDatabase: TApiDatabase
Height = 218
Width = 444
object ucBooking: TUniConnection
ProviderName = 'PostgreSQL'
LoginPrompt = False
Left = 41
Top = 63
end
object PostgreSQLUniProvider1: TPostgreSQLUniProvider
Left = 156
Top = 64
end
object UniQuery1: TUniQuery
Connection = ucBooking
Left = 287
Top = 62
end
end
unit Api.Database;
interface
uses
System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider,
PostgreSQLUniProvider;
type
TApiDatabase = class(TDataModule)
ucBooking: TUniConnection;
PostgreSQLUniProvider1: TPostgreSQLUniProvider;
UniQuery1: TUniQuery;
private
{ Private declarations }
public
{ Public declarations }
class procedure ExecSQL(const SQL: string);
end;
var
ApiDatabase: TApiDatabase;
implementation
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
class procedure TApiDatabase.ExecSQL(const SQL: string);
var
DB: TApiDatabase;
begin
DB := TApiDatabase.Create(nil);
try
DB.UniQuery1.SQL.Text := SQL;
DB.UniQuery1.ExecSQL;
finally
DB.Free;
end;
end;
end.
object ApiServerModule: TApiServerModule
Height = 179
Width = 190
object SparkleHttpSysDispatcher: TSparkleHttpSysDispatcher
Left = 74
Top = 16
end
object XDataServer: TXDataServer
Dispatcher = SparkleHttpSysDispatcher
EntitySetPermissions = <>
SwaggerOptions.Enabled = True
SwaggerOptions.AuthMode = Jwt
SwaggerUIOptions.Enabled = True
SwaggerUIOptions.ShowFilter = True
SwaggerUIOptions.TryItOutEnabled = True
Left = 70
Top = 88
object XDataServerCompress: TSparkleCompressMiddleware
end
object XDataServerCORS: TSparkleCorsMiddleware
end
object XDataServerGeneric: TSparkleGenericMiddleware
OnMiddlewareCreate = XDataServerGenericMiddlewareCreate
end
end
end
unit Api.Server.Module;
interface
uses
System.SysUtils, System.Classes, System.IniFiles,
Sparkle.HttpServer.Module, Sparkle.HttpServer.Context,
Sparkle.Comp.Server, Sparkle.Comp.HttpSysDispatcher,
XData.Comp.Server, XData.Comp.ConnectionPool, XData.OpenApi.Service,
Sparkle.Comp.GenericMiddleware, Sparkle.Comp.JwtMiddleware,
Sparkle.Comp.BasicAuthMiddleware, Sparkle.Comp.CorsMiddleware, Common.Middleware.Logging,
Sparkle.Comp.CompressMiddleware, VCL.Forms, XData.Server.Module, Common.Logging;
type
TApiServerModule = class(TDataModule)
SparkleHttpSysDispatcher: TSparkleHttpSysDispatcher;
XDataServer: TXDataServer;
XDataServerCompress: TSparkleCompressMiddleware;
XDataServerCORS: TSparkleCorsMiddleware;
XDataServerGeneric: TSparkleGenericMiddleware;
procedure XDataServerGenericMiddlewareCreate(Sender: TObject; var Middleware:
IHttpServerMiddleware);
public
procedure StartApiServer(ABaseUrl: string);
end;
const
SERVER_PATH_SEGMENT = 'api';
var
ApiServerModule: TApiServerModule;
implementation
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
procedure TApiServerModule.StartApiServer(ABaseUrl: string);
var
Url: string;
begin
RegisterOpenApiService;
Url := ABaseUrl;
if not Url.EndsWith('/') then
Url := Url + '/';
Url := Url + SERVER_PATH_SEGMENT;
XDataServer.BaseUrl := Url;
SparkleHttpSysDispatcher.Start;
Logger.Log(1, Format('Api server module listening at "%s"', [XDataServer.BaseUrl]));
end;
procedure TApiServerModule.XDataServerGenericMiddlewareCreate(Sender: TObject;
var Middleware: IHttpServerMiddleware);
begin
Middleware := TLoggingMiddleware.Create(Logger);
end;
end.
unit Api.Service;
interface
uses
XData.Service.Common, System.JSON;
const
API_MODEL = 'api';
type
[ServiceContract]
IApiService = interface(IInvokable)
['{46B3B095-5873-4452-B338-AEE009604DED}']
[HttpGet] function SendEmail(Name, Email, Subject, Body: string): string;
[HttpGet] function VerifyVersion(ClientVersion: string): TJSONObject;
end;
implementation
initialization
RegisterServiceType(TypeInfo(IApiService));
end.
unit Api.ServiceImpl;
interface
uses
XData.Server.Module,
XData.Service.Common,
IdSMTP, IdMessage, IdSSLOpenSSL, IdText, IdExplicitTLSClientServerBase, System.NetEncoding,
IdIOHandlerSocket, IdException, IdSSL, IdSMTPBase, IdGlobal, IdStack, IdWinsock2,
IdStackConsts, IdIOHandler, IdIOHandlerStack, IdBaseComponent, System.JSON,
IdComponent, IdTCPConnection, IdTCPClient, IdMessageClient, VCL.Forms, Api.Service;
type
[ServiceImplementation]
TApiService = class(TInterfacedObject, IApiService)
public
function SendEmail(Name, Email, Subject, Body: string): string;
function VerifyVersion(ClientVersion: string): TJSONObject;
end;
implementation
uses
System.SysUtils,
System.IniFiles,
Common.Logging,
Main;
function TApiService.SendEmail(Name, Email, Subject, Body: string): string;
var
smtp: TIdSMTP;
message: TIdMessage;
confirmMsg: TIdMessage;
ssl: TIdSSLIOHandlerSocketOpenSSL;
iniFile: TIniFile;
iniPath: string;
fromEmail: string;
recipientEmail: string;
safeSenderEmail: string;
safeSubject: string;
safeName: string;
submittedOn: string;
logPrefix: string;
htmlBody: string;
confirmHtml: string;
bodyHtml: string;
textPart: TIdText;
htmlPart: TIdText;
confirmTextPart: TIdText;
confirmHtmlPart: TIdText;
confirmSubject: string;
bodyLines: TArray<string>;
i: Integer;
begin
iniPath := ChangeFileExt(Application.ExeName, '.ini');
iniFile := TIniFile.Create(iniPath);
try
fromEmail := iniFile.ReadString('SMTP', 'EmailWebsiteUses', '');
recipientEmail := iniFile.ReadString('SMTP', 'RecipientEmail', '');
safeSenderEmail := Email.Replace(#13, '').Replace(#10, '').Trim;
safeSubject := Subject.Replace(#13, '').Replace(#10, '').Trim;
safeName := Name.Trim;
if SameText(safeSubject, 'Demo Request') then
logPrefix := '[DEMO]'
else
logPrefix := '[CONTACT]';
Logger.Log(2, logPrefix + ' ----------------------------------------');
Logger.Log(2, Format('%s SubmittedOn=%s', [logPrefix, FormatDateTime('yyyy-mm-dd HH:nn:ss', Now)]));
Logger.Log(2, Format('%s Name=%s', [logPrefix, safeName]));
Logger.Log(2, Format('%s Email=%s', [logPrefix, safeSenderEmail]));
Logger.Log(2, Format('%s Subject=%s', [logPrefix, safeSubject]));
Logger.Log(2, logPrefix + ' Body:');
bodyLines := Body.Replace(#13#10, #10).Replace(#13, #10).Split([#10]);
for i := 0 to High(bodyLines) do
Logger.Log(2, logPrefix + ' ' + bodyLines[i]);
smtp := TIdSMTP.Create(nil);
message := TIdMessage.Create(nil);
confirmMsg := TIdMessage.Create(nil);
ssl := TIdSSLIOHandlerSocketOpenSSL.Create(smtp);
try
smtp.IOHandler := ssl;
smtp.UseTLS := utUseExplicitTLS;
smtp.AuthType := satDefault;
smtp.Host := iniFile.ReadString('SMTP', 'Host', '');
smtp.Port := iniFile.ReadInteger('SMTP', 'Port', 0);
smtp.Username := iniFile.ReadString('SMTP', 'Username', '');
smtp.Password := iniFile.ReadString('SMTP', 'Password', '');
ssl.SSLOptions.Method := sslvTLSv1_2;
ssl.SSLOptions.SSLVersions := [sslvTLSv1_2];
submittedOn := FormatDateTime('yyyy-mm-dd hh:nn AM/PM', Now);
bodyHtml := TNetEncoding.HTML.Encode(Body);
bodyHtml := bodyHtml.Replace(#13#10, '<br>').Replace(#10, '<br>').Replace(#13, '<br>');
htmlBody := Format(
'<html>' +
'<body style="font-family: Arial, sans-serif; font-size: 14px; color:#222;">' +
'<h2 style="color: #2E86C1; margin: 0 0 12px 0;">New %s message from EM Systems Website</h2>' +
'<p style="margin: 0 0 6px 0;"><strong>Submitted On:</strong> %s</p>' +
'<p style="margin: 0 0 6px 0;"><strong>From:</strong> %s &lt;%s&gt;</p>' +
'<p style="margin: 0 0 14px 0;"><strong>Subject:</strong> %s</p>' +
'<hr style="border:0; border-top:1px solid #ddd; margin: 14px 0;">' +
'<p style="margin: 0 0 8px 0;"><strong>Message:</strong></p>' +
'<div style="line-height: 1.4;">%s</div>' +
'</body>' +
'</html>',
[
TNetEncoding.HTML.Encode(safeSubject),
submittedOn,
TNetEncoding.HTML.Encode(safeName),
TNetEncoding.HTML.Encode(safeSenderEmail),
TNetEncoding.HTML.Encode(safeSubject),
bodyHtml
]
);
message.Clear;
message.From.Address := fromEmail;
message.Recipients.EmailAddresses := recipientEmail;
if safeSenderEmail <> '' then
message.ReplyTo.EmailAddresses := safeSenderEmail;
message.Subject := safeSubject;
message.MessageParts.Clear;
message.ContentType := 'multipart/alternative';
textPart := TIdText.Create(message.MessageParts, nil);
textPart.ContentType := 'text/plain; charset=utf-8';
textPart.Body.Text :=
'New message from EM Systems Website' + sLineBreak +
'Submitted On: ' + submittedOn + sLineBreak +
'From: ' + safeName + ' <' + safeSenderEmail + '>' + sLineBreak +
'Subject: ' + safeSubject + sLineBreak +
sLineBreak +
Body;
htmlPart := TIdText.Create(message.MessageParts, nil);
htmlPart.ContentType := 'text/html; charset=utf-8';
htmlPart.Body.Text := htmlBody;
smtp.Connect;
try
smtp.Send(message);
Logger.Log(2, Format('%s InternalNotificationSent to=%s', [logPrefix, recipientEmail]));
if (safeSenderEmail <> '') and (Pos('@', safeSenderEmail) > 1) then
begin
if SameText(safeSubject, 'Demo Request') then
confirmSubject := 'We received your demo request'
else
confirmSubject := 'We received your message';
confirmHtml := Format(
'<html>' +
'<body style="font-family: Arial, sans-serif; font-size: 14px; color:#222;">' +
'<p style="margin:0 0 10px 0;">Hi %s,</p>' +
'<p style="margin:0 0 10px 0;">Thanks for reaching out. This is a quick confirmation that we received your message.</p>' +
'<p style="margin:0 0 10px 0;"><strong>Subject:</strong> %s</p>' +
'<p style="margin:0 0 10px 0;"><strong>Submitted On:</strong> %s</p>' +
'<p style="margin:0;">Well reply as soon as we can.</p>' +
'</body>' +
'</html>',
[
TNetEncoding.HTML.Encode(safeName),
TNetEncoding.HTML.Encode(safeSubject),
submittedOn
]
);
confirmMsg.Clear;
confirmMsg.From.Address := fromEmail;
confirmMsg.Recipients.EmailAddresses := safeSenderEmail;
confirmMsg.Subject := confirmSubject;
confirmMsg.ExtraHeaders.Values['Auto-Submitted'] := 'auto-replied';
confirmMsg.ExtraHeaders.Values['X-Auto-Response-Suppress'] := 'All';
confirmMsg.MessageParts.Clear;
confirmMsg.ContentType := 'multipart/alternative';
confirmTextPart := TIdText.Create(confirmMsg.MessageParts, nil);
confirmTextPart.ContentType := 'text/plain; charset=utf-8';
confirmTextPart.Body.Text :=
'Hi ' + safeName + ',' + sLineBreak +
sLineBreak +
'Thanks for reaching out. We have received your message and will be in touch soon.' + sLineBreak +
sLineBreak +
'-EM Systems Team';
confirmHtmlPart := TIdText.Create(confirmMsg.MessageParts, nil);
confirmHtmlPart.ContentType := 'text/html; charset=utf-8';
confirmHtmlPart.Body.Text := confirmHtml;
smtp.Send(confirmMsg);
Logger.Log(2, Format('%s ConfirmationSent to=%s', [logPrefix, safeSenderEmail]));
end
else
Logger.Log(2, logPrefix + ' ConfirmationSkipped: invalid sender email');
finally
smtp.Disconnect;
end;
finally
ssl.Free;
confirmMsg.Free;
message.Free;
smtp.Free;
end;
finally
iniFile.Free;
end;
Result := 'Email sent successfully';
end;
function TApiService.VerifyVersion(ClientVersion: string): TJSONObject;
var
iniFile: TIniFile;
iniPath: string;
webClientVersion: string;
errorMsg: string;
begin
iniPath := ChangeFileExt(Application.ExeName, '.ini');
iniFile := TIniFile.Create(iniPath);
try
webClientVersion := iniFile.ReadString('Settings', 'webClientVersion', '');
finally
iniFile.Free;
end;
errorMsg := '';
if (webClientVersion <> '') and (ClientVersion <> webClientVersion) then
begin
Logger.Log(2, Format('VerifyVersion called with mismatch. Server expects %s but client is %s.',
[webClientVersion, ClientVersion]));
errorMsg := 'You are running the wrong version of the website, please click the button to update.';
end;
Result := TJSONObject.Create;
Result.AddPair('webClientVersion', webClientVersion);
if errorMsg <> '' then
Result.AddPair('error', errorMsg);
Logger.Log(2, Format('VerifyVersion: client=%s server=%s mismatch=%s',
[ClientVersion, webClientVersion, BoolToStr(errorMsg <> '', True)]));
end;
initialization
RegisterServiceType(TApiService);
end.
object AppServerModule: TAppServerModule
Height = 264
Width = 254
object SparkleStaticServer: TSparkleStaticServer
Dispatcher = SparkleHttpSysDispatcher
Left = 114
Top = 160
object SparkleStaticServerCompress: TSparkleCompressMiddleware
end
object SparkleStaticServerLogging: 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
object SparkleHttpSysDispatcher: TSparkleHttpSysDispatcher
Left = 112
Top = 66
end
end
unit App.Server.Module;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections,
Sparkle.Comp.Server, Sparkle.Comp.StaticServer, Sparkle.Comp.HttpSysDispatcher,
Sparkle.Module.Static, Sparkle.Comp.CompressMiddleware,
Sparkle.HttpServer.Module, Sparkle.HttpServer.Context,
Sparkle.Comp.LoggingMiddleware;
type
TAppServerModule = class(TDataModule)
SparkleHttpSysDispatcher: TSparkleHttpSysDispatcher;
SparkleStaticServer: TSparkleStaticServer;
SparkleStaticServerCompress: TSparkleCompressMiddleware;
SparkleStaticServerLogging: TSparkleLoggingMiddleware;
private
{ Private declarations }
public
{ Public declarations }
procedure StartAppServer(ABaseUrl: string);
end;
const
SERVER_PATH_SEGMENT = 'app';
var
AppServerModule: TAppServerModule;
implementation
uses
Sparkle.Middleware.Compress,
Common.Logging,
Common.Config;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
{ TAppServerModule }
procedure TAppServerModule.StartAppServer(ABaseUrl: string);
var
url: string;
begin
url := ABaseUrl;
if not url.EndsWith('/') then
url := url + '/';
url := url + SERVER_PATH_SEGMENT;
SparkleStaticServer.BaseUrl := url;
SparkleStaticServer.RootDir := serverConfig.webAppFolder;
SparkleHttpSysDispatcher.Start;
Logger.Log(1, Format('App server module listening at "%s", rootDir: %s', [url, serverConfig.webAppFolder]));
end;
end.
// The configuartion file for the program. Contains important info like the admin
// password and the secret token. Should likely move this to the ini file..
unit Common.Config;
interface
const
defaultServerUrl = 'http://localhost:2001/emsys/';
type
TServerConfig = class
private
Furl: string;
FJWTTokenSecret: string;
FAdminPassword: string;
FWebAppFolder: string;
FMemoLogLevel: Integer;
FFileLogLevel: Integer;
public
constructor Create;
property url: string read FUrl write FUrl;
property jwtTokenSecret: string read FJWTTokenSecret write FJWTTokenSecret;
property adminPassword: string read FAdminPassword write FAdminPassword;
property webAppFolder: string read FWebAppFolder write FWebAppFolder;
end;
procedure LoadServerConfig;
var
serverConfig: TServerConfig;
implementation
uses
Bcl.Json, System.SysUtils, System.IOUtils,
Common.Logging;
procedure LoadServerConfig;
var
configFile: string;
localConfig: TServerConfig;
begin
Logger.Log( 1, '--LoadServerConfig - start' );
configFile := 'serverconfig.json';
Logger.Log( 1, '-- Config file: ' + ConfigFile );
if TFile.Exists(ConfigFile) then
begin
Logger.Log( 1, '-- Config file found.' );
localConfig := TJson.Deserialize<TServerConfig>(TFile.ReadAllText(configFile));
Logger.Log( 1, '-- localConfig loaded from config file' );
serverConfig.Free;
Logger.Log( 1, '-- serverConfig.Free - called' );
serverConfig := localConfig;
Logger.Log( 1, '-- serverConfig := localConfig - called' );
end
else
begin
Logger.Log( 1, '-- Config file not found.' );
end;
Logger.Log( 1, '-------------------------------------------------------------' );
Logger.Log( 1, '-- serverConfig.Server url: ' + serverConfig.url );
Logger.Log( 1, '-- serverConfig.adminPassword: ' + serverConfig.adminPassword );
Logger.Log( 1, '-- serverConfig.jwtTokenSecret: ' + serverConfig.jwtTokenSecret );
Logger.Log( 1, '-- serverConfig.webAppFolder: ' + serverConfig.webAppFolder );
Logger.Log( 1, '--LoadServerConfig - end' );
end;
{ TServerConfig }
constructor TServerConfig.Create;
//var
// ConfigFile: string;
// ServerConfigStr: string;
begin
Logger.Log( 1, '--TServerConfig.Create - start' );
url := defaultServerUrl;
adminPassword := 'whatisthisusedfor';
jwtTokenSecret := 'super_secret0123super_secret4567';
webAppFolder := 'static';
// ServerConfigStr := Bcl.Json.TJson.Serialize( ServerConfig );
// ConfigFile := 'serverconfig.json';
// TFile.WriteAllText( ConfigFile, ServerConfigStr );
// Logger.Log( 1, 'ServerConfig saved to file: ' + ConfigFile );
Logger.Log( 1, '--TServerConfig.Create - end' );
end;
end.
unit Common.Logging;
interface
uses
Generics.Collections;
type
ILog = interface;
ILogAppender = interface;
ILogger = interface
['{4D667DD2-BE11-496B-B92A-C47E03520BD6}']
procedure Log(logLevel: integer; Msg: string); overload;
procedure Log(logLevel: integer; Log: ILog); overload;
procedure AddAppender(ALogAppender: ILogAppender);
function Appenders: TArray<ILogAppender>;
end;
ILogAppender = interface
['{A3B7D6FB-C75F-4BEF-8797-907B6FDAD5D2}']
procedure Send(logLevel: integer; Log: ILog);
end;
ILog = interface
['{8E9C6580-C099-47C0-8B1B-6D7A28EC4FA3}']
function GetMessage: string;
end;
TLogger = class( TInterfacedObject, ILogger )
strict private
FAppenders: TList<ILogAppender>;
public
constructor Create; overload;
constructor Create(ALogger: ILogger); overload;
destructor Destroy; override;
procedure Log(logLevel: integer; Msg: string); overload;
procedure Log(logLevel: integer; Log: ILog); overload;
procedure AddAppender(ALogAppender: ILogAppender);
function Appenders: TArray<ILogAppender>;
end;
TLogMessage = class( TInterfacedObject, ILog )
private
FMsg: string;
public
constructor Create(AMsg: string);
function GetMessage: string;
end;
function Logger: ILogger;
implementation
var
_Logger: ILogger;
function Logger: ILogger;
begin
Result := _Logger;
end;
{ TLogMessage }
constructor TLogMessage.Create(AMsg: string);
begin
FMsg := AMsg;
end;
function TLogMessage.GetMessage: string;
begin
Result := FMsg;
end;
{ TLogger }
procedure TLogger.AddAppender(ALogAppender: ILogAppender);
begin
FAppenders.Add(ALogAppender);
end;
function TLogger.Appenders: TArray<ILogAppender>;
var
I: integer;
begin
SetLength(Result, FAppenders.Count);
for I := 0 to FAppenders.Count - 1 do
Result[I] := FAppenders[I];
end;
constructor TLogger.Create(ALogger: ILogger);
var
Appender: ILogAppender;
begin
FAppenders := TList<ILogAppender>.Create;
if ALogger <> nil then
for Appender in ALogger.Appenders do
AddAppender(Appender);
end;
constructor TLogger.Create;
begin
Create(nil);
end;
destructor TLogger.Destroy;
begin
FAppenders.Free;
inherited;
end;
procedure TLogger.Log(logLevel: integer; Log: ILog);
var
Appender: ILogAppender;
begin
for Appender in FAppenders do
Appender.Send(logLevel, Log);
end;
procedure TLogger.Log(logLevel: integer; Msg: string);
begin
Log(logLevel, TLogMessage.Create(Msg));
end;
initialization
_Logger := TLogger.Create;
end.
unit Common.Middleware.Logging;
interface
uses
System.Classes, System.SysUtils,
Sparkle.HttpServer.Module,
Sparkle.HttpServer.Context,
Sparkle.Http.Headers,
Common.Logging;
type
TLoggingMiddleware = class(THttpServerMiddleware, IHttpServerMiddleware)
private
FLogger: ILogger;
function GetNewHttpRequestLog(Request: THttpServerRequest): ILog;
protected
procedure ProcessRequest(Context: THttpServerContext; Next: THttpServerProc); override;
public
constructor Create(ALogger: ILogger);
end;
THttpRequestLog = class( TInterfacedObject, ILog )
strict private
FMethod: string;
FUriPath: string;
FUriQuery: string;
FProtocol: string;
FRemoteIp: string;
FHeaders: string;
FContent: string;
FContentLength: Int64;
public
constructor Create(AMethod: string; AUriPath: string; AUriQuery: string;
AProtocol: string; ARemoteIp: string; AHeaders: string; AContent: string;
AContentLength: Int64);
function GetMessage: string;
end;
// THttpResponseLog = class( TInterfacedObject, ILog )
// strict private
// FMethod: string;
// FUriPath: string;
// FUriQuery: string;
// FProtocol: string;
// FRemoteIp: string;
// FHeaders: string;
// FContent: string;
// FContentLength: Int64;
// public
// constructor Create(AMethod: string; AUriPath: string; AUriQuery: string;
// AProtocol: string; ARemoteIp: string; AHeaders: string; AContent: string;
// AContentLength: Int64);
// function GetMessage: string;
// end;
implementation
{ TLoggingMiddleware }
constructor TLoggingMiddleware.Create(ALogger: ILogger);
begin
FLogger := TLogger.Create(ALogger);
end;
function TLoggingMiddleware.GetNewHttpRequestLog(
Request: THttpServerRequest): ILog;
var
Msg: TStrings;
Header: THttpHeaderInfo;
StringStream: TStringStream;
Headers, Content: string;
begin
Result := nil;
Msg := TStringList.Create;
try
if Length(Request.Headers.AllHeaders.ToArray) = 0 then
Headers := ''
else
begin
for Header in Request.Headers.AllHeaders do
Msg.Add(Header.Name + ': ' + Header.Value);
Headers := Msg.Text;
end;
finally
Msg.Free;
end;
StringStream := TStringStream.Create(Request.Content);
try
Content := StringStream.DataString
finally
StringStream.Free;
end;
Result := THttpRequestLog.Create(
Request.Method,
Request.Uri.Path,
Request.Uri.Query,
Request.Protocol,
Request.RemoteIp,
Headers,
Content,
Request.ContentLength
);
end;
procedure TLoggingMiddleware.ProcessRequest(Context: THttpServerContext;
Next: THttpServerProc);
var
RequestLogMessage: string;
begin
FLogger.Log(1, 'TLoggingMiddleware.ProcessRequest:');
RequestLogMessage := GetNewHttpRequestLog(Context.Request).GetMessage;
Context.Response.OnHeaders(
procedure(Resp: THttpServerResponse)
begin
FLogger.Log(1, Format('--Resp.StatusCode: %d Resp.StatusReason: %s on %s',
[Resp.StatusCode, Resp.StatusReason, RequestLogMessage]));
end
);
FLogger.Log(1, Format('--Request.RemoteIP: %s RequestLogMessage: %s',
[Context.Request.RemoteIp, RequestLogMessage]));
Next(Context);
end;
{ THttpRequestLog }
constructor THttpRequestLog.Create(AMethod, AUriPath, AUriQuery,
AProtocol, ARemoteIp, AHeaders, AContent: string; AContentLength: Int64);
begin
FMethod := AMethod;
FUriPath := AUriPath;
FUriQuery := AUriQuery;
FProtocol := AProtocol;
FRemoteIp := ARemoteIp;
FHeaders := AHeaders;
FContent := AContent;
FContentLength := AContentLength;
end;
function THttpRequestLog.GetMessage: string;
var
Msg: TStrings;
begin
Result := '';
Msg := TStringList.Create;
try
Msg.Add(Format('%s %s %s',
[
FMethod,
FUriPath + FUriQuery,
FProtocol,
FRemoteIp
]));
// if Not FHeaders.IsEmpty then
// Msg.Add(FHeaders);
// if (Not FContent.IsEmpty) then
// Msg.Add(FContent);
Result := Trim(Msg.Text);
finally
Msg.Free;
end;
end;
end.
unit Data;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, AdvUtil, Data.DB, Vcl.Grids, AdvObj,
BaseGrid, AdvGrid, DBAdvGrid, MemDS, DBAccess, Uni;
type
TFData = class(TForm)
DBAdvGrid1: TDBAdvGrid;
DataSource1: TDataSource;
private
{ Private declarations }
public
{ Public declarations }
end;
var
FData: TFData;
implementation
{$R *.dfm}
end.
object FMain: TFMain
Left = 0
Top = 0
Caption = 'emSystemsWebsiteServer'
ClientHeight = 583
ClientWidth = 764
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OnClose = FormClose
DesignSize = (
764
583)
TextHeight = 13
object memoInfo: TMemo
Left = 16
Top = 40
Width = 744
Height = 535
Anchors = [akLeft, akTop, akRight, akBottom]
ReadOnly = True
TabOrder = 0
end
object btnSwaggerUI: TButton
Left = 299
Top = 8
Width = 128
Height = 25
Caption = 'Launch SwaggerUI'
TabOrder = 1
OnClick = btnSwaggerUIClick
end
object btnExit: TButton
Left = 671
Top = 8
Width = 75
Height = 25
Caption = 'Exit'
TabOrder = 2
OnClick = btnExitClick
end
object initTimer: TTimer
Interval = 250
OnTimer = initTimerTimer
Left = 159
Top = 405
end
object ExeInfo1: TExeInfo
Version = '1.6.1.1'
Left = 246
Top = 406
end
end
unit Main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, Vcl.ExtCtrls, System.IniFiles, ExeInfo, Common.Logging, Common.Config;
type
TFMain = class(TForm)
btnExit: TButton;
initTimer: TTimer;
memoInfo: TMemo;
ExeInfo1: TExeInfo;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure initTimerTimer(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure btnSwaggerUIClick(Sender: TObject);
private
procedure StartServers;
end;
var
FMain: TFMain;
implementation
uses
Api.Server.Module, Sparkle.Utils, Api.Database, App.Server.Module, Api.Service, Api.ServiceImpl;
{$R *.dfm}
{ TFMain }
procedure TFMain.btnExitClick(Sender: TObject);
begin
Close;
end;
procedure TFMain.btnSwaggerUIClick(Sender: TObject);
begin
ShellExecute(Handle, 'open', PChar(TSparkleUtils.CombineUrlFast(ApiServerModule.XDataServer.BaseUrl, 'swaggerui')), nil, nil, SW_SHOWNORMAL);
end;
procedure TFMain.initTimerTimer(Sender: TObject);
begin
initTimer.Enabled := False;
Caption := Caption + ' ver ' + ExeInfo1.FileVersion;
Logger.Log(1, 'Exe version: ' + ExeInfo1.FileVersion);
try
ServerConfig := TServerConfig.Create;
LoadServerConfig;
StartServers;
except
on E: Exception do
begin
Logger.Log(5, 'Failed to initialize server: ' + E.Message);
memoInfo.Lines.Add('Failed to start server: ' + E.Message);
end;
end;
end;
procedure TFMain.StartServers;
var
appServerUrl: string;
begin
Logger.Log(1, '');
Logger.Log(1, '******************************************************');
Logger.Log(1, ' emWebsite XData Server ');
Logger.Log(1, Format(' Version: %s ', [FMain.ExeInfo1.FileVersion]));
Logger.Log(1, ' by EM Systems, Inc. ');
Logger.Log(1, '******************************************************');
Logger.Log(1, '');
try
ApiServerModule := TApiServerModule.Create(Self);
ApiServerModule.StartApiServer(ServerConfig.url);
Logger.Log(1, 'API Server started at: ' + ApiServerModule.XDataServer.BaseUrl);
AppServerModule := TAppServerModule.Create(Self);
AppServerModule.StartAppServer(ServerConfig.url);
Logger.Log(1, 'App Server started at: ' + appServerUrl);
Logger.Log(1, '');
except
on E: Exception do
begin
Logger.Log(5, 'Failed to start server modules: ' + E.Message);
memoInfo.Lines.Add('Failed to start server modules: ' + E.Message);
end;
end;
Logger.Log(1, '--- StartServers: Complete ---');
end;
procedure TFMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
ApiServerModule.Free;
AppServerModule.Free;
serverConfig.Free;
end;
end.
object DMConnection: TDMConnection
Height = 256
Width = 337
object ApiConnection: TXDataWebConnection
URL = 'http://localhost:2002/website/'
Left = 48
Top = 80
end
object xdwcApi: TXDataWebClient
Connection = ApiConnection
Left = 187
Top = 84
end
end
unit ConnectionModule;
interface
uses
System.SysUtils, System.Classes,
WEBLib.Modules,
XData.Web.Connection, XData.Web.Client;
type
TVersionCheckCallback = reference to procedure(success: Boolean; errorMessage: string);
TDMConnection = class(TWebDataModule)
ApiConnection: TXDataWebConnection;
xdwcApi: TXDataWebClient;
procedure ApiConnectionError(error: TXDataWebConnectionError);
procedure XDataWebClientError(error: TXDataClientError);
private
configLoaded: Boolean;
versionCheckPending: Boolean;
pendingCallback: TVersionCheckCallback;
procedure LoadConfig(loadedProc: TProc<Boolean>);
public
const clientVersion = '1.0.3';
procedure InitApp(readyProc: TProc);
procedure SetClientConfig(callback: TVersionCheckCallback);
end;
var
DMConnection: TDMConnection;
implementation
uses
JS, Web,
XData.Web.Request,
XData.Web.Response;
{$R *.dfm}
procedure TDMConnection.ApiConnectionError(error: TXDataWebConnectionError);
begin
if versionCheckPending and Assigned(pendingCallback) then
begin
versionCheckPending := False;
pendingCallback(False, 'Unable to reach server to verify version. Please reload.');
pendingCallback := nil;
end;
end;
procedure TDMConnection.XDataWebClientError(error: TXDataClientError);
begin
if versionCheckPending and Assigned(pendingCallback) then
begin
versionCheckPending := False;
pendingCallback(False, 'Unable to verify version. Please reload.');
pendingCallback := nil;
end;
end;
procedure TDMConnection.LoadConfig(loadedProc: TProc<Boolean>);
procedure onSuccess(response: IHttpResponse);
var
obj: TJSObject;
apiUrl: string;
begin
apiUrl := '';
if response.StatusCode = 200 then
begin
obj := TJSObject(TJSJSON.parse(response.ContentAsText));
apiUrl := JS.toString(obj['ApiUrl']);
end;
if apiUrl <> '' then
begin
ApiConnection.URL := apiUrl;
configLoaded := True;
loadedProc(True);
end
else
loadedProc(False);
end;
procedure onError;
begin
loadedProc(False);
end;
var
conn: TXDataWebConnection;
begin
if configLoaded then
begin
loadedProc(True);
Exit;
end;
conn := TXDataWebConnection.Create(nil);
try
conn.SendRequest(THttpRequest.Create('config/config.json'), @onSuccess, @onError);
finally
conn.Free;
end;
end;
procedure TDMConnection.InitApp(readyProc: TProc);
begin
if Assigned(readyProc) then
readyProc;
end;
procedure TDMConnection.SetClientConfig(callback: TVersionCheckCallback);
procedure configLoadedProc(success: Boolean);
begin
if not success then
begin
callback(False, 'Missing or invalid config/config.json (ApiUrl).');
Exit;
end;
pendingCallback := callback;
versionCheckPending := True;
xdwcApi.Connection := ApiConnection;
ApiConnection.Open(
procedure
begin
xdwcApi.RawInvoke('IApiService.VerifyVersion', [clientVersion],
procedure(response: TXDataClientResponse)
var
jsonResult: TJSObject;
errorMsg: string;
begin
versionCheckPending := False;
jsonResult := TJSObject(response.Result);
if Assigned(jsonResult) and jsonResult.HasOwnProperty('error') then
errorMsg := string(jsonResult['error'])
else
errorMsg := '';
if errorMsg <> '' then
pendingCallback(False, errorMsg)
else
pendingCallback(True, '');
pendingCallback := nil;
end);
end);
end;
begin
LoadConfig(@configLoadedProc);
end;
end.
<html lang="en">
<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>Envoy International Foreign Exchange Services</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 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" type="text/javascript"></script>
<script async="async" defer="defer" src="https://www.google.com/recaptcha/api.js"></script>
<link href="css/App.css" rel="stylesheet" type="text/css"/>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<script src="$(ProjectName).js" type="text/javascript"></script>
</head>
<body>
<script type="text/javascript">rtl.run();</script>
</body>
</html>
unit Site.Footer;
interface
uses
System.SysUtils, WEBLib.WebTools, WEBLib.Controls, JS, Web;
procedure InjectFooter;
procedure SetFooterVersion(const versionText: string);
implementation
const
// Note: keep this in ONE place. Edit footer here, all pages update.
FooterHtml =
'<footer class="bg-dark text-light">' +
' <div class="container-main py-4">' +
' <div class="row gy-3 align-items-start text-center text-lg-start">' +
' <div class="col-lg-4 d-flex justify-content-center justify-content-lg-start">' +
' <a href="#FHome" class="navbar-brand m-0">' +
' <img src="images/EM_Logo_2c66a0.png" alt="EM_Logo" style="height: 30px;">' +
' </a>' +
' </div>' +
' <div class="col-lg-4">' +
' <ul class="list-unstyled mb-0 small">' +
' <li class="mb-1"><a href="#FHome" id="homefooter" class="link-light text-decoration-none">Home</a></li>' +
' <li class="mb-1"><a href="#FAboutUs" id="aboutusfooter" class="link-light text-decoration-none">About Us</a></li>' +
' <li><a href="#FContactUs" id="contactusfooter" class="link-light text-decoration-none">Contact Us</a></li>' +
' </ul>' +
' </div>' +
' <div class="col-lg-4">' +
' <address class="mb-0 small opacity-75">' +
' 4043 Maple Rd, Suite 211<br>' +
' Amherst, NY 14226<br>' +
' (716) 836-4910' +
' </address>' +
' </div>' +
' </div>' +
' <div class="border-top border-secondary mt-3 pt-3">' +
' <div class="d-flex flex-column flex-sm-row justify-content-center align-items-center gap-2 text-center small opacity-75">' +
' <div> 2011-2026 EM Systems Inc</div>' +
' <span class="d-none d-sm-inline"></span>' +
' <span id="lbl_version"></span>' +
' </div>' +
' </div>' +
' </div>' +
'</footer>';
procedure InjectFooter;
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('site_footer'));
if Assigned(el) then
el.innerHTML := FooterHtml;
end;
procedure SetFooterVersion(const versionText: string);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('lbl_version'));
if Assigned(el) then
el.textContent := versionText;
end;
end.
unit Site.Navbar;
interface
procedure InjectNavbar;
implementation
uses
JS, Web;
const
NavbarHtml =
'<div class="envoy-header">' +
' <div class="container envoy-container">' +
' <nav class="navbar navbar-expand-lg py-0">' +
' <div class="container-fluid px-0">' +
' <span class="navbar-brand envoy-brand me-lg-4 mb-0">' +
' <img src="images/e_head2_01.png" alt="Envoy International">' +
' </span>' +
' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#envoyNavbar"' +
' aria-controls="envoyNavbar" aria-expanded="false" aria-label="Toggle navigation">' +
' <span class="navbar-toggler-icon"></span>' +
' </button>' +
' <div class="collapse navbar-collapse" id="envoyNavbar">' +
' <ul class="navbar-nav ms-lg-auto envoy-nav">' +
' <li class="nav-item"><span class="nav-link">Home</span></li>' +
' <li class="nav-item"><span class="nav-link">Information Guide</span></li>' +
' <li class="nav-item"><span class="nav-link">Business Services</span></li>' +
' <li class="nav-item"><span class="nav-link">Compliance</span></li>' +
' <li class="nav-item"><span class="nav-link">Industry Links</span></li>' +
' <li class="nav-item"><span class="nav-link">Contact Us</span></li>' +
' </ul>' +
' </div>' +
' </div>' +
' </nav>' +
' </div>' +
'</div>';
procedure InjectNavbar;
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(document.getElementById('site_nav'));
if Assigned(el) then
el.innerHTML := NavbarHtml;
end;
end.
unit Utils;
interface
uses
System.Classes, SysUtils, JS, Web, WEBLib.Forms;
procedure ShowStatusMessage(const AMessage, AClass: string; const AElementId: string);
procedure HideStatusMessage(const AElementId: string);
procedure ShowSpinner(SpinnerID: string);
procedure HideSpinner(SpinnerID: string);
procedure ShowNotificationModal(msg: string);
implementation
procedure ShowStatusMessage(const AMessage, AClass: string; const AElementId: string);
var
StatusMessage: TJSHTMLElement;
begin
StatusMessage := TJSHTMLElement(document.getElementById(AElementId));
if Assigned(StatusMessage) then
begin
if AMessage = '' then
begin
StatusMessage.style.setProperty('display', 'none');
StatusMessage.className := '';
StatusMessage.innerHTML := '';
end
else
begin
StatusMessage.innerHTML := AMessage;
StatusMessage.className := 'alert ' + AClass;
StatusMessage.style.setProperty('display', 'block');
end
end
else
console.log('Error: Status message element not found');
end;
procedure HideStatusMessage(const AElementId: string);
var
StatusMessage: TJSHTMLElement;
begin
StatusMessage := TJSHTMLElement(document.getElementById(AElementId));
if Assigned(StatusMessage) then
begin
StatusMessage.style.setProperty('display', 'none');
StatusMessage.className := '';
StatusMessage.innerHTML := '';
end
else
console.log('Error: Status message element not found');
end;
procedure ShowSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then
begin
// Move spinner to the <body> if it's not already there
asm
if (SpinnerElement.parentNode !== document.body) {
document.body.appendChild(SpinnerElement);
}
end;
SpinnerElement.classList.remove('d-none');
SpinnerElement.classList.add('d-block');
end;
end;
procedure HideSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then
begin
SpinnerElement.classList.remove('d-block');
SpinnerElement.classList.add('d-none');
end;
end;
procedure ShowNotificationModal(msg: string);
begin
asm
var modal = document.getElementById('contact_notification_modal');
var label = document.getElementById('contact_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;
end.
object FAboutUs: TFAboutUs
Width = 1058
Height = 730
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
end
<div class="d-flex flex-column min-vh-100">
<div id="site_nav"></div>
<div class="container-fluid px-0">
<div class="hero-section-aboutus bg-dark text-white text-center">
<img src="images/niagara.jpg" alt="Our Story Image" class="img-fluid hero-image-aboutus">
<div class="hero-overlay-aboutus">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6"> <!-- Adjust the column sizes as needed -->
<div class="text-wrapper-aboutus mx-auto p-3 p-md-5 rounded shadow">
<h2>Our Story</h2>
<p class="fs-6">
Located in Buffalo, New York, EM Systems has designed and delivered a wide range of custom software
solutions across multiple industries, with the strongest emphasis on Public Safety Systems. Today, more
than 75 organizations throughout New York State depend on EM Systems applications to support their
communities and agencies.
EM Systems is distinguished by its ability to develop tailored software solutions that address the
unique requirements of each client. Our mission is to deliver reliable, efficient software that enhances
and streamlines existing business or agency operations. In addition to deep expertise in software
development, we bring extensive experience in analyzing, refining, and improving operational workflows
to drive greater efficiency and effectiveness.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="site_footer"></div>
</div>
unit View.AboutUs;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, Vcl.StdCtrls,
WEBLib.StdCtrls, Vcl.Controls, WEBLib.Dialogs, Vcl.Imaging.pngimage,
WEBLib.ExtCtrls, WEBLib.Controls, Web, JS, WEBLib.Menus, WEBLib.WebCtrls;
type
TFAboutUs = class(TWebForm)
[async]
procedure WebFormShow(Sender: TObject);
private
{ Private declarations }
public
procedure NavScrollSizing;
end;
var
FAboutUs: TFAboutUs;
implementation
{$R *.dfm}
uses
View.Home,
View.ContactUs,
View.CustomSoftware,
View.PublicSafety,
Site.Navbar,
Site.Footer,
ConnectionModule,
View.RecordsManagement;
procedure TFAboutUs.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFAboutUs.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50; // Adjust based on your preference
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
initialization
RegisterClass(TFAboutUs);
end.
\ No newline at end of file
object FContactUs: TFContactUs
Width = 930
Height = 802
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
object edtName: TWebEdit
Left = 24
Top = 40
Width = 100
Height = 25
TabStop = False
ElementID = 'name'
ElementFont = efCSS
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
HideSelection = False
WidthPercent = 100.000000000000000000
end
object edtEmail: TWebEdit
Left = 24
Top = 75
Width = 100
Height = 25
TabStop = False
ElementID = 'email'
ElementFont = efCSS
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
HideSelection = False
WidthPercent = 100.000000000000000000
end
object edtSubject: TWebEdit
Left = 24
Top = 112
Width = 100
Height = 25
TabStop = False
ElementID = 'subject'
ElementFont = efCSS
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
HideSelection = False
WidthPercent = 100.000000000000000000
end
object memoMessage: TWebMemo
Left = 28
Top = 151
Width = 464
Height = 303
TabStop = False
ElementID = 'message'
ElementFont = efCSS
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Role = 'null'
SelLength = 0
SelStart = 0
WidthPercent = 100.000000000000000000
end
object btnSubmit: TWebButton
Left = 36
Top = 482
Width = 100
Height = 25
Caption = 'Send Email'
ElementID = 'submit'
ElementFont = efCSS
ElementPosition = epRelative
HeightPercent = 100.000000000000000000
Role = 'button'
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnSubmitClick
end
object xdwcEmail: TXDataWebClient
Connection = DMConnection.ApiConnection
OnError = xdwcEmailError
Left = 412
Top = 512
end
end
<div class="d-flex flex-column min-vh-100">
<div id="site_nav"></div>
<!-- CONTACT US SECTION -->
<main class="container pt-5 mt-5 flex-grow-1">
<div class="row g-4">
<div class="col-lg-6">
<h2 class="mb-3">Contact Us</h2>
<hr class="mb-4">
<form>
<div class="mb-3">
<label for="name" class="form-label">Name*</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email*</label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="mb-3">
<label for="subject" class="form-label">Subject*</label>
<input type="text" class="form-control" id="subject" required>
</div>
<div class="mb-3">
<label for="message" class="form-label">Your Message*</label>
<textarea class="form-control" id="message" rows="4" required></textarea>
</div>
<button type="submit" class="btn btn-primary" id="submit">Submit</button>
</form>
<div id="card_status_message" class="alert mt-3 d-none" role="alert"></div>
</div>
<div class="col-lg-6">
<h2 class="mb-3">EM Systems Inc.</h2>
<hr class="mb-4">
<p class="mb-1">4043 Maple Rd, Suite 211 | Amherst, NY 14226</p>
<p class="mb-3"><strong>Phone:</strong> (716) 836-4910</p>
<div class="ratio ratio-4x3 shadow-sm rounded overflow-hidden">
<iframe
src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d11673.673539086676!2d-78.8117081!3d42.990524!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x89d37240e1804e2f%3A0xce8f13b6c4c68386!2sE%20M%20Systems%20Inc!5e0!3m2!1sen!2sus!4v1714072248166!5m2!1sen!2sus"
style="border:0;"
allowfullscreen=""
loading="lazy"
referrerpolicy="no-referrer-when-downgrade">
</iframe>
</div>
</div>
</div>
</main>
<div id="site_footer" class="mt-auto"></div>
</div>
<div id="spinner" class="position-fixed top-50 start-50 translate-middle d-none" style="z-index: 1100;">
<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="contact_notification_modal" tabindex="-1" aria-labelledby="contact_notification_modal_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="contact_notification_modal_label">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="contact_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.ContactUs;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, Vcl.StdCtrls,
WEBLib.StdCtrls, Vcl.Controls, WEBLib.Dialogs, Vcl.Imaging.pngimage,
WEBLib.ExtCtrls, WEBLib.Controls, Web, JS, WEBLib.Menus, WEBLib.WebCtrls,
XData.Web.Connection, XData.Web.Client, Data.DB, Utils,
XData.Web.JsonDataset, XData.Web.Dataset, Vcl.Grids,
WEBLib.DBCtrls, WEBLib.DB, WEBLib.Grids, WEBLib.CDS, WEBLib.REST,
WEBLib.WebTools, System.NetEncoding, WebLib.RegularExpressions,
WEBLib.Toast, XData.Web.Request, XData.Web.Response;
type
TFContactUs = class(TWebForm)
edtName: TWebEdit;
edtEmail: TWebEdit;
edtSubject: TWebEdit;
memoMessage: TWebMemo;
btnSubmit: TWebButton;
xdwcEmail: TXDataWebClient;
procedure btnSubmitClick(Sender: TObject);
procedure WebFormShow(Sender: TObject);
procedure xdwcEmailError(Error: TXDataClientError);
private
{ Private declarations }
function IsInputValid: Boolean;
function IsEmailValid(const email: String): Boolean;
[async] procedure SendEmail;
public
procedure NavScrollSizing;
end;
var
FContactUs: TFContactUs;
implementation
{$R *.dfm}
uses
ConnectionModule,
View.Home,
View.PublicSafety,
View.RecordsManagement,
View.CustomSoftware,
Site.Navbar,
Site.Footer,
View.AboutUs;
procedure TFContactUs.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFContactUs.xdwcEmailError(Error: TXDataClientError);
begin
ShowNotificationModal('Error when attempting to send: ' + error.ErrorMessage);
end;
procedure TFContactUs.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50;
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
function TFContactUs.IsEmailValid(const email: string): Boolean;
const
pattern = '^[^\s@]+@[^\s@]+\.[^\s@]+$';
begin
Result := TRegEx.IsMatch(email.Trim, pattern);
end;
function TFContactUs.IsInputValid: Boolean;
var
nameOk: Boolean;
emailOk: Boolean;
subjectOk: Boolean;
messageOk: Boolean;
begin
nameOk := edtName.Text.Trim <> '';
emailOk := IsEmailValid(edtEmail.Text);
subjectOk := edtSubject.Text.Trim <> '';
messageOk := memoMessage.Text.Trim <> '';
Result := nameOk and emailOk and subjectOk and messageOk;
end;
procedure TFContactUs.btnSubmitClick(Sender: TObject);
begin
if not IsInputValid then
begin
ShowNotificationModal('Please complete all fields and ensure the email address is valid.');
Exit;
end;
if not DMConnection.ApiConnection.Connected then
begin
DMConnection.ApiConnection.Open(
procedure
begin
SendEmail;
end);
Exit;
end;
SendEmail;
end;
procedure TFContactUs.SendEmail;
begin
xdwcEmail.RawInvoke('IApiService.SendEmail',
[edtName.Text, edtEmail.Text, edtSubject.Text, memoMessage.Text],
procedure(response: TXDataClientResponse)
begin
ShowNotificationModal('Your message has been sent successfully. Please check your email for a response in the next 48 hours.');
edtName.Text := '';
edtEmail.Text := '';
edtSubject.Text := '';
memoMessage.Text := '';
end);
end;
initialization
RegisterClass(TFContactUs);
end.
object FCustomSoftware: TFCustomSoftware
Width = 640
Height = 480
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
end
<div class="d-flex flex-column min-vh-100">
<div id="site_nav"></div>
<!-- HERO SECTION -->
<div class="container-fluid px-0">
<div class="page-hero-section bg-dark text-white text-center">
<img
src="images\computer_code.jpg"
alt="Hero Image"
class="img-fluid hero-image"
/>
<div
class="hero-overlay d-flex justify-content-center align-items-center"
>
<div class="text-wrapper">
<h1 class="display-4">Custom Software</h1>
</div>
</div>
</div>
</div>
<!--MAIN CONTAINER-->
<div class="container-main">
<div class="learn-boxes">
<!-- CUSTOM SOFTWARE BOX 1 -->
<div class="row mb-4 g-4 align-items-center bg-white shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<p class="mx-auto" style="max-width: 850px">
EM Systems started as a custom software development company. Our
Public Safety and Records Management Notification System solutions
started as custom software development projects. Over the years,
we have developed many custom programs that are currently in use
by our customers to help manage and run their operations.
</p>
</div>
</div>
</div>
<!-- CUSTOM SOFTWARE BOX 2 -->
<div class="row mb-4 g-4 align-items-center bg-grey shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>AutoPark</h2>
<p class="mx-auto" style="max-width: 850px">
AutoPark is a parking permit and ticket management system. It
streamlines and coordinates the process and procedures of managing
a parking area. From parking permits, tickets, online payments,
and appeals, this solution has it all when it comes to parking
management. This program is in use by a State University of New
York college to manage their parking services operation.
</p>
</div>
</div>
</div>
<!-- CUSTOM SOFTWARE BOX 3 -->
<div class="row mb-4 g-4 align-items-center bg-white shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>TreeStar</h2>
<p class="mx-auto" style="max-width: 850px">
TreeStar is a full suite of tools to manage wholesale Christmas
trees and accessories. It has tools for load and route planning,
carrier and shipping oversight, customer support, sales analysis,
invoicing, and field management. It also has EDI support for
automatic processing of purchase orders, sales, and payments.
</p>
</div>
</div>
</div>
<!-- CUSTOM SOFTWARE BOX 4 -->
<div class="row mb-4 g-4 align-items-center bg-grey shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>HSTManager</h2>
<p class="mx-auto" style="max-width: 850px">
This is a workflow management program for Home Sleep Studies
(HST). It is used by a Sleep Medicine company to manage the HSTs’
flow of information through the process of scheduling, sleep
testing, diagnosing, and generating the medical reports.
HSTManager interfaces to the Electronic Medical Records system and
the Diagnostic Sleep Study Software to automatically exchange the
data needed to allow sleep lab technicians and clinicians to track
the HSTs from start to end. It starts by retrieving the list of
scheduled HSTs and end by uploading the final physician report to
the patient medical record.
</p>
</div>
</div>
</div>
<!-- CUSTOM SOFTWARE BOX 5 -->
<div class="row mb-4 g-4 align-items-center bg-white shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>PawnShopPro</h2>
<p class="mx-auto" style="max-width: 850px">
PawnShopPro simplifies your pawn shop record keeping, billing, and
inventory management. It manages your loans, repairs, sales, and
layaways, and automatically processes interest charges and late
notices.
</p>
</div>
</div>
</div>
<!-- CUSTOM SOFTWARE BOX 6 -->
<div class="row mb-4 g-4 align-items-center bg-grey shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>TransportStar</h2>
<p class="mx-auto" style="max-width: 850px">
TransportStar is everything you need to manage your trucking
company. Track your shipments and calculate costs for your
same-day and next-day LTL and TL shipments, generate invoices and
track payments, and manage your expenses for your fleet of trucks.
</p>
</div>
</div>
</div>
</div>
</div>
<div id="site_footer"></div>
</div>
unit View.CustomSoftware;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, Vcl.StdCtrls,
WEBLib.StdCtrls, Vcl.Controls, WEBLib.Dialogs, Vcl.Imaging.pngimage,
WEBLib.ExtCtrls, WEBLib.Controls, Web, JS, WEBLib.Menus, WEBLib.WebCtrls;
type
TFCustomSoftware = class(TWebForm)
[async]
procedure WebFormShow(Sender: TObject);
private
{ Private declarations }
public
procedure NavScrollSizing;
end;
var
FCustomSoftware: TFCustomSoftware;
implementation
{$R *.dfm}
uses
View.AboutUs,
View.ContactUs,
View.Home,
View.PublicSafety,
Site.Navbar,
Site.Footer,
ConnectionModule,
View.RecordsManagement;
procedure TFCustomSoftware.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFCustomSoftware.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50; // Adjust based on your preference
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
initialization
RegisterClass(TFCustomSoftware);
end.
\ No newline at end of file
object FHome: TFHome
Width = 1290
Height = 1090
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
end
<div class="envoy-page min-vh-100 d-flex flex-column">
<div id="site_nav"></div>
<main class="flex-grow-1">
<section class="envoy-main py-4 py-md-5">
<div class="container envoy-container">
<div class="row justify-content-center">
<div class="col-12">
<div class="envoy-card shadow-sm p-4 p-md-5">
<p class="envoy-subhead mb-4">
The Envoy International website is currently undergoing maintenance.
</p>
<p class="envoy-text mb-4">
We apologize for the inconvenience. If you have any questions or would like a quotation on a currency transaction,
please contact Envoy International using the information below.
</p>
<div class="row g-4">
<div class="col-md-6">
<p class="envoy-section-title mb-2">Contact Envoy International</p>
<p class="envoy-text mb-1">
<strong>Phone</strong> (800) 755-5614
</p>
<p class="envoy-text mb-1">
<strong>Fax</strong> (800) 294-1173
</p>
<p class="envoy-text mb-0">
<strong>Email</strong>
<span>info@envoyinternational.com</span>
</p>
</div>
<div class="col-md-6">
<p class="envoy-section-title mb-2">Office Information</p>
<p class="envoy-text mb-3">
<strong>Envoy International</strong><br>
1972 Military Road<br>
Niagara Falls, NY 14304
</p>
<p class="envoy-text mb-0">
<strong>Envoy International Management Ltd.</strong><br>
427 Garrison Road<br>
Fort Erie, Ontario L2A 6E6
</p>
</div>
</div>
</div>
<div class="envoy-footer mt-3">
<b>Note:</b> Not all services described herein are available in all areas.
Please call us for specific information about services available in your area.
<br>
© Envoy International. © Envoy International Management Ltd. All rights reserved.
</div>
<div class="text-center py-3">
<span class="envoy-managed">Website Managed by EM Systems, Inc.</span>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
unit View.Home;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms,
WEBLib.Controls;
type
TFHome = class(TWebForm)
procedure WebFormShow(Sender: TObject);
end;
var
FHome: TFHome;
implementation
{$R *.dfm}
uses
Site.Navbar;
procedure TFHome.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clWhite;
InjectNavbar;
end;
initialization
RegisterClass(TFHome);
end.
object FPublicSafety: TFPublicSafety
Width = 640
Height = 480
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
end
unit View.PublicSafety;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, Vcl.StdCtrls,
WEBLib.StdCtrls, Vcl.Controls, WEBLib.Dialogs, Vcl.Imaging.pngimage,
WEBLib.ExtCtrls, WEBLib.Controls, Web, JS, WEBLib.Menus, WEBLib.WebCtrls;
type
TFPublicSafety = class(TWebForm)
procedure WebFormShow(Sender: TObject);
[async]
private
{ Private declarations }
public
procedure NavScrollSizing;
end;
var
FPublicSafety: TFPublicSafety;
implementation
{$R *.dfm}
uses
View.AboutUs,
View.ContactUs,
View.CustomSoftware,
View.Home,
Site.Navbar,
Site.Footer,
ConnectionModule,
View.RecordsManagement;
procedure TFPublicSafety.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFPublicSafety.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50; // Adjust based on your preference
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
initialization
RegisterClass(TFPublicSafety);
end.
\ No newline at end of file
object FRecordsManagement: TFRecordsManagement
Width = 640
Height = 480
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
Font.Charset = ANSI_CHARSET
Font.Color = clBlack
Font.Height = -11
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
OnShow = WebFormShow
end
<div class="d-flex flex-column min-vh-100">
<div id="site_nav"></div>
<!-- HERO SECTION -->
<div class="container-fluid px-0">
<div class="page-hero-section bg-dark text-white text-center" id="FRecordsManagement" tabindex="-1">
<img src="images\computer_code.jpg" alt="Hero Image" class="img-fluid hero-image">
<div class="hero-overlay d-flex justify-content-center align-items-center">
<div class="text-wrapper">
<h1 class="display-4">Records Management Notification System</h1>
</div>
</div>
</div>
</div>
<!--MAIN CONTAINER-->
<div class="container-main">
<div class="learn-boxes">
<!-- RECORDS MANAGEMENT BOX 1 -->
<div class="row mb-4 g-4 align-items-center bg-white shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>Records Retention Notification</h2>
<p class="mx-auto" style="max-width: 850px;">With multi departmental government organizations constantly in
a state of flux, the Records Management Notification System is designed to provide consistent and informed
alerts to aid governments in following records management best practices.
This system is designed to help Town Clerk’s, Police Departments, and Records Managers fulfill records
retention mandates by providing alerts to complete standard records management practices. This software
will aid in the management of day-to-day records and help local government meet records retention
requirements.
The Records Retention RMNS is fully aligned with the New York State LG-1 schedule, integrating
comprehensive retention and disposition policies that streamline the management of both electronic and
paper records through a task-based notification system.
</p>
</div>
</div>
</div>
<!-- RECORDS MANAGEMENT BOX 2 -->
<div class="row mb-4 g-4 align-items-center bg-gray shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>Legal Hold Notices</h2>
<p class="mx-auto" style="max-width: 850px;">The Legal Holds Notices RMNS module is designed to notify
government officials and records managers of a current obligation to preserve evidence and ensure critical
information remains intact and accessible for review under a legal hold. The notification will remind key
stakeholders that legal holds are current and in place; preventing governments from receiving penalties
for non-compliance.</p>
</div>
</div>
</div>
<!-- RECORDS MANAGEMENT BOX 3 -->
<div class="row mb-4 g-4 align-items-center bg-white shadow-sm rounded">
<div class="col-md-12">
<div class="p-3 text-center">
<h2>Freedom of Information Law (FOIL)</h2>
<p class="mx-auto" style="max-width: 850px;">The FOIL Requests RMNS module provides automated notifications
to government employees when a FOIL request is received and alerts users to the required tasks and steps
necessary to maintain compliance with the Freedom of Information Law. The system issues reminders to
ensure requests are properly filed, delivers automatic status updates, and audits completed tasks to
support ongoing compliance and accountability.</p>
</div>
</div>
</div>
</div>
</div>
<div id="site_footer"></div>
</div>
unit View.RecordsManagement;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, Vcl.StdCtrls,
WEBLib.StdCtrls, Vcl.Controls, WEBLib.Dialogs, Vcl.Imaging.pngimage,
WEBLib.ExtCtrls, WEBLib.Controls, Web, JS, WEBLib.Menus, WEBLib.WebCtrls;
type
TFRecordsManagement = class(TWebForm)
[async]
procedure WebFormShow(Sender: TObject);
private
{ Private declarations }
public
procedure NavScrollSizing;
end;
var
FRecordsManagement: TFRecordsManagement;
implementation
{$R *.dfm}
uses
View.AboutUs,
View.ContactUs,
View.CustomSoftware,
View.PublicSafety,
Site.Navbar,
Site.Footer,
ConnectionModule,
View.Home;
procedure TFRecordsManagement.WebFormShow(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFRecordsManagement.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50; // Adjust based on your preference
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
initialization
RegisterClass(TFRecordsManagement);
end.
\ No newline at end of file
object FRequestDemo: TFRequestDemo
Width = 640
Height = 480
Caption = 'EM Systems, Inc'
CSSLibrary = cssBootstrap
ElementFont = efCSS
OnCreate = WebFormCreate
object lblReqPrefMethod: TWebLabel
Left = 50
Top = 218
Width = 225
Height = 15
Caption = 'Please choose a preferred contact method.'
ElementID = 'lbl_req_pref_method'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object edtReqName: TWebEdit
Left = 50
Top = 34
Width = 267
Height = 22
TabStop = False
ElementID = 'edt_req_name'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtReqEmail: TWebEdit
Left = 50
Top = 62
Width = 121
Height = 22
TabStop = False
ChildOrder = 1
ElementClassName = 'form-control'
ElementID = 'edt_req_email'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtReqAgency: TWebEdit
Left = 50
Top = 90
Width = 121
Height = 22
TabStop = False
ChildOrder = 2
ElementID = 'edt_req_agency'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtReqPhoneNumber: TWebEdit
Left = 196
Top = 62
Width = 121
Height = 22
TabStop = False
ChildOrder = 3
EditType = weNumeric
ElementClassName = 'form-control'
ElementID = 'edt_req_phone_number'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object radReqPhone: TWebRadioButton
Left = 50
Top = 239
Width = 117
Height = 22
Caption = 'Phone'
Checked = False
ChildOrder = 5
Color = clNone
ElementID = 'rad_req_phone'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
OnClick = radReqPhoneClick
end
object radReqEmail: TWebRadioButton
Left = 196
Top = 239
Width = 121
Height = 22
Caption = 'Email'
Checked = True
ChildOrder = 6
Color = clNone
ElementID = 'rad_req_email'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = radReqEmailClick
end
object btnReqSendRequest: TWebButton
Left = 50
Top = 267
Width = 96
Height = 25
Caption = 'Send Request'
ChildOrder = 7
ElementID = 'btn_req_send_request'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnReqSendRequestClick
end
object edtReqState: TWebEdit
Left = 196
Top = 90
Width = 121
Height = 22
TabStop = False
ChildOrder = 8
ElementClassName = 'form-control'
ElementID = 'edt_req_state'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object edtReqCounty: TWebEdit
Left = 50
Top = 118
Width = 121
Height = 22
TabStop = False
ChildOrder = 8
ElementClassName = 'form-control'
ElementID = 'edt_req_county'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object memoAdditionalComments: TWebMemo
Left = 50
Top = 147
Width = 267
Height = 65
TabStop = False
ElementID = 'memo_additional_comments'
ElementFont = efCSS
HeightPercent = 100.000000000000000000
Lines.Strings = (
'')
SelLength = 0
SelStart = 2
ShowFocus = False
WidthPercent = 100.000000000000000000
end
object wcbInterest: TWebComboBox
Left = 196
Top = 118
Width = 121
Height = 23
ElementClassName = 'form-select'
ElementID = 'wcb_interest'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
ShowFocus = False
TabStop = False
WidthPercent = 100.000000000000000000
ItemIndex = -1
Items.Strings = (
''
'Public Safety'
'Records Management'
'Custom Software'
'Other')
end
object xdwcRequestDemo: TXDataWebClient
Connection = DMConnection.ApiConnection
OnError = xdwcRequestDemoError
Left = 264
Top = 362
end
end
<div class="d-flex flex-column min-vh-100">
<div id="site_nav"></div>
<!-- REQUEST DEMO SECTION -->
<div class="container pt-5 mt-5 flex-grow-1">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-7">
<div class="card shadow-sm border-0">
<div class="card-body p-4 p-md-5">
<h2 class="mb-2 text-center">Request a Demo</h2>
<p class="text-muted text-center mb-4">
Please fill in the information, and we will reach out to schedule a demo.
</p>
<form id="frm_request_demo" novalidate>
<div class="row g-3">
<div class="col-12">
<label for="edt_req_name" class="form-label mb-1">Name*</label>
<input type="text" class="form-control" id="edt_req_name">
<div id="fb_req_name" class="invalid-feedback">Please enter your name.</div>
</div>
<div class="col-12 col-md-7">
<label for="edt_req_email" class="form-label mb-1">Email*</label>
<input type="email" class="form-control" id="edt_req_email">
<div id="fb_req_email" class="invalid-feedback">Please enter a valid email address.</div>
</div>
<div class="col-12 col-md-5">
<label for="edt_req_phone_number" class="form-label mb-1">Phone Number*</label>
<input type="tel" class="form-control" id="edt_req_phone_number">
<div id="fb_req_phone" class="invalid-feedback">Please enter a phone number.</div>
</div>
<div class="col-12 col-md-6">
<label for="edt_req_agency" class="form-label mb-1">Company/Agency*</label>
<input type="text" class="form-control" id="edt_req_agency">
<div id="fb_req_agency" class="invalid-feedback">Please enter your Agency or Company.</div>
</div>
<div class="col-12 col-md-6">
<label for="edt_req_state" class="form-label mb-1">State</label>
<input type="text" class="form-control" id="edt_req_state">
</div>
<div class="col-12 col-md-6">
<label for="edt_req_county" class="form-label mb-1">County</label>
<input type="text" class="form-control" id="edt_req_county">
</div>
<div class="col-12 col-md-6">
<label for="wcb_interest" class="form-label mb-1">Interest</label>
<select class="form-select" id="wcb_interest"></select>
</div>
<div class="col-12">
<label for="memo_additional_comments" class="form-label mb-1">Additional Comments</label>
<textarea class="form-control" id="memo_additional_comments" rows="4"></textarea>
</div>
<div class="col-12">
<div id="lbl_req_pref_method" class="form-label mb-2">Preferred Contact Method</div>
<div class="d-flex flex-column flex-sm-row gap-2 gap-sm-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="req_pref_method" id="rad_req_phone">
<label class="form-check-label" for="rad_req_phone">Phone</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="req_pref_method" id="rad_req_email">
<label class="form-check-label" for="rad_req_email">Email</label>
</div>
</div>
</div>
<div class="col-12 mt-4">
<button type="button" class="btn btn-primary w-100 d-md-inline-block w-md-auto px-4" id="btn_req_send_request">
Send Request
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div id="site_footer" class="mt-auto"></div>
</div>
<div id="spinner" class="position-fixed top-50 start-50 translate-middle d-none" style="z-index: 1100;">
<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="contact_notification_modal" tabindex="-1" aria-labelledby="contact_notification_modal_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="contact_notification_modal_label">Demo Requested Successfully</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="contact_notification_modal_body">
Please contact EMSystems to solve the issue.
</div>
<div class="modal-footer justify-content-center">
<a href="#FHome"
id="btn_modal_close"
class="btn btn-primary"
data-bs-dismiss="modal"
onclick="window.location.hash='#FHome';">
Close
</a>
</div>
</div>
</div>
</div>
unit View.RequestDemo;
interface
uses
System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
WEBLib.Forms, WEBLib.Dialogs, Vcl.StdCtrls, WEBLib.StdCtrls, Vcl.Controls,
XData.Web.Client, ConnectionModule, Utils, WEBLib.RegularExpressions;
type
TFRequestDemo = class(TWebForm)
edtReqName: TWebEdit;
edtReqEmail: TWebEdit;
edtReqAgency: TWebEdit;
edtReqPhoneNumber: TWebEdit;
lblReqPrefMethod: TWebLabel;
radReqPhone: TWebRadioButton;
radReqEmail: TWebRadioButton;
btnReqSendRequest: TWebButton;
xdwcRequestDemo: TXDataWebClient;
edtReqState: TWebEdit;
edtReqCounty: TWebEdit;
memoAdditionalComments: TWebMemo;
wcbInterest: TWebComboBox;
procedure btnReqSendRequestClick(Sender: TObject);
procedure radReqEmailClick(Sender: TObject);
procedure radReqPhoneClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
procedure xdwcRequestDemoError(Error: TXDataClientError);
private
function IsEmailValid(const email: string): Boolean;
function PreferredMethod: string;
procedure NavScrollSizing;
procedure ClearFieldErrors;
procedure SetInvalid(const inputId: string);
function ValidateRequest: Boolean;
procedure SendRequest;
end;
var
FRequestDemo: TFRequestDemo;
implementation
{$R *.dfm}
uses
Site.Navbar,
Site.Footer;
procedure TFRequestDemo.WebFormCreate(Sender: TObject);
begin
Application.ThemeColor := clTMSWEB;
InjectNavbar;
InjectFooter;
SetFooterVersion('ver - ' + TDMConnection.clientVersion);
NavScrollSizing;
end;
procedure TFRequestDemo.xdwcRequestDemoError(Error: TXDataClientError);
begin
Utils.HideSpinner('spinner');
ShowNotificationModal('Error when attempting to send: ' + Error.ErrorMessage);
end;
procedure TFRequestDemo.NavScrollSizing;
begin
asm
window.addEventListener('scroll', function() {
var navbar = document.querySelector('.em-navbar-wrap .navbar');
var logo = document.querySelector('.em-brand img');
var scrollDistance = 50;
if (window.scrollY > scrollDistance) {
navbar.classList.add('scrolled');
logo.classList.add('scrolled-logo');
} else {
navbar.classList.remove('scrolled');
logo.classList.remove('scrolled-logo');
}
});
end;
end;
function TFRequestDemo.IsEmailValid(const email: string): Boolean;
const
pattern = '^[^\s@]+@[^\s@]+\.[^\s@]+$';
begin
Result := TRegEx.IsMatch(email.Trim, pattern);
end;
function TFRequestDemo.PreferredMethod: string;
begin
if radReqPhone.Checked then
Exit('phone');
if radReqEmail.Checked then
Exit('email');
Result := '';
end;
procedure TFRequestDemo.SetInvalid(const inputId: string);
var
inputEl: TJSHTMLElement;
begin
inputEl := TJSHTMLElement(document.getElementById(inputId));
if Assigned(inputEl) then
inputEl.classList.add('is-invalid');
end;
procedure TFRequestDemo.ClearFieldErrors;
var
inputEl: TJSHTMLElement;
begin
inputEl := TJSHTMLElement(document.getElementById('edt_req_name'));
if Assigned(inputEl) then inputEl.classList.remove('is-invalid');
inputEl := TJSHTMLElement(document.getElementById('edt_req_email'));
if Assigned(inputEl) then inputEl.classList.remove('is-invalid');
inputEl := TJSHTMLElement(document.getElementById('edt_req_phone_number'));
if Assigned(inputEl) then inputEl.classList.remove('is-invalid');
inputEl := TJSHTMLElement(document.getElementById('edt_req_agency'));
if Assigned(inputEl) then inputEl.classList.remove('is-invalid');
end;
function TFRequestDemo.ValidateRequest: Boolean;
var
isValid: Boolean;
emailText: string;
begin
ClearFieldErrors;
isValid := True;
if edtReqName.Text.Trim = '' then
begin
SetInvalid('edt_req_name');
isValid := False;
end;
emailText := edtReqEmail.Text.Trim;
if emailText = '' then
begin
SetInvalid('edt_req_email');
isValid := False;
end
else if not IsEmailValid(emailText) then
begin
SetInvalid('edt_req_email');
isValid := False;
end;
if edtReqPhoneNumber.Text.Trim = '' then
begin
SetInvalid('edt_req_phone_number');
isValid := False;
end;
if edtReqAgency.Text.Trim = '' then
begin
SetInvalid('edt_req_agency');
isValid := False;
end;
Result := isValid;
end;
procedure TFRequestDemo.btnReqSendRequestClick(Sender: TObject);
begin
if not ValidateRequest then
Exit;
Utils.ShowSpinner('spinner');
if not DMConnection.ApiConnection.Connected then
begin
DMConnection.ApiConnection.Open(
procedure
begin
SendRequest;
end);
Exit;
end;
SendRequest;
end;
procedure TFRequestDemo.radReqEmailClick(Sender: TObject);
begin
if radReqEmail.Checked then
radReqPhone.Checked := False;
end;
procedure TFRequestDemo.radReqPhoneClick(Sender: TObject);
begin
if radReqPhone.Checked then
radReqEmail.Checked := False;
end;
procedure TFRequestDemo.SendRequest;
var
bodyText: string;
pref: string;
begin
pref := PreferredMethod;
bodyText :=
'Demo request details:' + sLineBreak +
'Name: ' + edtReqName.Text + sLineBreak +
'Email: ' + edtReqEmail.Text + sLineBreak +
'Phone: ' + edtReqPhoneNumber.Text + sLineBreak +
'Company/Agency: ' + edtReqAgency.Text + sLineBreak +
'State: ' + edtReqState.Text + sLineBreak +
'County: ' + edtReqCounty.Text + sLineBreak +
'Interest: ' + wcbInterest.Text + sLineBreak +
'Additional comments: ' + memoAdditionalComments.Text;
if pref <> '' then
bodyText := bodyText + sLineBreak + 'Preferred contact: ' + pref;
xdwcRequestDemo.RawInvoke('IApiService.SendEmail',
[edtReqName.Text, edtReqEmail.Text, 'Demo Request', bodyText],
procedure(response: TXDataClientResponse)
begin
Utils.HideSpinner('spinner');
ClearFieldErrors;
edtReqName.Text := '';
edtReqEmail.Text := '';
edtReqPhoneNumber.Text := '';
edtReqAgency.Text := '';
edtReqState.Text := '';
edtReqCounty.Text := '';
memoAdditionalComments.Text := '';
radReqPhone.Checked := False;
radReqEmail.Checked := True;
wcbInterest.ItemIndex := -1;
ShowNotificationModal('Your demo request has been submitted. Please check your email for confirmation. If you did not recieve an email, reach out directly.');
end);
end;
initialization
RegisterClass(TFRequestDemo);
end.
{
"ApiUrl" : "http://localhost:2002/website/",
"AppUrl" : "http://localhost:2002/website/App"
}
html,
body {
min-height: 100%;
}
body {
font-family: Arial, Helvetica, sans-serif;
color: #333333;
background: #ffffff;
margin: 0;
}
.envoy-page {
min-height: 100vh;
background: #ffffff;
}
.envoy-container {
max-width: 900px;
}
.envoy-header {
background-color: #e9eef4;
background-image: url("../images/simple_bg2.png");
background-repeat: repeat-x;
background-position: top center;
}
.envoy-brand img {
width: 158px;
height: 106px;
object-fit: contain;
}
.envoy-nav .nav-link {
color: #999999;
font-size: 13px;
font-weight: 700;
padding: 0.45rem 0.65rem;
cursor: default;
white-space: nowrap;
opacity: 0.55;
}
.envoy-nav .nav-link:hover,
.envoy-nav .nav-link:focus {
color: #315a83;
}
.envoy-main {
background-color: #ffffff;
background-image: url("../images/simple_bg.jpg");
background-repeat: repeat-x;
background-position: top center;
min-height: 370px;
}
.envoy-card {
background: #ffffff;
border: 1px solid #dddddd;
}
.envoy-subhead {
color: #315a83;
font-size: 24px;
line-height: 1.35;
font-weight: 400;
}
.envoy-text {
color: #555555;
font-size: 13px;
line-height: 1.6;
font-weight: 400;
}
.envoy-section-title {
color: #315a83;
font-size: 17px;
line-height: 1.3;
font-weight: 700;
}
.envoy-footer {
color: #666666;
font-size: 11px;
line-height: 1.5;
}
.envoy-managed {
color: #666666;
font-size: 11px;
opacity: 0.55;
}
@media (max-width: 991.98px) {
.envoy-header {
padding-bottom: 0.75rem;
}
}
.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: var(--bs-primary);
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 envoyInternationalWeb;
{$R *.dres}
uses
Vcl.Forms,
WEBLib.Forms,
View.Home in 'View.Home.pas' {FHome: TWebForm} {*.html},
View.PublicSafety in 'View.PublicSafety.pas' {FPublicSafety: TWebForm} {*.html},
View.RecordsManagement in 'View.RecordsManagement.pas' {FRecordsManagement: TWebForm} {*.html},
View.CustomSoftware in 'View.CustomSoftware.pas' {FCustomSoftware: TWebForm} {*.html},
View.AboutUs in 'View.AboutUs.pas' {FAboutUs: TWebForm} {*.html},
View.ContactUs in 'View.ContactUs.pas' {FContactUs: TWebForm} {*.html},
Utils in 'Utils.pas',
ConnectionModule in 'ConnectionModule.pas' {DMConnection: TWebDataModule},
View.RequestDemo in 'View.RequestDemo.pas' {FRequestDemo: TWebForm} {*.html},
Site.Footer in 'Site.Footer.pas',
Site.Navbar in 'Site.Navbar.pas';
{$R *.res}
const
MaintenanceMode = True;
procedure StartMaintenancePage;
begin
asm
window.location.hash = '#FHome';
end;
Application.CreateForm(TFHome, FHome);
end;
procedure ShowVersionModal(const errorMessage, clientVer: string);
begin
asm
var dlg = document.createElement("dialog");
dlg.classList.add("shadow", "rounded", "border", "p-4");
dlg.style.maxWidth = "520px";
dlg.style.width = "92%";
dlg.style.fontFamily = "system-ui, sans-serif";
dlg.innerHTML =
"<h5 class='fw-bold mb-3 text-danger'>EM Systems web site</h5>" +
"<p class='mb-3' style='white-space: pre-wrap;'>" + errorMessage + "</p>" +
"<div class='text-end'>" +
"<button id='refreshBtn' class='btn btn-danger'>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;
procedure StartRouting;
begin
asm
window.addEventListener('hashchange', function() {
window.scrollTo(0, 0);
}, false);
if (window.location.hash === '') {
window.location.hash = '#FHome';
}
end;
if not Application.Route then
Application.CreateForm(TFHome, FHome);
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
StartRouting
else
ShowVersionModal(errorMessage, clientVer);
end
);
end
);
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
if MaintenanceMode then
StartMaintenancePage
else
begin
Application.CreateForm(TDMConnection, DMConnection);
StartApplication;
end;
Application.Run;
end.
 <!--NAVBAR CONTAINER-->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow">
<div class="container-fluid">
<a class="navbar-brand" href="#FHome">
<img src="images\EM_Logo_2c66a0.png" alt="EM_Logo" style="max-height: 50px;">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" id="homenav" href="#FHome">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" id="aboutusnav" href="#FAboutUs">About Us</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
What We Do
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" id="customsoftwarenav" href="#FCustomSoftware">Custom Software</a></li>
<li><a class="dropdown-item" id="publicsafetynav" href="#FPublicSafety">Public Safety</a></li>
<li><a class="dropdown-item" id="recordsmanagementnav" href="#FRecordsManagement">Records Management</a>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" id="contactusnav" href="#FContactUs">Contact Us</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- HERO SECTION -->
<div class="hero-section bg-dark text-white text-center" style="margin-top: 56px;">
<img class="img-fluid" src="images\table_with_computers.jpg" alt="Hero Image">
<div class="d-flex justify-content-center align-items-center" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0;">
<div class="text-center">
<h1 class="display-4">Custom Software, Comprehensive Support</h1>
<p class="lead">Real connections. Reliable support. Software made for you.</p>
</div>
</div>
</div>
<!-- MAIN CONTENT-->
<div class="container my-5 flex-grow-1">
<div class="row g-4">
<!-- First Box: Records and Information Management Solutions -->
<div class="col-md-6">
<img src="http://gator3304.temp.domains/~emsys065/wp-content/uploads/2021/12/archive-gc84e8bebd_1920-1024x683.jpg" class="img-fluid rounded" alt="Storage Image">
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="text-center">
<h2>Records Management Notification System</h2>
<p>EM Systems has several new records and information management systems which incorporate modern paradigms in the field. Thinking of integrating electronic medical records with public safety systems? We can help. Feel free to contact us any time.</p>
<a href="#FRecordsManagement" class="btn btn-primary" id="recordsmanagementlearn">Learn More</a>
</div>
</div>
<!-- Second Box: Integrated Public Safety Software -->
<div class="col-md-6 order-md-2">
<img src="http://gator3304.temp.domains/~emsys065/wp-content/uploads/2021/12/digitization-g72e897cbb_1920-1024x680.jpg" class="img-fluid rounded" alt="Logistics Image">
</div>
<div class="col-md-6 d-flex align-items-center order-md-1">
<div class="text-center">
<h2>Integrated Public Safety Software</h2>
<p>EM Systems maintains a full suite of proven integrated systems that can be tailored to fit the specific needs of your organization. Seeking to move from an older outdated, or more expensive solution? Facilitate a smooth transition and don’t lose a thing. Ask us about data conversion and training programs.</p>
<a href="#FPublicSafety" class="btn btn-primary" id="publicsafetylearn">Learn More</a>
</div>
</div>
<!-- Third Box: Custom Software Development -->
<div class="col-md-6">
<img src="http://gator3304.temp.domains/~emsys065/wp-content/uploads/2021/10/computer-ga073c6b91_1920-1024x682.jpg" class="img-fluid rounded" alt="Computer Image">
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="text-center">
<h2>Custom Software Development</h2>
<p>EM Systems has great talent when it comes to working with customers to develop software solutions designed and tailored to their specific needs. Over the years we have created hundreds of custom solutions that have worked wonders for our customers.</p>
<a href="#FCustomSoftware" class="btn btn-primary" id="customsoftwarelearn">Learn More</a>
</div>
</div>
</div>
</div>
<!-- FOOTER -->
<footer class="bg-dark text-light py-3">
<div class="container">
<div class="row text-center">
<!-- Logo Column -->
<div class="col-md-4 mb-3">
<a href="#FHome" class="navbar-brand">
<img src="images/EM_Logo_2c66a0.png" alt="EM_Logo" style="max-height: 30px">
</a>
</div>
<div class="col-md-4 mb-3">
<div>
<h5>Explore</h5>
<ul class="list-unstyled">
<li><a href="#FHome" id="homefooter">Home</a></li>
<li><a href="#FAboutUs" id="aboutusfooter">About Us</a></li>
<li><a href="#FContactUs" id="contactusfooter">Contact Us</a></li>
</ul>
</div>
</div>
<div class="col-md-4 mb-3">
<div>
<address>
4043 Maple Rd, Suite 211<br>
Amherst, NY 14226<br>
(716) 836-4910
</address>
</div>
</div>
</div>
</div>
<div class="footer-copyright">
© 2011-2024 EM Systems Inc
</div>
</footer>
\ No newline at end of file
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