Commit db1c3eb9 by Mac Stephens

Simplify shared and agency folder layout, rename INI selector to Agency, and update developer guide

parent 4944d577
......@@ -45,16 +45,16 @@ Shared/Server/Source/__history/
Shared/Server/Source/__recovery/
Shared/Web/__history/
Shared/Web/__recovery/
WC/Web/Win32/
WC/Web/Win64/
WC/Web/TMSWeb/
WC/Web/**/__history/
WC/Web/**/__recovery/
BFD/Web/Win32/
BFD/Web/Win64/
BFD/Web/TMSWeb/
BFD/Web/**/__history/
BFD/Web/**/__recovery/
Agency Specific/WC/Web/Win32/
Agency Specific/WC/Web/Win64/
Agency Specific/WC/Web/TMSWeb/
Agency Specific/WC/Web/**/__history/
Agency Specific/WC/Web/**/__recovery/
Agency Specific/BFD/Web/Win32/
Agency Specific/BFD/Web/Win64/
Agency Specific/BFD/Web/TMSWeb/
Agency Specific/BFD/Web/**/__history/
Agency Specific/BFD/Web/**/__recovery/
# Runtime libraries already versioned with the server
!Shared/Server/bin/FastMM_FullDebugMode.dll
......
......@@ -28,8 +28,8 @@ uses
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in '..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
View.DeviceRegistration in '..\..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res}
......
......@@ -61,7 +61,7 @@
<SanitizedProjectName>bfdEmiMobile</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject>
<TMSWebOutputPath>..\..\Shared\Server\bin\static\BFD</TMSWebOutputPath>
<TMSWebOutputPath>..\..\..\Shared\Server\bin\static\BFD</TMSWebOutputPath>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.802;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;LastCompiledTime=2018/07/25 12:57:53</VerInfo_Keys>
<DCC_Define>SKIA;$(DCC_Define)</DCC_Define>
<TMSWebHTMLFile>index.html</TMSWebHTMLFile>
......@@ -200,11 +200,11 @@
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceRegistration.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceManager.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
......
......@@ -6,7 +6,7 @@
<Projects Include="bfdEmiMobile.dproj">
<Dependencies/>
</Projects>
<Projects Include="..\..\Shared\Server\emiMobileServer.dproj">
<Projects Include="..\..\..\Shared\Server\emiMobileServer.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
......@@ -27,13 +27,13 @@
<MSBuild Projects="bfdEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="emiMobileServer">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj"/>
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj"/>
</Target>
<Target Name="emiMobileServer:Clean">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/>
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/>
</Target>
<Target Name="emiMobileServer:Make">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/>
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="bfdEmiMobile;emiMobileServer"/>
......
{
"AuthUrl" : "http://localhost:2001/emsys/emimobile/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emimobile/api/",
"AppUrl" : "http://localhost:2001/emsys/emimobile/app/",
"WsUrl": "ws://localhost:8091/"
}
/* --- TMS WEB Core Specific Fixes --- */
/* Removes the default border from the main Form wrapper */
span.card {
border: none;
}
/* --- Login Screen Styling --- */
.login-card {
display: inline-block; /* Or use d-flex on the parent to center it */
width: 100%;
max-width: 350px; /* Better than fixed 300px */
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
/* Optional: Custom navbar look for the login screen */
.login-navbar {
max-width: 1200px;
margin: auto;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border: 1px solid #d3d3d3;
}
/* --- Table Customization --- */
/* Bootstrap has .table-hover, but this sets your specific blue color */
.table tbody tr:hover {
background-color: #d1e7fd;
cursor: pointer;
}
/* --- Pagination Theme Overrides --- */
/* These override Bootstrap's blue to your specific darker blue (#204d74) */
.pagination .page-link {
color: #204d74; /* Text color for standard links */
}
/* Active State (Selected Page) */
.pagination .page-item.active .page-link {
background-color: #204d74;
border-color: #204d74;
color: #fff !important;
}
/* Hover State */
.pagination .page-item:not(.active) .page-link:hover {
background-color: #e9ecef; /* Standard Bootstrap light grey hover */
color: #16344a; /* Darker text on hover */
}
/* --- Utilities --- */
.list-section-header:empty {
display: none;
}
#map_pnlmap > span {
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
position: absolute !important;
}
html, body {
height: 100%;
overflow: hidden;
}
/* iOS safe area: absorb status bar into top nav, home indicator into bottom nav */
@supports (padding-top: env(safe-area-inset-top)) {
#top_nav {
padding-top: calc(0.5rem + env(safe-area-inset-top));
}
#bottom_nav {
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
}
}
@supports (-webkit-touch-callout: none) {
/* CSS specific to iOS devices */
span.card {
border: none;
/*padding-top: 2em !important;*/
}
.vh-100 {
height: 93vh !important;
}
}
.tab-hidden { display: none !important; }
.connection-status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 1.75rem;
padding: 0.2rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 999px;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.connection-status-dot {
width: 0.55rem;
height: 0.55rem;
flex: 0 0 auto;
border-radius: 50%;
background-color: #adb5bd;
}
.connection-status-connected .connection-status-dot {
background-color: #75d995;
box-shadow: 0 0 0 0.14rem rgba(117, 217, 149, 0.2);
}
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
background-color: #ffd166;
animation: connection-status-pulse 1.4s ease-in-out infinite;
}
.connection-status-offline .connection-status-dot,
.connection-status-stopped .connection-status-dot {
background-color: #ff8b94;
}
@keyframes connection-status-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
@media (max-width: 430px) {
.connection-status {
width: 1.75rem;
justify-content: center;
padding-right: 0;
padding-left: 0;
}
.connection-status-text {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
animation: none;
}
}
.summary-chevron-icon {
width: 1rem;
height: 1rem;
display: block;
transition: transform 0.15s ease-in-out;
}
/* Expanded => show DOWN (rotate base UP chevron) */
.summary-toggle[aria-expanded="true"] .summary-chevron-icon {
transform: rotate(180deg);
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th,
.time-table td {
overflow: hidden;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.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);
}
}
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{27B04508-3F72-45A2-83EF-A007750D2923}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Projects Include="wcEmiMobile.dproj">
<Dependencies/>
</Projects>
<Projects Include="..\..\..\Shared\Server\emiMobileServer.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Default.Personality/>
</BorlandProject>
</ProjectExtensions>
<Target Name="wcEmiMobile">
<MSBuild Projects="wcEmiMobile.dproj"/>
</Target>
<Target Name="wcEmiMobile:Clean">
<MSBuild Projects="wcEmiMobile.dproj" Targets="Clean"/>
</Target>
<Target Name="wcEmiMobile:Make">
<MSBuild Projects="wcEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="emiMobileServer">
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj"/>
</Target>
<Target Name="emiMobileServer:Clean">
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/>
</Target>
<Target Name="emiMobileServer:Make">
<MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="wcEmiMobile;emiMobileServer"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="wcEmiMobile:Clean;emiMobileServer:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="wcEmiMobile:Make;emiMobileServer:Make"/>
</Target>
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
</Project>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>emiMobile</title>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<!-- jQuery -->
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js" type="text/javascript"></script>
<!-- Icons & fonts -->
<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"/>
<!-- Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" type="text/javascript"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"/>
<!-- App -->
<script src="wcEmiMobile.js" type="text/javascript"></script>
<!-- App styles -->
<style></style>
<link href="css/app.css" rel="stylesheet"/>
</head>
<body>
<script type="text/javascript">rtl.run();</script>
<script>
function setIOSDivHeight() {
const targetDiv = document.querySelector('body'); // Replace with your div ID
if (targetDiv) {
// Get the inner height of the window
const height = window.innerHeight ;
// Apply the height in pixels
targetDiv.style.maxHeight = (height - 100) + 'px';
//alert("set height: " + (height - 100));
}
}
// 2. Attach event listeners for Load, Resize, and Scroll
// Scroll is critical for iOS to handle the address bar expanding/collapsing
const isIOSUA = /iPad|iPhone|iPod/.test(navigator.userAgent);
if(isIOSUA){
window.addEventListener('load', setIOSDivHeight);
window.addEventListener('resize', setIOSDivHeight);
window.addEventListener('scroll', setIOSDivHeight);
}
</script>
</body>
</html>
unit uMapFilters;
interface
uses
System.SysUtils, System.StrUtils,
JS, Web,
VCL.TMSFNCLeaflet, VCL.TMSFNCMaps, VCL.TMSFNCMapsCommonTypes;
type
TMapFilters = class
private
FMap: TTMSFNCLeaflet;
FShowUnits: Boolean;
FShowComplaints: Boolean;
FShowLocation: Boolean;
function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
procedure SetCheckBoxChecked(const elementId: string; checked: Boolean);
procedure SetText(const elementId: string; const value: string);
procedure ReadUi;
procedure ResetUi;
procedure UpdateSummary;
procedure ApplyToMap;
procedure HandleDocClick(e: TJSMouseEvent);
procedure HandleDocChange(e: TJSEvent);
public
constructor Create(AMap: TTMSFNCLeaflet);
procedure Init;
procedure Apply;
procedure Reset;
end;
implementation
{ TMapFilters }
constructor TMapFilters.Create(AMap: TTMSFNCLeaflet);
begin
inherited Create;
FMap := AMap;
FShowUnits := True;
FShowComplaints := True;
FShowLocation := False;
end;
procedure TMapFilters.Init;
begin
Document.addEventListener('click', @HandleDocClick);
Document.addEventListener('change', @HandleDocChange);
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Apply;
begin
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Reset;
begin
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.HandleDocClick(e: TJSMouseEvent);
var
el: TJSElement;
id: string;
begin
el := TJSElement(e.target);
if el = nil then
Exit;
// Note: This supports clicking inner icons/spans by walking up to a parent with an id
asm
while (el && !el.id) { el = el.parentElement; }
end;
if (el <> nil) and (el is TJSHtmlElement) then
begin
id := string(TJSHtmlElement(el).id);
if id = 'map_filters_apply' then
begin
e.preventDefault;
e.stopPropagation;
Apply;
Exit;
end;
if id = 'map_filters_reset' then
begin
e.preventDefault;
e.stopPropagation;
Reset;
Exit;
end;
end;
end;
procedure TMapFilters.HandleDocChange(e: TJSEvent);
begin
//Note: Summary text changes when toggles change
ReadUi;
UpdateSummary;
end;
procedure TMapFilters.ReadUi;
begin
FShowUnits := GetCheckBoxChecked('map_filter_units', True);
FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True);
FShowLocation := GetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.ResetUi;
begin
SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True);
SetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.UpdateSummary;
var
summaryText: string;
begin
summaryText := '';
if FShowUnits then
summaryText := 'Units';
if FShowComplaints then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Complaints';
end;
if FShowLocation then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Location';
end;
if summaryText = '' then
summaryText := 'None';
SetText('map_filters_summary', summaryText);
end;
procedure TMapFilters.ApplyToMap;
var
i: Integer;
m: TTMSFNCMapsMarker;
ds: string;
showMarker: Boolean;
begin
if FMap = nil then
Exit;
FMap.BeginUpdate;
try
for i := 0 to FMap.Markers.Count - 1 do
begin
m := FMap.Markers[i];
ds := Trim(string(m.DataString));
// Note: Map form sets:
// - units: m.DataString := 'unit'
// - complaints: m.DataString := 'complaint'
showMarker := True;
if SameText(ds, 'unit') or StartsText('unit|', ds) then
showMarker := FShowUnits
else if SameText(ds, 'complaint') or StartsText('complaint|', ds) then
showMarker := FShowComplaints
else if SameText(ds, 'device') then
showMarker := FShowLocation;
// Note: TTMSFNCMapsMarker supports visibility toggling
m.Visible := showMarker;
end;
finally
FMap.EndUpdate;
end;
end;
function TMapFilters.GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
var
el: TJSElement;
begin
Result := defaultValue;
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
Result := TJSHtmlInputElement(el).checked;
end;
procedure TMapFilters.SetCheckBoxChecked(const elementId: string; checked: Boolean);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
TJSHtmlInputElement(el).checked := checked;
end;
procedure TMapFilters.SetText(const elementId: string; const value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlElement) then
TJSHtmlElement(el).innerText := value;
end;
end.
program wcEmiMobile;
{$R *.dres}
uses
Vcl.Forms,
System.SysUtils,
XData.Web.Connection,
Auth.Service in 'Auth.Service.pas',
App.Types in 'App.Types.pas',
ConnectionModule in 'ConnectionModule.pas' {DMConnection: TWebDataModule},
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
View.UserProfile in 'View.UserProfile.pas' {FViewUserProfile: TWebForm} {*.html},
View.Units in 'View.Units.pas' {FViewUnits: TWebForm} {*.html},
App.Config in 'App.Config.pas',
Paginator.Plugins in 'Paginator.Plugins.pas',
View.Complaints in 'View.Complaints.pas' {FViewComplaints: TWebForm} {*.html},
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
View.Map in 'View.Map.pas' {FViewMap: TWebForm} {*.html},
View.Admin in 'View.Admin.pas' {FViewAdmin: TWebForm} {*.html},
View.Users in 'View.Users.pas' {FViewUsers: TWebForm} {*.html},
View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html},
Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas',
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in '..\..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res}
procedure DisplayLoginView(AMessage: string = ''); forward;
procedure DisplayDeviceRegistrationView; forward;
procedure DisplayMainView;
procedure ConnectProc;
begin
if Assigned(FViewLogin) then
FreeAndNil(FViewLogin);
if AuthService.DeviceManagementMode then
FViewDeviceManager := TFViewDeviceManager.CreateNew
else
TFViewMain.Display(@DisplayLoginView);
end;
begin
if not DMConnection.ApiConnection.Connected then
DMConnection.ApiConnection.Open(@ConnectProc)
else
ConnectProc;
end;
procedure DisplayLoginView(AMessage: string);
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewDeviceManager) then
FreeAndNil(FViewDeviceManager);
if Assigned(FViewMain) then
FViewMain.Free;
TFViewLogin.Display(@DisplayMainView, AMessage);
end;
procedure DisplayDeviceRegistrationView;
procedure OnRegistered;
begin
// Device registered — proceed to login
if Assigned(FViewDeviceRegistration) then
FViewDeviceRegistration.Free;
TFViewLogin.Display(@DisplayMainView);
end;
begin
if Assigned(FViewDeviceRegistration) then
FViewDeviceRegistration.Free;
TFViewDeviceRegistration.Display(@OnRegistered);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
DisplayLoginView(AMessage);
end;
procedure StartApplication;
begin
if AuthService.DeviceManagementMode then
begin
DisplayLoginView;
Exit;
end;
// Step 1: device must be registered before login is allowed
if not AuthService.IsDeviceRegistered then
begin
DisplayDeviceRegistrationView;
Exit;
end;
// Step 2: normal JWT auth check
if (not AuthService.Authenticated) or AuthService.TokenExpired then
DisplayLoginView
else
DisplayMainView;
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection);
Application.Run;
DMConnection.InitApp(@StartApplication, @UnauthorizedAccessProc);
end.
......@@ -61,7 +61,7 @@
<SanitizedProjectName>wcEmiMobile</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject>
<TMSWebOutputPath>..\..\Shared\Server\bin\static\WC</TMSWebOutputPath>
<TMSWebOutputPath>..\..\..\Shared\Server\bin\static\WC</TMSWebOutputPath>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.802;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;LastCompiledTime=2018/07/25 12:57:53</VerInfo_Keys>
<DCC_Define>SKIA;$(DCC_Define)</DCC_Define>
<TMSWebHTMLFile>index.html</TMSWebHTMLFile>
......@@ -201,11 +201,11 @@
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceRegistration.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceManager.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
......
......@@ -6,7 +6,7 @@ uses
Client.Data, Client.Auth, Device.Store, Uni;
function CreateClientAuth: TClientAuth;
procedure ValidateClient;
procedure ValidateAgency;
function CreateDeviceStore(AConnection: TUniConnection = nil): TDeviceStore;
function CreateClientData: TClientData;
......@@ -18,23 +18,23 @@ uses
function CreateClientAuth: TClientAuth;
begin
if IniEntries.Client = 'BFD' then
if IniEntries.Agency = 'BFD' then
Result := TBFDAuthDatabase.Create(nil)
else
Result := TWCAuthDatabase.Create(nil);
end;
procedure ValidateClient;
procedure ValidateAgency;
begin
if IniEntries.Client = '' then
raise Exception.Create('Missing [Application] Client in the server INI file. Set Client=WC or Client=BFD.');
if (IniEntries.Client <> 'WC') and (IniEntries.Client <> 'BFD') then
raise Exception.CreateFmt('Unrecognized [Application] Client "%s". Supported clients: WC, BFD.', [IniEntries.Client]);
if IniEntries.Agency = '' then
raise Exception.Create('Missing [Application] Agency in the server INI file. Set Agency=WC or Agency=BFD.');
if (IniEntries.Agency <> 'WC') and (IniEntries.Agency <> 'BFD') then
raise Exception.CreateFmt('Unrecognized [Application] Agency "%s". Supported agencies: WC, BFD.', [IniEntries.Agency]);
end;
function CreateClientData: TClientData;
begin
if IniEntries.Client = 'BFD' then
if IniEntries.Agency = 'BFD' then
Result := TBFDAPIDataModule.Create(nil)
else
Result := TWCAPIDataModule.Create(nil);
......@@ -42,7 +42,7 @@ end;
function CreateDeviceStore(AConnection: TUniConnection): TDeviceStore;
begin
if IniEntries.Client = 'BFD' then
if IniEntries.Agency = 'BFD' then
Result := TOracleDeviceDatabase.Create(nil)
else
Result := TPostgreSQLDeviceDatabase.Create(nil);
......
......@@ -65,12 +65,12 @@ var
jsonObj: TJSONObject;
begin
Logger.Log(1, '--LoadServerConfig - start');
ValidateClient;
ValidateAgency;
IniEntries.ValidateDatabaseSettings;
serverConfig.FWebAppFolder := TPath.Combine(TPath.Combine(ExtractFilePath(ParamStr(0)), 'static'), IniEntries.Client);
Logger.Log(1, '-- Client: ' + IniEntries.Client + ' [from ini]');
Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + ' [derived from Client]');
serverConfig.FWebAppFolder := TPath.Combine(TPath.Combine(ExtractFilePath(ParamStr(0)), 'static'), IniEntries.Agency);
Logger.Log(1, '-- Agency: ' + IniEntries.Agency + ' [from ini]');
Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + ' [derived from Agency]');
serverConfig.twilioAccountSid := IniEntries.TwilioAccountSid;
serverConfig.twilioAuthToken := IniEntries.TwilioAuthToken;
......
......@@ -8,7 +8,7 @@ uses
type
TIniEntries = class
private
FClient: string;
FAgency: string;
// [Settings]
FMemoLogLevel: Integer;
FMemoLogLevelFromIni: Boolean;
......@@ -43,7 +43,7 @@ type
constructor Create;
procedure ValidateDatabaseSettings;
procedure ConfigureConnection(Connection: TUniConnection);
property Client: string read FClient;
property Agency: string read FAgency;
// [Settings]
property MemoLogLevel: Integer read FMemoLogLevel;
......@@ -100,7 +100,7 @@ var
begin
iniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
FClient := UpperCase(Trim(iniFile.ReadString('Application', 'Client', '')));
FAgency := UpperCase(Trim(iniFile.ReadString('Application', 'Agency', '')));
// [Settings]
FMemoLogLevel := iniFile.ReadInteger('Settings', 'MemoLogLevel', 3);
......@@ -143,7 +143,7 @@ begin
// Note: Oracle uses a TNS alias; PostgreSQL uses host, port, and database.
if FClient = 'BFD' then
if FAgency = 'BFD' then
begin
FDatabaseServer := Trim(iniFile.ReadString('Oracle', 'Server', ''));
FDatabaseUsername := Trim(iniFile.ReadString('Oracle', 'Username', ''));
......@@ -164,14 +164,14 @@ end;
procedure TIniEntries.ValidateDatabaseSettings;
begin
if (FClient = 'BFD') and
if (FAgency = 'BFD') and
((FDatabaseServer = '') or (FDatabaseUsername = '') or (FDatabasePassword = '')) then
raise Exception.Create('Set Server, Username, and Password in the server INI [Oracle] section.');
end;
procedure TIniEntries.ConfigureConnection(Connection: TUniConnection);
begin
if FClient = 'BFD' then
if FAgency = 'BFD' then
Connection.ProviderName := 'Oracle'
else
begin
......
......@@ -190,7 +190,7 @@ begin
Logger.Log(1, LogValue('--Settings->LogFileNum', IniEntries.LogFileNum.ToString, IniEntries.LogFileNumFromIni));
Logger.Log(1, LogValue('--Settings->webClientVersion', IniEntries.WebClientVersion, IniEntries.WebClientVersionFromIni));
if IniEntries.Client = 'BFD' then
if IniEntries.Agency = 'BFD' then
Logger.Log(1, '--- Oracle database: using [Oracle] INI settings ---')
else
begin
......
# Oracle device storage
Current configuration: Shared/Server/Source/Common.Ini.pas reads [Oracle] Server, Username, Password for Client=BFD. Server is the existing TNS alias. The same ConfigureConnection method configures CAD, authentication and standalone device-store connections. Designer credentials are not a runtime fallback. Oracle.Connection has been removed.
Current configuration: Shared/Server/Source/Common.Ini.pas reads [Oracle] Server, Username, Password for Agency=BFD. Server is the existing TNS alias. The same ConfigureConnection method configures CAD, authentication and standalone device-store connections. Designer credentials are not a runtime fallback. Oracle.Connection has been removed.
Use the common server project for WC and BFD. The standalone BFD project is retired.
......@@ -35,7 +35,7 @@ Oracle cursor/locking reference: https://docs.oracle.com/en/database/oracle/orac
The standalone BFD server builds successfully with this unit. An offline Delphi smoke test instantiated the data module, loaded its DFM, confirmed all 25 queries were inactive and attached to the disconnected connection, checked key/output parameter types, and freed the module. Static checks matched SQL bind names to DFM parameter declarations.
SQL execution, Oracle parameter binding, timestamp/boolean conversions, uniqueness behavior, and concurrent code allocation still require tests against the eventual tables. The shared Device.Store boundary and PostgreSQL counterpart are implemented, and WC services use them. Oracle operations implement the same contract but still require live validation and selection/configuration integration. Client=BFD remains disabled in the common server.
SQL execution, Oracle parameter binding, timestamp/boolean conversions, uniqueness behavior, and concurrent code allocation still require tests against the eventual tables. The shared Device.Store boundary and PostgreSQL counterpart are implemented, and WC services use them. Oracle operations implement the same contract but still require live validation and selection/configuration integration. Agency=BFD remains disabled in the common server.
## Oracle connection configuration
......@@ -52,12 +52,12 @@ Use the actual credentials locally. Server is the TNS alias, not the PostgreSQL
BFD CAD and auth modules apply this section before connecting. For compatibility with the standalone server, an absent Oracle section retains their existing designer settings. A present but incomplete section fails with a configuration error. A standalone Oracle device store requires the section; a store borrowing the auth connection uses that connection's settings.
The common server now compiles the BFD CAD/auth and Oracle device implementations and includes their selection branches. ValidateClient still rejects BFD before any branch executes. Removing that block alone is not a completed integration: require explicit Oracle configuration for the common-server CAD/auth path, finish the web/auth integration, create and test the deferred device tables, and validate Oracle listener behavior on the shared WebSocket worker before enabling BFD.
The common server now compiles the BFD CAD/auth and Oracle device implementations and includes their selection branches. ValidateAgency still rejects BFD before any branch executes. Removing that block alone is not a completed integration: require explicit Oracle configuration for the common-server CAD/auth path, finish the web/auth integration, create and test the deferred device tables, and validate Oracle listener behavior on the shared WebSocket worker before enabling BFD.
No running INI was changed and no database tables were created. Both server builds passed; Oracle INI connections have not been runtime-tested.
## Latest activation checkpoint
The user manually created both tables in dbForge. The common server now accepts Client=BFD and requires explicit Oracle INI settings. Earlier descriptions of the startup block are historical. Server=BFD still uses the existing TNS alias; credentials come from the INI. Both builds passed, but Oracle query/binding/listener and device-flow checks remain pending. Use webClientVersion=9.4.0 for the current BFD web project.
The user manually created both tables in dbForge. The common server now accepts Agency=BFD and requires explicit Oracle INI settings. Earlier descriptions of the startup block are historical. Server=BFD still uses the existing TNS alias; credentials come from the INI. Both builds passed, but Oracle query/binding/listener and device-flow checks remain pending. Use webClientVersion=9.4.0 for the current BFD web project.
The earlier checkpoint text above is historical. Current schema was manually created by the user; real-number BFD browser registration and login were successful. ClearVerificationCode was removed after the send-before-save fix, leaving 24 Oracle designer queries.
[Application]
Client=WC
Agency=WC
[Settings]
LogFileNum=186
......
......@@ -11,19 +11,19 @@ uses
Api.Server.Module in 'Source\Api.Server.Module.pas' {ApiServerModule: TDataModule},
Main in 'Source\Main.pas' {FMain},
Common.Logging in 'Source\Common.Logging.pas',
BFD.Api.Database in '..\..\BFD\Server\BFD.Api.Database.pas' {BFDAPIDataModule: TDataModule},
BFD.Auth.Database in '..\..\BFD\Server\BFD.Auth.Database.pas' {BFDAuthDatabase: TDataModule},
BFD.Api.Database in '..\..\Agency Specific\BFD\Server\BFD.Api.Database.pas' {BFDAPIDataModule: TDataModule},
BFD.Auth.Database in '..\..\Agency Specific\BFD\Server\BFD.Auth.Database.pas' {BFDAuthDatabase: TDataModule},
Oracle.Device.Database in 'Source\Oracle.Device.Database.pas' {OracleDeviceDatabase: TDataModule},
Device.Store in 'Source\Device.Store.pas',
Client.Auth in 'Source\Client.Auth.pas',
PostgreSQL.Device.Database in 'Source\PostgreSQL.Device.Database.pas' {PostgreSQLDeviceDatabase: TDataModule},
Client.Data in 'Source\Client.Data.pas',
Client.Selection in 'Source\Client.Selection.pas',
WC.Api.Database in '..\..\WC\Server\WC.Api.Database.pas' {WCAPIDataModule: TDataModule},
WC.Api.Database in '..\..\Agency Specific\WC\Server\WC.Api.Database.pas' {WCAPIDataModule: TDataModule},
Common.Middleware.Logging in 'Source\Common.Middleware.Logging.pas',
Common.Config in 'Source\Common.Config.pas',
Auth.Server.Module in 'Source\Auth.Server.Module.pas' {AuthServerModule: TDataModule},
WC.Auth.Database in '..\..\WC\Server\WC.Auth.Database.pas' {WCAuthDatabase: TDataModule},
WC.Auth.Database in '..\..\Agency Specific\WC\Server\WC.Auth.Database.pas' {WCAuthDatabase: TDataModule},
uLibrary in 'Source\uLibrary.pas',
Auth.Service in 'Source\Auth.Service.pas',
Api.Service in 'Source\Api.Service.pas',
......
......@@ -142,12 +142,12 @@
<Form>FMain</Form>
</DCCReference>
<DCCReference Include="Source\Common.Logging.pas"/>
<DCCReference Include="..\..\BFD\Server\BFD.Api.Database.pas">
<DCCReference Include="..\..\Agency Specific\BFD\Server\BFD.Api.Database.pas">
<Form>BFDAPIDataModule</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="..\..\BFD\Server\BFD.Auth.Database.pas">
<DCCReference Include="..\..\Agency Specific\BFD\Server\BFD.Auth.Database.pas">
<Form>BFDAuthDatabase</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
......@@ -165,7 +165,7 @@
</DCCReference>
<DCCReference Include="Source\Client.Data.pas"/>
<DCCReference Include="Source\Client.Selection.pas"/>
<DCCReference Include="..\..\WC\Server\WC.Api.Database.pas">
<DCCReference Include="..\..\Agency Specific\WC\Server\WC.Api.Database.pas">
<Form>WCAPIDataModule</Form>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
......@@ -175,7 +175,7 @@
<Form>AuthServerModule</Form>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="..\..\WC\Server\WC.Auth.Database.pas">
<DCCReference Include="..\..\Agency Specific\WC\Server\WC.Auth.Database.pas">
<Form>WCAuthDatabase</Form>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
......
{
"AuthUrl" : "http://localhost:2001/emsys/emimobile/auth/",
"ApiUrl" : "http://localhost:2001/emsys/emimobile/api/",
"AppUrl" : "http://localhost:2001/emsys/emimobile/app/",
"WsUrl": "ws://localhost:8091/"
}
/* --- TMS WEB Core Specific Fixes --- */
/* Removes the default border from the main Form wrapper */
span.card {
border: none;
}
/* --- Login Screen Styling --- */
.login-card {
display: inline-block; /* Or use d-flex on the parent to center it */
width: 100%;
max-width: 350px; /* Better than fixed 300px */
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
/* Optional: Custom navbar look for the login screen */
.login-navbar {
max-width: 1200px;
margin: auto;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border: 1px solid #d3d3d3;
}
/* --- Table Customization --- */
/* Bootstrap has .table-hover, but this sets your specific blue color */
.table tbody tr:hover {
background-color: #d1e7fd;
cursor: pointer;
}
/* --- Pagination Theme Overrides --- */
/* These override Bootstrap's blue to your specific darker blue (#204d74) */
.pagination .page-link {
color: #204d74; /* Text color for standard links */
}
/* Active State (Selected Page) */
.pagination .page-item.active .page-link {
background-color: #204d74;
border-color: #204d74;
color: #fff !important;
}
/* Hover State */
.pagination .page-item:not(.active) .page-link:hover {
background-color: #e9ecef; /* Standard Bootstrap light grey hover */
color: #16344a; /* Darker text on hover */
}
/* --- Utilities --- */
.list-section-header:empty {
display: none;
}
#map_pnlmap > span {
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
position: absolute !important;
}
html, body {
height: 100%;
overflow: hidden;
}
/* iOS safe area: absorb status bar into top nav, home indicator into bottom nav */
@supports (padding-top: env(safe-area-inset-top)) {
#top_nav {
padding-top: calc(0.5rem + env(safe-area-inset-top));
}
#bottom_nav {
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));
}
}
@supports (-webkit-touch-callout: none) {
/* CSS specific to iOS devices */
span.card {
border: none;
/*padding-top: 2em !important;*/
}
.vh-100 {
height: 93vh !important;
}
}
.tab-hidden { display: none !important; }
.connection-status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 1.75rem;
padding: 0.2rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 999px;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.connection-status-dot {
width: 0.55rem;
height: 0.55rem;
flex: 0 0 auto;
border-radius: 50%;
background-color: #adb5bd;
}
.connection-status-connected .connection-status-dot {
background-color: #75d995;
box-shadow: 0 0 0 0.14rem rgba(117, 217, 149, 0.2);
}
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
background-color: #ffd166;
animation: connection-status-pulse 1.4s ease-in-out infinite;
}
.connection-status-offline .connection-status-dot,
.connection-status-stopped .connection-status-dot {
background-color: #ff8b94;
}
@keyframes connection-status-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
@media (max-width: 430px) {
.connection-status {
width: 1.75rem;
justify-content: center;
padding-right: 0;
padding-left: 0;
}
.connection-status-text {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
animation: none;
}
}
.summary-chevron-icon {
width: 1rem;
height: 1rem;
display: block;
transition: transform 0.15s ease-in-out;
}
/* Expanded => show DOWN (rotate base UP chevron) */
.summary-toggle[aria-expanded="true"] .summary-chevron-icon {
transform: rotate(180deg);
}
.time-vscroll {
height: 100%;
overflow: auto;
}
.time-vscroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bs-body-bg);
}
.time-vscroll thead th.th-resize {
z-index: 3;
}
.time-table {
table-layout: fixed;
}
.time-table th,
.time-table td {
overflow: hidden;
}
.time-table .dropdown,
.time-dd-toggle,
.time-dd-label,
.cell-input,
.cell-textarea {
min-width: 0;
}
.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);
}
}
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{27B04508-3F72-45A2-83EF-A007750D2923}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Projects Include="wcEmiMobile.dproj">
<Dependencies/>
</Projects>
<Projects Include="..\..\Shared\Server\emiMobileServer.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Default.Personality/>
</BorlandProject>
</ProjectExtensions>
<Target Name="wcEmiMobile">
<MSBuild Projects="wcEmiMobile.dproj"/>
</Target>
<Target Name="wcEmiMobile:Clean">
<MSBuild Projects="wcEmiMobile.dproj" Targets="Clean"/>
</Target>
<Target Name="wcEmiMobile:Make">
<MSBuild Projects="wcEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="emiMobileServer">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj"/>
</Target>
<Target Name="emiMobileServer:Clean">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/>
</Target>
<Target Name="emiMobileServer:Make">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="wcEmiMobile;emiMobileServer"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="wcEmiMobile:Clean;emiMobileServer:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="wcEmiMobile:Make;emiMobileServer:Make"/>
</Target>
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
</Project>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>emiMobile</title>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<!-- jQuery -->
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js" type="text/javascript"></script>
<!-- Icons & fonts -->
<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"/>
<!-- Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" type="text/javascript"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"/>
<!-- App -->
<script src="wcEmiMobile.js" type="text/javascript"></script>
<!-- App styles -->
<style></style>
<link href="css/app.css" rel="stylesheet"/>
</head>
<body>
<script type="text/javascript">rtl.run();</script>
<script>
function setIOSDivHeight() {
const targetDiv = document.querySelector('body'); // Replace with your div ID
if (targetDiv) {
// Get the inner height of the window
const height = window.innerHeight ;
// Apply the height in pixels
targetDiv.style.maxHeight = (height - 100) + 'px';
//alert("set height: " + (height - 100));
}
}
// 2. Attach event listeners for Load, Resize, and Scroll
// Scroll is critical for iOS to handle the address bar expanding/collapsing
const isIOSUA = /iPad|iPhone|iPod/.test(navigator.userAgent);
if(isIOSUA){
window.addEventListener('load', setIOSDivHeight);
window.addEventListener('resize', setIOSDivHeight);
window.addEventListener('scroll', setIOSDivHeight);
}
</script>
</body>
</html>
unit uMapFilters;
interface
uses
System.SysUtils, System.StrUtils,
JS, Web,
VCL.TMSFNCLeaflet, VCL.TMSFNCMaps, VCL.TMSFNCMapsCommonTypes;
type
TMapFilters = class
private
FMap: TTMSFNCLeaflet;
FShowUnits: Boolean;
FShowComplaints: Boolean;
FShowLocation: Boolean;
function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
procedure SetCheckBoxChecked(const elementId: string; checked: Boolean);
procedure SetText(const elementId: string; const value: string);
procedure ReadUi;
procedure ResetUi;
procedure UpdateSummary;
procedure ApplyToMap;
procedure HandleDocClick(e: TJSMouseEvent);
procedure HandleDocChange(e: TJSEvent);
public
constructor Create(AMap: TTMSFNCLeaflet);
procedure Init;
procedure Apply;
procedure Reset;
end;
implementation
{ TMapFilters }
constructor TMapFilters.Create(AMap: TTMSFNCLeaflet);
begin
inherited Create;
FMap := AMap;
FShowUnits := True;
FShowComplaints := True;
FShowLocation := False;
end;
procedure TMapFilters.Init;
begin
Document.addEventListener('click', @HandleDocClick);
Document.addEventListener('change', @HandleDocChange);
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Apply;
begin
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Reset;
begin
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.HandleDocClick(e: TJSMouseEvent);
var
el: TJSElement;
id: string;
begin
el := TJSElement(e.target);
if el = nil then
Exit;
// Note: This supports clicking inner icons/spans by walking up to a parent with an id
asm
while (el && !el.id) { el = el.parentElement; }
end;
if (el <> nil) and (el is TJSHtmlElement) then
begin
id := string(TJSHtmlElement(el).id);
if id = 'map_filters_apply' then
begin
e.preventDefault;
e.stopPropagation;
Apply;
Exit;
end;
if id = 'map_filters_reset' then
begin
e.preventDefault;
e.stopPropagation;
Reset;
Exit;
end;
end;
end;
procedure TMapFilters.HandleDocChange(e: TJSEvent);
begin
//Note: Summary text changes when toggles change
ReadUi;
UpdateSummary;
end;
procedure TMapFilters.ReadUi;
begin
FShowUnits := GetCheckBoxChecked('map_filter_units', True);
FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True);
FShowLocation := GetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.ResetUi;
begin
SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True);
SetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.UpdateSummary;
var
summaryText: string;
begin
summaryText := '';
if FShowUnits then
summaryText := 'Units';
if FShowComplaints then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Complaints';
end;
if FShowLocation then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Location';
end;
if summaryText = '' then
summaryText := 'None';
SetText('map_filters_summary', summaryText);
end;
procedure TMapFilters.ApplyToMap;
var
i: Integer;
m: TTMSFNCMapsMarker;
ds: string;
showMarker: Boolean;
begin
if FMap = nil then
Exit;
FMap.BeginUpdate;
try
for i := 0 to FMap.Markers.Count - 1 do
begin
m := FMap.Markers[i];
ds := Trim(string(m.DataString));
// Note: Map form sets:
// - units: m.DataString := 'unit'
// - complaints: m.DataString := 'complaint'
showMarker := True;
if SameText(ds, 'unit') or StartsText('unit|', ds) then
showMarker := FShowUnits
else if SameText(ds, 'complaint') or StartsText('complaint|', ds) then
showMarker := FShowComplaints
else if SameText(ds, 'device') then
showMarker := FShowLocation;
// Note: TTMSFNCMapsMarker supports visibility toggling
m.Visible := showMarker;
end;
finally
FMap.EndUpdate;
end;
end;
function TMapFilters.GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
var
el: TJSElement;
begin
Result := defaultValue;
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
Result := TJSHtmlInputElement(el).checked;
end;
procedure TMapFilters.SetCheckBoxChecked(const elementId: string; checked: Boolean);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
TJSHtmlInputElement(el).checked := checked;
end;
procedure TMapFilters.SetText(const elementId: string; const value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlElement) then
TJSHtmlElement(el).innerText := value;
end;
end.
program wcEmiMobile;
{$R *.dres}
uses
Vcl.Forms,
System.SysUtils,
XData.Web.Connection,
Auth.Service in 'Auth.Service.pas',
App.Types in 'App.Types.pas',
ConnectionModule in 'ConnectionModule.pas' {DMConnection: TWebDataModule},
View.Login in 'View.Login.pas' {FViewLogin: TWebForm} {*.html},
View.UserProfile in 'View.UserProfile.pas' {FViewUserProfile: TWebForm} {*.html},
View.Units in 'View.Units.pas' {FViewUnits: TWebForm} {*.html},
App.Config in 'App.Config.pas',
Paginator.Plugins in 'Paginator.Plugins.pas',
View.Complaints in 'View.Complaints.pas' {FViewComplaints: TWebForm} {*.html},
View.Main in 'View.Main.pas' {FViewMain: TWebForm} {*.html},
View.Map in 'View.Map.pas' {FViewMap: TWebForm} {*.html},
View.Admin in 'View.Admin.pas' {FViewAdmin: TWebForm} {*.html},
View.Users in 'View.Users.pas' {FViewUsers: TWebForm} {*.html},
View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html},
Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas',
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in '..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res}
procedure DisplayLoginView(AMessage: string = ''); forward;
procedure DisplayDeviceRegistrationView; forward;
procedure DisplayMainView;
procedure ConnectProc;
begin
if Assigned(FViewLogin) then
FreeAndNil(FViewLogin);
if AuthService.DeviceManagementMode then
FViewDeviceManager := TFViewDeviceManager.CreateNew
else
TFViewMain.Display(@DisplayLoginView);
end;
begin
if not DMConnection.ApiConnection.Connected then
DMConnection.ApiConnection.Open(@ConnectProc)
else
ConnectProc;
end;
procedure DisplayLoginView(AMessage: string);
begin
AuthService.Logout;
DMConnection.ApiConnection.Connected := False;
if Assigned(FViewDeviceManager) then
FreeAndNil(FViewDeviceManager);
if Assigned(FViewMain) then
FViewMain.Free;
TFViewLogin.Display(@DisplayMainView, AMessage);
end;
procedure DisplayDeviceRegistrationView;
procedure OnRegistered;
begin
// Device registered — proceed to login
if Assigned(FViewDeviceRegistration) then
FViewDeviceRegistration.Free;
TFViewLogin.Display(@DisplayMainView);
end;
begin
if Assigned(FViewDeviceRegistration) then
FViewDeviceRegistration.Free;
TFViewDeviceRegistration.Display(@OnRegistered);
end;
procedure UnauthorizedAccessProc(AMessage: string);
begin
DisplayLoginView(AMessage);
end;
procedure StartApplication;
begin
if AuthService.DeviceManagementMode then
begin
DisplayLoginView;
Exit;
end;
// Step 1: device must be registered before login is allowed
if not AuthService.IsDeviceRegistered then
begin
DisplayDeviceRegistrationView;
Exit;
end;
// Step 2: normal JWT auth check
if (not AuthService.Authenticated) or AuthService.TokenExpired then
DisplayLoginView
else
DisplayMainView;
end;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TDMConnection, DMConnection);
Application.Run;
DMConnection.InitApp(@StartApplication, @UnauthorizedAccessProc);
end.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment