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/ ...@@ -45,16 +45,16 @@ Shared/Server/Source/__history/
Shared/Server/Source/__recovery/ Shared/Server/Source/__recovery/
Shared/Web/__history/ Shared/Web/__history/
Shared/Web/__recovery/ Shared/Web/__recovery/
WC/Web/Win32/ Agency Specific/WC/Web/Win32/
WC/Web/Win64/ Agency Specific/WC/Web/Win64/
WC/Web/TMSWeb/ Agency Specific/WC/Web/TMSWeb/
WC/Web/**/__history/ Agency Specific/WC/Web/**/__history/
WC/Web/**/__recovery/ Agency Specific/WC/Web/**/__recovery/
BFD/Web/Win32/ Agency Specific/BFD/Web/Win32/
BFD/Web/Win64/ Agency Specific/BFD/Web/Win64/
BFD/Web/TMSWeb/ Agency Specific/BFD/Web/TMSWeb/
BFD/Web/**/__history/ Agency Specific/BFD/Web/**/__history/
BFD/Web/**/__recovery/ Agency Specific/BFD/Web/**/__recovery/
# Runtime libraries already versioned with the server # Runtime libraries already versioned with the server
!Shared/Server/bin/FastMM_FullDebugMode.dll !Shared/Server/bin/FastMM_FullDebugMode.dll
......
...@@ -28,8 +28,8 @@ uses ...@@ -28,8 +28,8 @@ uses
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html}, View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas', uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule}, Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in '..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html}, View.DeviceRegistration in '..\..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html}; View.DeviceManager in '..\..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res} {$R *.res}
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
<SanitizedProjectName>bfdEmiMobile</SanitizedProjectName> <SanitizedProjectName>bfdEmiMobile</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale> <VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject> <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> <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> <DCC_Define>SKIA;$(DCC_Define)</DCC_Define>
<TMSWebHTMLFile>index.html</TMSWebHTMLFile> <TMSWebHTMLFile>index.html</TMSWebHTMLFile>
...@@ -200,11 +200,11 @@ ...@@ -200,11 +200,11 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceRegistration.pas"> <DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form> <Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceManager.pas"> <DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form> <Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<Projects Include="bfdEmiMobile.dproj"> <Projects Include="bfdEmiMobile.dproj">
<Dependencies/> <Dependencies/>
</Projects> </Projects>
<Projects Include="..\..\Shared\Server\emiMobileServer.dproj"> <Projects Include="..\..\..\Shared\Server\emiMobileServer.dproj">
<Dependencies/> <Dependencies/>
</Projects> </Projects>
</ItemGroup> </ItemGroup>
...@@ -27,13 +27,13 @@ ...@@ -27,13 +27,13 @@
<MSBuild Projects="bfdEmiMobile.dproj" Targets="Make"/> <MSBuild Projects="bfdEmiMobile.dproj" Targets="Make"/>
</Target> </Target>
<Target Name="emiMobileServer"> <Target Name="emiMobileServer">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj"/> <MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj"/>
</Target> </Target>
<Target Name="emiMobileServer:Clean"> <Target Name="emiMobileServer:Clean">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/> <MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Clean"/>
</Target> </Target>
<Target Name="emiMobileServer:Make"> <Target Name="emiMobileServer:Make">
<MSBuild Projects="..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/> <MSBuild Projects="..\..\..\Shared\Server\emiMobileServer.dproj" Targets="Make"/>
</Target> </Target>
<Target Name="Build"> <Target Name="Build">
<CallTarget Targets="bfdEmiMobile;emiMobileServer"/> <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 @@ ...@@ -61,7 +61,7 @@
<SanitizedProjectName>wcEmiMobile</SanitizedProjectName> <SanitizedProjectName>wcEmiMobile</SanitizedProjectName>
<VerInfo_Locale>1046</VerInfo_Locale> <VerInfo_Locale>1046</VerInfo_Locale>
<TMSWebProject>2</TMSWebProject> <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> <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> <DCC_Define>SKIA;$(DCC_Define)</DCC_Define>
<TMSWebHTMLFile>index.html</TMSWebHTMLFile> <TMSWebHTMLFile>index.html</TMSWebHTMLFile>
...@@ -201,11 +201,11 @@ ...@@ -201,11 +201,11 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceRegistration.pas"> <DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form> <Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\Shared\Web\View.DeviceManager.pas"> <DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form> <Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
......
...@@ -6,7 +6,7 @@ uses ...@@ -6,7 +6,7 @@ uses
Client.Data, Client.Auth, Device.Store, Uni; Client.Data, Client.Auth, Device.Store, Uni;
function CreateClientAuth: TClientAuth; function CreateClientAuth: TClientAuth;
procedure ValidateClient; procedure ValidateAgency;
function CreateDeviceStore(AConnection: TUniConnection = nil): TDeviceStore; function CreateDeviceStore(AConnection: TUniConnection = nil): TDeviceStore;
function CreateClientData: TClientData; function CreateClientData: TClientData;
...@@ -18,23 +18,23 @@ uses ...@@ -18,23 +18,23 @@ uses
function CreateClientAuth: TClientAuth; function CreateClientAuth: TClientAuth;
begin begin
if IniEntries.Client = 'BFD' then if IniEntries.Agency = 'BFD' then
Result := TBFDAuthDatabase.Create(nil) Result := TBFDAuthDatabase.Create(nil)
else else
Result := TWCAuthDatabase.Create(nil); Result := TWCAuthDatabase.Create(nil);
end; end;
procedure ValidateClient; procedure ValidateAgency;
begin begin
if IniEntries.Client = '' then if IniEntries.Agency = '' then
raise Exception.Create('Missing [Application] Client in the server INI file. Set Client=WC or Client=BFD.'); raise Exception.Create('Missing [Application] Agency in the server INI file. Set Agency=WC or Agency=BFD.');
if (IniEntries.Client <> 'WC') and (IniEntries.Client <> 'BFD') then if (IniEntries.Agency <> 'WC') and (IniEntries.Agency <> 'BFD') then
raise Exception.CreateFmt('Unrecognized [Application] Client "%s". Supported clients: WC, BFD.', [IniEntries.Client]); raise Exception.CreateFmt('Unrecognized [Application] Agency "%s". Supported agencies: WC, BFD.', [IniEntries.Agency]);
end; end;
function CreateClientData: TClientData; function CreateClientData: TClientData;
begin begin
if IniEntries.Client = 'BFD' then if IniEntries.Agency = 'BFD' then
Result := TBFDAPIDataModule.Create(nil) Result := TBFDAPIDataModule.Create(nil)
else else
Result := TWCAPIDataModule.Create(nil); Result := TWCAPIDataModule.Create(nil);
...@@ -42,7 +42,7 @@ end; ...@@ -42,7 +42,7 @@ end;
function CreateDeviceStore(AConnection: TUniConnection): TDeviceStore; function CreateDeviceStore(AConnection: TUniConnection): TDeviceStore;
begin begin
if IniEntries.Client = 'BFD' then if IniEntries.Agency = 'BFD' then
Result := TOracleDeviceDatabase.Create(nil) Result := TOracleDeviceDatabase.Create(nil)
else else
Result := TPostgreSQLDeviceDatabase.Create(nil); Result := TPostgreSQLDeviceDatabase.Create(nil);
......
...@@ -65,12 +65,12 @@ var ...@@ -65,12 +65,12 @@ var
jsonObj: TJSONObject; jsonObj: TJSONObject;
begin begin
Logger.Log(1, '--LoadServerConfig - start'); Logger.Log(1, '--LoadServerConfig - start');
ValidateClient; ValidateAgency;
IniEntries.ValidateDatabaseSettings; IniEntries.ValidateDatabaseSettings;
serverConfig.FWebAppFolder := TPath.Combine(TPath.Combine(ExtractFilePath(ParamStr(0)), 'static'), IniEntries.Client); serverConfig.FWebAppFolder := TPath.Combine(TPath.Combine(ExtractFilePath(ParamStr(0)), 'static'), IniEntries.Agency);
Logger.Log(1, '-- Client: ' + IniEntries.Client + ' [from ini]'); Logger.Log(1, '-- Agency: ' + IniEntries.Agency + ' [from ini]');
Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + ' [derived from Client]'); Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + ' [derived from Agency]');
serverConfig.twilioAccountSid := IniEntries.TwilioAccountSid; serverConfig.twilioAccountSid := IniEntries.TwilioAccountSid;
serverConfig.twilioAuthToken := IniEntries.TwilioAuthToken; serverConfig.twilioAuthToken := IniEntries.TwilioAuthToken;
......
...@@ -8,7 +8,7 @@ uses ...@@ -8,7 +8,7 @@ uses
type type
TIniEntries = class TIniEntries = class
private private
FClient: string; FAgency: string;
// [Settings] // [Settings]
FMemoLogLevel: Integer; FMemoLogLevel: Integer;
FMemoLogLevelFromIni: Boolean; FMemoLogLevelFromIni: Boolean;
...@@ -43,7 +43,7 @@ type ...@@ -43,7 +43,7 @@ type
constructor Create; constructor Create;
procedure ValidateDatabaseSettings; procedure ValidateDatabaseSettings;
procedure ConfigureConnection(Connection: TUniConnection); procedure ConfigureConnection(Connection: TUniConnection);
property Client: string read FClient; property Agency: string read FAgency;
// [Settings] // [Settings]
property MemoLogLevel: Integer read FMemoLogLevel; property MemoLogLevel: Integer read FMemoLogLevel;
...@@ -100,7 +100,7 @@ var ...@@ -100,7 +100,7 @@ var
begin begin
iniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini')); iniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try try
FClient := UpperCase(Trim(iniFile.ReadString('Application', 'Client', ''))); FAgency := UpperCase(Trim(iniFile.ReadString('Application', 'Agency', '')));
// [Settings] // [Settings]
FMemoLogLevel := iniFile.ReadInteger('Settings', 'MemoLogLevel', 3); FMemoLogLevel := iniFile.ReadInteger('Settings', 'MemoLogLevel', 3);
...@@ -143,7 +143,7 @@ begin ...@@ -143,7 +143,7 @@ begin
// Note: Oracle uses a TNS alias; PostgreSQL uses host, port, and database. // Note: Oracle uses a TNS alias; PostgreSQL uses host, port, and database.
if FClient = 'BFD' then if FAgency = 'BFD' then
begin begin
FDatabaseServer := Trim(iniFile.ReadString('Oracle', 'Server', '')); FDatabaseServer := Trim(iniFile.ReadString('Oracle', 'Server', ''));
FDatabaseUsername := Trim(iniFile.ReadString('Oracle', 'Username', '')); FDatabaseUsername := Trim(iniFile.ReadString('Oracle', 'Username', ''));
...@@ -164,14 +164,14 @@ end; ...@@ -164,14 +164,14 @@ end;
procedure TIniEntries.ValidateDatabaseSettings; procedure TIniEntries.ValidateDatabaseSettings;
begin begin
if (FClient = 'BFD') and if (FAgency = 'BFD') and
((FDatabaseServer = '') or (FDatabaseUsername = '') or (FDatabasePassword = '')) then ((FDatabaseServer = '') or (FDatabaseUsername = '') or (FDatabasePassword = '')) then
raise Exception.Create('Set Server, Username, and Password in the server INI [Oracle] section.'); raise Exception.Create('Set Server, Username, and Password in the server INI [Oracle] section.');
end; end;
procedure TIniEntries.ConfigureConnection(Connection: TUniConnection); procedure TIniEntries.ConfigureConnection(Connection: TUniConnection);
begin begin
if FClient = 'BFD' then if FAgency = 'BFD' then
Connection.ProviderName := 'Oracle' Connection.ProviderName := 'Oracle'
else else
begin begin
......
...@@ -190,7 +190,7 @@ begin ...@@ -190,7 +190,7 @@ begin
Logger.Log(1, LogValue('--Settings->LogFileNum', IniEntries.LogFileNum.ToString, IniEntries.LogFileNumFromIni)); Logger.Log(1, LogValue('--Settings->LogFileNum', IniEntries.LogFileNum.ToString, IniEntries.LogFileNumFromIni));
Logger.Log(1, LogValue('--Settings->webClientVersion', IniEntries.WebClientVersion, IniEntries.WebClientVersionFromIni)); 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 ---') Logger.Log(1, '--- Oracle database: using [Oracle] INI settings ---')
else else
begin begin
......
# Oracle device storage # 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. 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 ...@@ -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. 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 ## Oracle connection configuration
...@@ -52,12 +52,12 @@ Use the actual credentials locally. Server is the TNS alias, not the PostgreSQL ...@@ -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. 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. 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 ## 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. 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] [Application]
Client=WC Agency=WC
[Settings] [Settings]
LogFileNum=186 LogFileNum=186
......
...@@ -11,19 +11,19 @@ uses ...@@ -11,19 +11,19 @@ uses
Api.Server.Module in 'Source\Api.Server.Module.pas' {ApiServerModule: TDataModule}, Api.Server.Module in 'Source\Api.Server.Module.pas' {ApiServerModule: TDataModule},
Main in 'Source\Main.pas' {FMain}, Main in 'Source\Main.pas' {FMain},
Common.Logging in 'Source\Common.Logging.pas', Common.Logging in 'Source\Common.Logging.pas',
BFD.Api.Database in '..\..\BFD\Server\BFD.Api.Database.pas' {BFDAPIDataModule: TDataModule}, BFD.Api.Database in '..\..\Agency Specific\BFD\Server\BFD.Api.Database.pas' {BFDAPIDataModule: TDataModule},
BFD.Auth.Database in '..\..\BFD\Server\BFD.Auth.Database.pas' {BFDAuthDatabase: 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}, Oracle.Device.Database in 'Source\Oracle.Device.Database.pas' {OracleDeviceDatabase: TDataModule},
Device.Store in 'Source\Device.Store.pas', Device.Store in 'Source\Device.Store.pas',
Client.Auth in 'Source\Client.Auth.pas', Client.Auth in 'Source\Client.Auth.pas',
PostgreSQL.Device.Database in 'Source\PostgreSQL.Device.Database.pas' {PostgreSQLDeviceDatabase: TDataModule}, PostgreSQL.Device.Database in 'Source\PostgreSQL.Device.Database.pas' {PostgreSQLDeviceDatabase: TDataModule},
Client.Data in 'Source\Client.Data.pas', Client.Data in 'Source\Client.Data.pas',
Client.Selection in 'Source\Client.Selection.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.Middleware.Logging in 'Source\Common.Middleware.Logging.pas',
Common.Config in 'Source\Common.Config.pas', Common.Config in 'Source\Common.Config.pas',
Auth.Server.Module in 'Source\Auth.Server.Module.pas' {AuthServerModule: TDataModule}, 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', uLibrary in 'Source\uLibrary.pas',
Auth.Service in 'Source\Auth.Service.pas', Auth.Service in 'Source\Auth.Service.pas',
Api.Service in 'Source\Api.Service.pas', Api.Service in 'Source\Api.Service.pas',
......
...@@ -142,12 +142,12 @@ ...@@ -142,12 +142,12 @@
<Form>FMain</Form> <Form>FMain</Form>
</DCCReference> </DCCReference>
<DCCReference Include="Source\Common.Logging.pas"/> <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> <Form>BFDAPIDataModule</Form>
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\BFD\Server\BFD.Auth.Database.pas"> <DCCReference Include="..\..\Agency Specific\BFD\Server\BFD.Auth.Database.pas">
<Form>BFDAuthDatabase</Form> <Form>BFDAuthDatabase</Form>
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
...@@ -165,7 +165,7 @@ ...@@ -165,7 +165,7 @@
</DCCReference> </DCCReference>
<DCCReference Include="Source\Client.Data.pas"/> <DCCReference Include="Source\Client.Data.pas"/>
<DCCReference Include="Source\Client.Selection.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> <Form>WCAPIDataModule</Form>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
<Form>AuthServerModule</Form> <Form>AuthServerModule</Form>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="..\..\WC\Server\WC.Auth.Database.pas"> <DCCReference Include="..\..\Agency Specific\WC\Server\WC.Auth.Database.pas">
<Form>WCAuthDatabase</Form> <Form>WCAuthDatabase</Form>
<DesignClass>TDataModule</DesignClass> <DesignClass>TDataModule</DesignClass>
</DCCReference> </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