Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
emiMobile
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mac Stephens
emiMobile
Commits
ee79d138
Commit
ee79d138
authored
Jul 24, 2026
by
Michael Brachmann
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
websocket progress
parent
7481bbad
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
858 additions
and
157 deletions
+858
-157
Ws.DataModel.pas
emiMobileServer/Source/Ws.DataModel.pas
+476
-0
Ws.Server.Module.pas
emiMobileServer/Source/Ws.Server.Module.pas
+15
-1
WsMessages.pas
emiMobileServer/Source/shared/WsMessages.pas
+75
-0
Module.Websocket.pas
webEMIMobile/Module.Websocket.pas
+72
-6
View.Complaints.pas
webEMIMobile/View.Complaints.pas
+18
-0
View.Main.pas
webEMIMobile/View.Main.pas
+60
-1
View.Map.pas
webEMIMobile/View.Map.pas
+127
-149
View.Units.pas
webEMIMobile/View.Units.pas
+15
-0
No files found.
emiMobileServer/Source/Ws.DataModel.pas
0 → 100644
View file @
ee79d138
unit
Ws
.
DataModel
;
// Server-side WebSocket data model.
// Owns a VCL timer that fires every FIntervalMs milliseconds, queries the
// database for the five data sets used by the polling timers that existed in
// each connected browser client, and broadcasts the results to every
// handshaked WebSocket connection via the supplied broadcast callback.
//
// All five result shapes mirror what the XData API methods return so that the
// client-side parsing code can be shared between the initial HTTP load and the
// subsequent WebSocket pushes.
interface
uses
System
.
SysUtils
,
System
.
Classes
,
System
.
JSON
,
Vcl
.
ExtCtrls
,
Api
.
Database
,
WsMessages
,
Common
.
Logging
;
type
TWsBroadcastProc
=
reference
to
procedure
(
const
AMessage
:
string
);
TWsDataModel
=
class
private
FDb
:
TApiDatabaseModule
;
FTimer
:
TTimer
;
FBroadcast
:
TWsBroadcastProc
;
procedure
TimerFire
(
Sender
:
TObject
);
procedure
BroadcastAll
;
function
BuildBadgeCountsJson
:
string
;
function
BuildUnitMapJson
:
string
;
function
BuildComplaintMapJson
:
string
;
function
BuildUnitListJson
:
string
;
function
BuildComplaintListJson
:
string
;
public
constructor
Create
(
ABroadcast
:
TWsBroadcastProc
;
AIntervalMs
:
Integer
=
30000
);
destructor
Destroy
;
override
;
end
;
implementation
uses
System
.
StrUtils
,
System
.
DateUtils
;
{ TWsDataModel }
constructor
TWsDataModel
.
Create
(
ABroadcast
:
TWsBroadcastProc
;
AIntervalMs
:
Integer
);
begin
inherited
Create
;
FBroadcast
:=
ABroadcast
;
FDb
:=
TApiDatabaseModule
.
Create
(
nil
);
FTimer
:=
TTimer
.
Create
(
nil
);
FTimer
.
Interval
:=
AIntervalMs
;
FTimer
.
OnTimer
:=
TimerFire
;
FTimer
.
Enabled
:=
True
;
end
;
destructor
TWsDataModel
.
Destroy
;
begin
FTimer
.
Enabled
:=
False
;
FTimer
.
Free
;
FDb
.
Free
;
inherited
;
end
;
procedure
TWsDataModel
.
TimerFire
(
Sender
:
TObject
);
begin
BroadcastAll
;
end
;
procedure
TWsDataModel
.
BroadcastAll
;
begin
Logger
.
Log
(
3
,
'WsDataModel: broadcasting all'
);
try
FBroadcast
(
BuildBadgeCountsJson
);
except
on
E
:
Exception
do
Logger
.
Log
(
2
,
'WsDataModel BADGE_COUNTS error: '
+
E
.
Message
);
end
;
try
FBroadcast
(
BuildUnitMapJson
);
except
on
E
:
Exception
do
Logger
.
Log
(
2
,
'WsDataModel UNIT_MAP error: '
+
E
.
Message
);
end
;
try
FBroadcast
(
BuildComplaintMapJson
);
except
on
E
:
Exception
do
Logger
.
Log
(
2
,
'WsDataModel COMPLAINT_MAP error: '
+
E
.
Message
);
end
;
try
FBroadcast
(
BuildUnitListJson
);
except
on
E
:
Exception
do
Logger
.
Log
(
2
,
'WsDataModel UNIT_LIST error: '
+
E
.
Message
);
end
;
try
FBroadcast
(
BuildComplaintListJson
);
except
on
E
:
Exception
do
Logger
.
Log
(
2
,
'WsDataModel COMPLAINT_LIST error: '
+
E
.
Message
);
end
;
end
;
function
TWsDataModel
.
BuildBadgeCountsJson
:
string
;
var
msg
:
TWsBadgeCountsMessage
;
begin
msg
:=
TWsBadgeCountsMessage
.
Create
;
try
with
FDb
.
uqBadgeCounts
do
begin
Open
;
try
msg
.
BadgeComplaints
:=
FieldByName
(
'COMPLAINTS'
).
AsInteger
;
msg
.
BadgeUnits
:=
FieldByName
(
'UNITS'
).
AsInteger
;
finally
Close
;
end
;
end
;
Result
:=
msg
.
AsJson
;
finally
msg
.
Free
;
end
;
end
;
function
TWsDataModel
.
BuildUnitMapJson
:
string
;
var
obj
:
TJSONObject
;
data
:
TJSONArray
;
item
:
TJSONObject
;
unitStatus
,
updateTimeText
:
string
;
begin
obj
:=
TJSONObject
.
Create
;
data
:=
TJSONArray
.
Create
;
try
obj
.
AddPair
(
'RequestId'
,
WS_MSG_UNIT_MAP
);
obj
.
AddPair
(
'data'
,
data
);
with
FDb
.
uqMapUnits
do
begin
Close
;
Open
;
try
First
;
while
not
Eof
do
begin
if
(
not
FDb
.
uqMapUnitsGPS_LATITUDE
.
IsNull
)
and
(
not
FDb
.
uqMapUnitsGPS_LONGITUDE
.
IsNull
)
then
begin
item
:=
TJSONObject
.
Create
;
data
.
AddElement
(
item
);
item
.
AddPair
(
'UnitId'
,
FDb
.
uqMapUnitsUNITID
.
AsString
);
item
.
AddPair
(
'UnitName'
,
FDb
.
uqMapUnitsUNITNAME
.
AsString
);
item
.
AddPair
(
'UnitBadge'
,
FDb
.
uqMapUnitsUNITNAME
.
AsString
);
item
.
AddPair
(
'Agency'
,
FDb
.
uqMapUnitsAGENCY
.
AsString
);
item
.
AddPair
(
'AgencyName'
,
FDb
.
uqMapUnitsAGENCY_NAME
.
AsString
);
item
.
AddPair
(
'AgencyType'
,
FDb
.
uqMapUnitsAGENCYTYPE
.
AsString
);
item
.
AddPair
(
'Lat'
,
TJSONNumber
.
Create
(
FDb
.
uqMapUnitsGPS_LATITUDE
.
AsFloat
));
item
.
AddPair
(
'Lng'
,
TJSONNumber
.
Create
(
FDb
.
uqMapUnitsGPS_LONGITUDE
.
AsFloat
));
item
.
AddPair
(
'CallType'
,
FDb
.
uqMapUnitsCALL_TYPE
.
AsString
);
item
.
AddPair
(
'Priority'
,
FDb
.
uqMapUnitsPRIORITY
.
AsString
);
unitStatus
:=
FDb
.
uqMapUnitsUNIT_STATUS_DESC
.
AsString
;
if
Trim
(
unitStatus
)
=
''
then
unitStatus
:=
'Available'
;
item
.
AddPair
(
'Status'
,
unitStatus
);
updateTimeText
:=
''
;
if
not
FDb
.
uqMapUnitsUPDATE_TIME
.
IsNull
then
updateTimeText
:=
FormatDateTime
(
'yyyy-mm-dd hh:nn:ss'
,
FDb
.
uqMapUnitsUPDATE_TIME
.
AsDateTime
);
item
.
AddPair
(
'UpdateTime'
,
updateTimeText
);
item
.
AddPair
(
'Officer1Lname'
,
FDb
.
uqMapUnitsOFFICER1_LNAME
.
AsString
);
item
.
AddPair
(
'Officer1Fname'
,
FDb
.
uqMapUnitsOFFICER1_FNAME
.
AsString
);
item
.
AddPair
(
'Officer1Empnum'
,
FDb
.
uqMapUnitsOFFICER1_EMPNUM
.
AsString
);
item
.
AddPair
(
'Officer2Lname'
,
FDb
.
uqMapUnitsOFFICER2_LNAME
.
AsString
);
item
.
AddPair
(
'Officer2Fname'
,
FDb
.
uqMapUnitsOFFICER2_FNAME
.
AsString
);
item
.
AddPair
(
'Officer2Empnum'
,
FDb
.
uqMapUnitsOFFICER2_EMPNUM
.
AsString
);
item
.
AddPair
(
'CanShowDetails'
,
TJSONBool
.
Create
(
not
FDb
.
uqMapUnitsDIS_UNITID
.
IsNull
));
end
;
Next
;
end
;
finally
Close
;
end
;
end
;
obj
.
AddPair
(
'count'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'returned'
,
TJSONNumber
.
Create
(
data
.
Count
));
Result
:=
obj
.
ToJSON
;
finally
obj
.
Free
;
end
;
end
;
function
TWsDataModel
.
BuildComplaintMapJson
:
string
;
var
obj
:
TJSONObject
;
data
:
TJSONArray
;
item
,
unitObj
:
TJSONObject
;
unitArray
:
TJSONArray
;
UnitsByComplaintMap
:
TDictionary
<
string
,
TJSONArray
>;
complaintId
,
unitStatus
:
string
;
latestUpdate
:
TDateTime
;
begin
obj
:=
TJSONObject
.
Create
;
data
:=
TJSONArray
.
Create
;
UnitsByComplaintMap
:=
TDictionary
<
string
,
TJSONArray
>.
Create
;
try
obj
.
AddPair
(
'RequestId'
,
WS_MSG_COMPLAINT_MAP
);
obj
.
AddPair
(
'data'
,
data
);
// Build unit→complaint map first
FDb
.
uqMapComplaintUnitsList
.
Close
;
FDb
.
uqMapComplaintUnitsList
.
Open
;
try
while
not
FDb
.
uqMapComplaintUnitsList
.
Eof
do
begin
complaintId
:=
FDb
.
uqMapComplaintUnitsListCOMPLAINTID
.
AsString
;
if
not
UnitsByComplaintMap
.
TryGetValue
(
complaintId
,
unitArray
)
then
begin
unitArray
:=
TJSONArray
.
Create
;
UnitsByComplaintMap
.
Add
(
complaintId
,
unitArray
);
end
;
unitStatus
:=
'Dispatched'
;
if
not
FDb
.
uqMapComplaintUnitsListDATECLEARED
.
IsNull
then
unitStatus
:=
'Cleared'
else
if
not
FDb
.
uqMapComplaintUnitsListDATEARRIVED
.
IsNull
then
unitStatus
:=
'On Scene'
else
if
not
FDb
.
uqMapComplaintUnitsListDATERESPONDED
.
IsNull
then
unitStatus
:=
'Enroute'
;
latestUpdate
:=
0
;
if
not
FDb
.
uqMapComplaintUnitsListDATEDISPATCHED
.
IsNull
then
latestUpdate
:=
FDb
.
uqMapComplaintUnitsListDATEDISPATCHED
.
AsDateTime
;
if
(
not
FDb
.
uqMapComplaintUnitsListDATERESPONDED
.
IsNull
)
and
(
FDb
.
uqMapComplaintUnitsListDATERESPONDED
.
AsDateTime
>
latestUpdate
)
then
latestUpdate
:=
FDb
.
uqMapComplaintUnitsListDATERESPONDED
.
AsDateTime
;
if
(
not
FDb
.
uqMapComplaintUnitsListDATEARRIVED
.
IsNull
)
and
(
FDb
.
uqMapComplaintUnitsListDATEARRIVED
.
AsDateTime
>
latestUpdate
)
then
latestUpdate
:=
FDb
.
uqMapComplaintUnitsListDATEARRIVED
.
AsDateTime
;
if
(
not
FDb
.
uqMapComplaintUnitsListDATECLEARED
.
IsNull
)
and
(
FDb
.
uqMapComplaintUnitsListDATECLEARED
.
AsDateTime
>
latestUpdate
)
then
latestUpdate
:=
FDb
.
uqMapComplaintUnitsListDATECLEARED
.
AsDateTime
;
unitObj
:=
TJSONObject
.
Create
;
unitArray
.
AddElement
(
unitObj
);
unitObj
.
AddPair
(
'Unit'
,
FDb
.
uqMapComplaintUnitsListUNITNAME
.
AsString
);
unitObj
.
AddPair
(
'Status'
,
unitStatus
);
unitObj
.
AddPair
(
'Updated'
,
IfThen
(
latestUpdate
<>
0
,
FormatDateTime
(
'yyyy-mm-dd hh:nn:ss'
,
latestUpdate
),
''
));
FDb
.
uqMapComplaintUnitsList
.
Next
;
end
;
finally
FDb
.
uqMapComplaintUnitsList
.
Close
;
end
;
// Build complaint rows
FDb
.
uqMapComplaints
.
Close
;
FDb
.
uqMapComplaints
.
Open
;
try
while
not
FDb
.
uqMapComplaints
.
Eof
do
begin
item
:=
TJSONObject
.
Create
;
data
.
AddElement
(
item
);
complaintId
:=
FDb
.
uqMapComplaintsCOMPLAINTID
.
AsString
;
item
.
AddPair
(
'ComplaintId'
,
complaintId
);
item
.
AddPair
(
'DispatchDistrict'
,
FDb
.
uqMapComplaintsDISPATCHDISTRICT
.
AsString
);
item
.
AddPair
(
'Agency'
,
FDb
.
uqMapComplaintsAGENCY
.
AsString
);
item
.
AddPair
(
'AgencyName'
,
FDb
.
uqMapComplaintsAGENCY_NAME
.
AsString
);
item
.
AddPair
(
'DispatchCodeDesc'
,
FDb
.
uqMapComplaintsDISPATCH_CODE_DESC
.
AsString
);
item
.
AddPair
(
'DispatchCodeCategory'
,
FDb
.
uqMapComplaintsDISPATCHCODECATEGORY
.
AsString
);
item
.
AddPair
(
'Priority'
,
FDb
.
uqMapComplaintsPRIORITY
.
AsString
);
item
.
AddPair
(
'PriorityBadge'
,
FDb
.
uqMapComplaintspriorityBadge
.
AsString
);
item
.
AddPair
(
'ComplaintStatusKey'
,
FDb
.
uqMapComplaintscomplaintStatusKey
.
AsString
);
item
.
AddPair
(
'pngName'
,
FDb
.
uqMapComplaintspngName
.
AsString
);
item
.
AddPair
(
'Address'
,
FDb
.
uqMapComplaintsADDRESS
.
AsString
);
item
.
AddPair
(
'Business'
,
FDb
.
uqMapComplaintsBUSINESS
.
AsString
);
item
.
AddPair
(
'Lat'
,
TJSONNumber
.
Create
(
FDb
.
uqMapComplaintsLAT
.
AsFloat
));
item
.
AddPair
(
'Lng'
,
TJSONNumber
.
Create
(
FDb
.
uqMapComplaintsLNG
.
AsFloat
));
if
UnitsByComplaintMap
.
TryGetValue
(
complaintId
,
unitArray
)
then
item
.
AddPair
(
'Units'
,
TJSONArray
(
unitArray
.
Clone
))
else
item
.
AddPair
(
'Units'
,
TJSONArray
.
Create
);
FDb
.
uqMapComplaints
.
Next
;
end
;
finally
FDb
.
uqMapComplaints
.
Close
;
end
;
obj
.
AddPair
(
'count'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'returned'
,
TJSONNumber
.
Create
(
data
.
Count
));
Result
:=
obj
.
ToJSON
;
finally
// Free the per-complaint unit arrays (not cloned copies — those are now owned by item)
for
unitArray
in
UnitsByComplaintMap
.
Values
do
unitArray
.
Free
;
UnitsByComplaintMap
.
Free
;
obj
.
Free
;
end
;
end
;
function
TWsDataModel
.
BuildUnitListJson
:
string
;
var
obj
:
TJSONObject
;
data
:
TJSONArray
;
item
:
TJSONObject
;
lastAgency
,
curAgency
:
string
;
o1
,
f1
,
m1
,
o2
,
f2
,
m2
:
string
;
statusDesc
,
complaintNumber
:
string
;
mapLat
,
mapLng
:
Double
;
canShowMap
:
Boolean
;
begin
obj
:=
TJSONObject
.
Create
;
data
:=
TJSONArray
.
Create
;
lastAgency
:=
''
;
try
obj
.
AddPair
(
'RequestId'
,
WS_MSG_UNIT_LIST
);
with
FDb
.
uqUnitList
do
begin
Open
;
First
;
while
not
Eof
do
begin
item
:=
TJSONObject
.
Create
;
curAgency
:=
Trim
(
FDb
.
uqUnitListAGENCY_NAME
.
AsString
);
if
curAgency
=
''
then
curAgency
:=
Trim
(
FDb
.
uqUnitListAGENCY
.
AsString
);
if
curAgency
=
''
then
curAgency
:=
'Unknown Agency'
;
if
not
SameText
(
curAgency
,
lastAgency
)
then
begin
item
.
AddPair
(
'AgencyHeader'
,
curAgency
);
lastAgency
:=
curAgency
;
end
else
item
.
AddPair
(
'AgencyHeader'
,
''
);
item
.
AddPair
(
'UnitId'
,
FDb
.
uqUnitListUNITID
.
AsString
);
item
.
AddPair
(
'UnitName'
,
FDb
.
uqUnitListUNITNAME
.
AsString
);
item
.
AddPair
(
'Agency'
,
FDb
.
uqUnitListAGENCY
.
AsString
);
item
.
AddPair
(
'AgencyName'
,
FDb
.
uqUnitListAGENCY_NAME
.
AsString
);
item
.
AddPair
(
'AgencyType'
,
FDb
.
uqUnitListAGENCYTYPE
.
AsString
);
item
.
AddPair
(
'CarNumberDesc'
,
FDb
.
uqUnitListCARNUMBER_DESC
.
AsString
);
item
.
AddPair
(
'District'
,
FDb
.
uqUnitListDISTRICT_DESC
.
AsString
);
item
.
AddPair
(
'Sector'
,
FDb
.
uqUnitListSECTOR_DESC
.
AsString
);
item
.
AddPair
(
'CallType'
,
FDb
.
uqUnitListCALL_TYPE
.
AsString
);
canShowMap
:=
False
;
if
(
not
FDb
.
uqUnitListGPS_LATITUDE
.
IsNull
)
and
(
not
FDb
.
uqUnitListGPS_LONGITUDE
.
IsNull
)
then
begin
mapLat
:=
FDb
.
uqUnitListGPS_LATITUDE
.
AsFloat
;
mapLng
:=
FDb
.
uqUnitListGPS_LONGITUDE
.
AsFloat
;
canShowMap
:=
((
mapLat
<>
0
)
or
(
mapLng
<>
0
))
and
(
Abs
(
mapLat
)
<=
90
)
and
(
Abs
(
mapLng
)
<=
180
);
end
;
item
.
AddPair
(
'CanShowMap'
,
IfThen
(
canShowMap
,
'true'
,
'false'
));
item
.
AddPair
(
'MapButtonClass'
,
IfThen
(
canShowMap
,
'btn-primary'
,
'btn-secondary disabled'
));
item
.
AddPair
(
'MapButtonDisabled'
,
IfThen
(
canShowMap
,
''
,
'disabled="disabled" aria-disabled="true"'
));
item
.
AddPair
(
'MapButtonTitle'
,
IfThen
(
canShowMap
,
'Show on map'
,
'No map location available'
));
complaintNumber
:=
Trim
(
FDb
.
uqUnitListCOMPLAINT
.
AsString
);
item
.
AddPair
(
'Location'
,
FDb
.
uqUnitListLOCATION
.
AsString
);
item
.
AddPair
(
'Complaint'
,
complaintNumber
);
item
.
AddPair
(
'ComplaintHeader'
,
IfThen
(
complaintNumber
<>
''
,
' - '
+
complaintNumber
,
''
));
statusDesc
:=
FDb
.
uqUnitListUNIT_STATUS_DESC
.
AsString
;
if
statusDesc
=
''
then
statusDesc
:=
'Available'
;
item
.
AddPair
(
'Status'
,
statusDesc
);
o1
:=
Trim
(
FDb
.
uqUnitListOFFICER1_LAST_NAME
.
AsString
);
f1
:=
Trim
(
FDb
.
uqUnitListOFFICER1_FIRST_NAME
.
AsString
);
m1
:=
Trim
(
FDb
.
uqUnitListOFFICER1_MI
.
AsString
);
if
o1
<>
''
then
begin
if
f1
<>
''
then
o1
:=
o1
+
', '
+
f1
;
if
m1
<>
''
then
o1
:=
o1
+
' '
+
m1
;
item
.
AddPair
(
'Officer1'
,
o1
);
end
;
o2
:=
Trim
(
FDb
.
uqUnitListOFFICER2_LAST_NAME
.
AsString
);
f2
:=
Trim
(
FDb
.
uqUnitListOFFICER2_FIRST_NAME
.
AsString
);
m2
:=
Trim
(
FDb
.
uqUnitListOFFICER2_MI
.
AsString
);
if
o2
<>
''
then
begin
if
f2
<>
''
then
o2
:=
o2
+
', '
+
f2
;
if
m2
<>
''
then
o2
:=
o2
+
' '
+
m2
;
item
.
AddPair
(
'Officer2'
,
o2
);
end
;
data
.
AddElement
(
item
);
Next
;
end
;
end
;
obj
.
AddPair
(
'count'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'returned'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'data'
,
data
);
Result
:=
obj
.
ToJSON
;
finally
FDb
.
uqUnitList
.
Close
;
obj
.
Free
;
end
;
end
;
function
TWsDataModel
.
BuildComplaintListJson
:
string
;
var
obj
:
TJSONObject
;
data
:
TJSONArray
;
item
:
TJSONObject
;
lastDistrict
,
curAgency
:
string
;
status
,
statusColor
,
statusTextColor
:
string
;
colorVal
:
Integer
;
complaintNumber
:
string
;
begin
obj
:=
TJSONObject
.
Create
;
data
:=
TJSONArray
.
Create
;
lastDistrict
:=
''
;
try
obj
.
AddPair
(
'RequestId'
,
WS_MSG_COMPLAINT_LIST
);
with
FDb
.
uqComplaintList
do
begin
Open
;
(
FieldByName
(
'DATEREPORTED'
)
as
TDateTimeField
).
DisplayFormat
:=
'yyyy-mm-dd hh:nn:ss'
;
First
;
while
not
Eof
do
begin
if
not
FieldByName
(
'DATEARRIVED'
).
IsNull
then
begin
status
:=
'On Scene'
;
statusColor
:=
'#22C55E'
;
statusTextColor
:=
'#000000'
;
end
else
if
not
FieldByName
(
'DATERESPONDED'
).
IsNull
then
begin
status
:=
'Enroute'
;
statusColor
:=
'#FFFF00'
;
statusTextColor
:=
'#000000'
;
end
else
if
not
FieldByName
(
'DATEDISPATCHED'
).
IsNull
then
begin
status
:=
'Dispatched'
;
statusColor
:=
'#FFFF00'
;
statusTextColor
:=
'#000000'
;
end
else
begin
status
:=
'Pending'
;
statusColor
:=
'#FF8080'
;
statusTextColor
:=
'#000000'
;
end
;
item
:=
TJSONObject
.
Create
;
curAgency
:=
Trim
(
FDb
.
uqComplaintListAGENCY
.
AsString
);
if
curAgency
=
''
then
curAgency
:=
'Unknown Agency'
;
if
not
SameText
(
curAgency
,
lastDistrict
)
then
item
.
AddPair
(
'DistrictHeader'
,
curAgency
);
lastDistrict
:=
curAgency
;
item
.
AddPair
(
'AgencyLine'
,
'Agency: '
+
curAgency
);
colorVal
:=
FDb
.
uqComplaintListPRIORITY_COLOR
.
AsInteger
;
item
.
AddPair
(
'PriorityColor'
,
'#'
+
IntToHex
(
colorVal
and
$
FFFFFF
,
6
));
item
.
AddPair
(
'PriorityTextColor'
,
'#000000'
);
complaintNumber
:=
FDb
.
uqComplaintListcomplaintNumber
.
AsString
;
item
.
AddPair
(
'ComplaintId'
,
FDb
.
uqComplaintListCOMPLAINTID
.
AsString
);
item
.
AddPair
(
'Complaint'
,
complaintNumber
);
item
.
AddPair
(
'Agency'
,
FDb
.
uqComplaintListAGENCY
.
AsString
);
item
.
AddPair
(
'Priority'
,
FDb
.
uqComplaintListPRIORITY
.
AsString
);
item
.
AddPair
(
'DispatchCodeDesc'
,
FDb
.
uqComplaintListDISPATCH_CODE_DESC
.
AsString
);
item
.
AddPair
(
'Address'
,
FDb
.
uqComplaintListADDRESS
.
AsString
);
item
.
AddPair
(
'Business'
,
FDb
.
uqComplaintListBUSINESS
.
AsString
);
item
.
AddPair
(
'CFSId'
,
FDb
.
uqComplaintListCFSID
.
AsString
);
item
.
AddPair
(
'Status'
,
status
);
item
.
AddPair
(
'StatusColor'
,
statusColor
);
item
.
AddPair
(
'StatusTextColor'
,
statusTextColor
);
item
.
AddPair
(
'DispatchDistrict'
,
FDb
.
uqComplaintListDISPATCHDISTRICT
.
AsString
);
item
.
AddPair
(
'DateReported'
,
FDb
.
uqComplaintListDATEREPORTED
.
AsString
);
data
.
AddElement
(
item
);
Next
;
end
;
end
;
obj
.
AddPair
(
'count'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'returned'
,
TJSONNumber
.
Create
(
data
.
Count
));
obj
.
AddPair
(
'data'
,
data
);
Result
:=
obj
.
ToJSON
;
finally
FDb
.
uqComplaintList
.
Close
;
obj
.
Free
;
end
;
end
;
end
.
emiMobileServer/Source/Ws.Server.Module.pas
View file @
ee79d138
...
@@ -5,7 +5,8 @@ interface
...
@@ -5,7 +5,8 @@ interface
uses
uses
System
.
SysUtils
,
System
.
Classes
,
System
.
SysUtils
,
System
.
Classes
,
IdContext
,
IdContext
,
WebSocketServer
;
WebSocketServer
,
Ws
.
DataModel
;
type
type
TWsServerModule
=
class
(
TDataModule
)
TWsServerModule
=
class
(
TDataModule
)
...
@@ -13,6 +14,7 @@ type
...
@@ -13,6 +14,7 @@ type
procedure
DataModuleDestroy
(
Sender
:
TObject
);
procedure
DataModuleDestroy
(
Sender
:
TObject
);
private
private
FServer
:
TWebSocketServer
;
FServer
:
TWebSocketServer
;
FDataModel
:
TWsDataModel
;
procedure
DoConnect
(
AContext
:
TIdContext
);
procedure
DoConnect
(
AContext
:
TIdContext
);
procedure
DoDisconnect
(
AContext
:
TIdContext
);
procedure
DoDisconnect
(
AContext
:
TIdContext
);
procedure
DoExecute
(
AContext
:
TIdContext
);
procedure
DoExecute
(
AContext
:
TIdContext
);
...
@@ -55,6 +57,9 @@ end;
...
@@ -55,6 +57,9 @@ end;
procedure
TWsServerModule
.
DataModuleDestroy
(
Sender
:
TObject
);
procedure
TWsServerModule
.
DataModuleDestroy
(
Sender
:
TObject
);
begin
begin
FDataModel
.
Free
;
FDataModel
:=
nil
;
if
Assigned
(
FServer
)
then
if
Assigned
(
FServer
)
then
begin
begin
FServer
.
Active
:=
False
;
FServer
.
Active
:=
False
;
...
@@ -97,6 +102,15 @@ begin
...
@@ -97,6 +102,15 @@ begin
FServer
.
DefaultPort
:=
WS_PORT
;
FServer
.
DefaultPort
:=
WS_PORT
;
FServer
.
Active
:=
True
;
FServer
.
Active
:=
True
;
Logger
.
Log
(
1
,
Format
(
'WebSocket server listening on ws://0.0.0.0:%d/'
,
[
WS_PORT
]));
Logger
.
Log
(
1
,
Format
(
'WebSocket server listening on ws://0.0.0.0:%d/'
,
[
WS_PORT
]));
FDataModel
:=
TWsDataModel
.
Create
(
procedure
(
const
AMessage
:
string
)
begin
Broadcast
(
AMessage
);
end
,
30000
// broadcast interval in ms
);
Logger
.
Log
(
1
,
'WsDataModel started (30 s broadcast interval)'
);
end
;
end
;
procedure
TWsServerModule
.
Broadcast
(
const
AMessage
:
string
);
procedure
TWsServerModule
.
Broadcast
(
const
AMessage
:
string
);
...
...
emiMobileServer/Source/shared/WsMessages.pas
0 → 100644
View file @
ee79d138
unit
WsMessages
;
// WebSocket push message types.
// Each constant is the value of RequestId that identifies the message kind.
// Server builds and broadcasts; client dispatches on RequestId.
interface
uses
BaseRequest
,
Pkg
.
Json
.
DTO
,
REST
.
Json
.
Types
;
{$M+}
const
WS_MSG_BADGE_COUNTS
=
'BADGE_COUNTS'
;
WS_MSG_UNIT_MAP
=
'UNIT_MAP'
;
WS_MSG_COMPLAINT_MAP
=
'COMPLAINT_MAP'
;
WS_MSG_UNIT_LIST
=
'UNIT_LIST'
;
WS_MSG_COMPLAINT_LIST
=
'COMPLAINT_LIST'
;
type
// Simple typed DTO — serialized directly via AsJson for broadcast.
TWsBadgeCountsMessage
=
class
(
TRequest
)
private
FBadgeComplaints
:
Integer
;
FBadgeUnits
:
Integer
;
published
property
BadgeComplaints
:
Integer
read
FBadgeComplaints
write
FBadgeComplaints
;
property
BadgeUnits
:
Integer
read
FBadgeUnits
write
FBadgeUnits
;
public
constructor
Create
;
end
;
// Array-payload messages: server adds RequestId to the existing TJSONObject
// produced by the DB query methods, then broadcasts as a plain JSON string.
// These classes exist for requestId-based type identification and future
// client→server use (cast a received TRequest to the appropriate type).
TWsUnitMapMessage
=
class
(
TRequest
)
public
constructor
Create
;
end
;
TWsComplaintMapMessage
=
class
(
TRequest
)
public
constructor
Create
;
end
;
TWsUnitListMessage
=
class
(
TRequest
)
public
constructor
Create
;
end
;
TWsComplaintListMessage
=
class
(
TRequest
)
public
constructor
Create
;
end
;
implementation
constructor
TWsBadgeCountsMessage
.
Create
;
begin
inherited
;
RequestId
:=
WS_MSG_BADGE_COUNTS
;
end
;
constructor
TWsUnitMapMessage
.
Create
;
begin
inherited
;
RequestId
:=
WS_MSG_UNIT_MAP
;
end
;
constructor
TWsComplaintMapMessage
.
Create
;
begin
inherited
;
RequestId
:=
WS_MSG_COMPLAINT_MAP
;
end
;
constructor
TWsUnitListMessage
.
Create
;
begin
inherited
;
RequestId
:=
WS_MSG_UNIT_LIST
;
end
;
constructor
TWsComplaintListMessage
.
Create
;
begin
inherited
;
RequestId
:=
WS_MSG_COMPLAINT_LIST
;
end
;
end
.
webEMIMobile/Module.Websocket.pas
View file @
ee79d138
...
@@ -4,9 +4,12 @@ interface
...
@@ -4,9 +4,12 @@ interface
uses
uses
System
.
SysUtils
,
System
.
Classes
,
WEBLib
.
WebSocketClient
,
Web
,
WEBLib
.
Controls
,
WEBLib
.
Modules
,
System
.
SysUtils
,
System
.
Classes
,
WEBLib
.
WebSocketClient
,
Web
,
WEBLib
.
Controls
,
WEBLib
.
Modules
,
Auth
.
Service
;
Auth
.
Service
,
JS
;
type
type
// Handler signature: receives the fully parsed JSON object for one push message.
TWsDataHandler
=
procedure
(
aData
:
TJSObject
)
of
object
;
TdmWebsocket
=
class
(
TWebDataModule
)
TdmWebsocket
=
class
(
TWebDataModule
)
private
private
...
@@ -19,11 +22,26 @@ type
...
@@ -19,11 +22,26 @@ type
procedure
EMiMobileWebSocketClientBinaryDataReceived
(
Sender
:
TObject
;
procedure
EMiMobileWebSocketClientBinaryDataReceived
(
Sender
:
TObject
;
AData
:
TBytes
);
AData
:
TBytes
);
procedure
DispatchMessage
(
const
AMessage
:
string
);
FOnBadgeCounts
:
TWsDataHandler
;
FOnUnitMap
:
TWsDataHandler
;
FOnComplaintMap
:
TWsDataHandler
;
FOnUnitList
:
TWsDataHandler
;
FOnComplaintList
:
TWsDataHandler
;
public
public
EMiMobileWebSocketClient
:
TWebSocketClient
;
EMiMobileWebSocketClient
:
TWebSocketClient
;
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
DataModuleCreate
(
Sender
:
TObject
);
procedure
Connect
(
const
AWsUrl
:
string
);
procedure
Connect
(
const
AWsUrl
:
string
);
// Assign these before calling Connect so that pushes are routed immediately.
property
OnBadgeCounts
:
TWsDataHandler
read
FOnBadgeCounts
write
FOnBadgeCounts
;
property
OnUnitMap
:
TWsDataHandler
read
FOnUnitMap
write
FOnUnitMap
;
property
OnComplaintMap
:
TWsDataHandler
read
FOnComplaintMap
write
FOnComplaintMap
;
property
OnUnitList
:
TWsDataHandler
read
FOnUnitList
write
FOnUnitList
;
property
OnComplaintList
:
TWsDataHandler
read
FOnComplaintList
write
FOnComplaintList
;
end
;
end
;
var
var
...
@@ -102,32 +120,80 @@ begin
...
@@ -102,32 +120,80 @@ begin
EMiMobileWebSocketClient
.
Active
:=
True
;
EMiMobileWebSocketClient
.
Active
:=
True
;
end
;
end
;
procedure
TdmWebsocket
.
DispatchMessage
(
const
AMessage
:
string
);
var
obj
:
TJSObject
;
requestId
:
string
;
begin
if
AMessage
=
''
then
Exit
;
asm
try
{
obj = JSON.parse(AMessage);
}
catch
(
e
)
{
obj = null;
}
end
;
if
obj
=
nil
then
begin
console
.
log
(
'WS: could not parse message'
);
Exit
;
end
;
requestId
:=
string
(
obj
[
'RequestId'
]);
if
SameText
(
requestId
,
'BADGE_COUNTS'
)
then
begin
if
Assigned
(
FOnBadgeCounts
)
then
FOnBadgeCounts
(
obj
);
end
else
if
SameText
(
requestId
,
'UNIT_MAP'
)
then
begin
if
Assigned
(
FOnUnitMap
)
then
FOnUnitMap
(
obj
);
end
else
if
SameText
(
requestId
,
'COMPLAINT_MAP'
)
then
begin
if
Assigned
(
FOnComplaintMap
)
then
FOnComplaintMap
(
obj
);
end
else
if
SameText
(
requestId
,
'UNIT_LIST'
)
then
begin
if
Assigned
(
FOnUnitList
)
then
FOnUnitList
(
obj
);
end
else
if
SameText
(
requestId
,
'COMPLAINT_LIST'
)
then
begin
if
Assigned
(
FOnComplaintList
)
then
FOnComplaintList
(
obj
);
end
else
console
.
log
(
'WS: unknown RequestId: '
+
requestId
);
end
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientBinaryDataReceived
(
procedure
TdmWebsocket
.
EMiMobileWebSocketClientBinaryDataReceived
(
Sender
:
TObject
;
AData
:
TBytes
);
Sender
:
TObject
;
AData
:
TBytes
);
begin
begin
console
.
log
(
'
EMiMobileWebSocketClientBinaryDataReceived
'
);
console
.
log
(
'
WS: binary data received (ignored)
'
);
end
;
end
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientConnect
(
Sender
:
TObject
);
procedure
TdmWebsocket
.
EMiMobileWebSocketClientConnect
(
Sender
:
TObject
);
begin
begin
console
.
log
(
'
EMiMobileWebSocketClientConnect
'
);
console
.
log
(
'
WS: connected
'
);
end
;
end
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientDataReceived
(
Sender
:
TObject
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientDataReceived
(
Sender
:
TObject
;
Origin
:
string
;
SocketData
:
TJSObjectRecord
);
Origin
:
string
;
SocketData
:
TJSObjectRecord
);
begin
begin
console
.
log
(
'EMiMobileWebSocketClientDataReceived'
);
// Text messages arrive via EMiMobileWebSocketClientMessageReceived.
end
;
end
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientDisconnect
(
Sender
:
TObject
);
procedure
TdmWebsocket
.
EMiMobileWebSocketClientDisconnect
(
Sender
:
TObject
);
begin
begin
console
.
log
(
'
EMiMobileWebSocketClientDisconnect
'
);
console
.
log
(
'
WS: disconnected
'
);
end
;
end
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientMessageReceived
(
Sender
:
TObject
;
procedure
TdmWebsocket
.
EMiMobileWebSocketClientMessageReceived
(
Sender
:
TObject
;
AMessage
:
string
);
AMessage
:
string
);
begin
begin
console
.
log
(
'EMiMobileWebSocketClientMessageReceived'
);
DispatchMessage
(
AMessage
);
end
;
end
;
end
.
end
.
webEMIMobile/View.Complaints.pas
View file @
ee79d138
...
@@ -45,6 +45,7 @@ type
...
@@ -45,6 +45,7 @@ type
public
public
property
OnShowDetails
:
TSelectProc
read
FSelectProc
write
FSelectProc
;
property
OnShowDetails
:
TSelectProc
read
FSelectProc
write
FSelectProc
;
procedure
RefreshData
;
procedure
RefreshData
;
procedure
ApplyWsData
(
aRespObj
:
TJSObject
);
end
;
end
;
var
var
...
@@ -186,5 +187,22 @@ begin
...
@@ -186,5 +187,22 @@ begin
GetComplaints
;
GetComplaints
;
end
;
end
;
procedure
TFViewComplaints
.
ApplyWsData
(
aRespObj
:
TJSObject
);
var
complaintsCount
:
Integer
;
begin
if
FLoading
then
Exit
;
xdwdsComplaints
.
Close
;
xdwdsComplaints
.
SetJsonData
(
aRespObj
[
'data'
]);
xdwdsComplaints
.
Open
;
ShowHideBusinessRows
;
complaintsCount
:=
Integer
(
aRespObj
[
'count'
]);
lblEntries
.
Caption
:=
Format
(
'%d active complaints'
,
[
complaintsCount
]);
end
;
end
.
end
.
webEMIMobile/View.Main.pas
View file @
ee79d138
...
@@ -55,7 +55,6 @@ type
...
@@ -55,7 +55,6 @@ type
FDetailsForm
:
TWebForm
;
FDetailsForm
:
TWebForm
;
FArchiveForm
:
TWebForm
;
FArchiveForm
:
TWebForm
;
FLogoutProc
:
TLogoutProc
;
FLogoutProc
:
TLogoutProc
;
//WebSocketModule: Module.Websocket;
[
async
]
procedure
RefreshBadgesAsync
;
[
async
]
procedure
RefreshBadgesAsync
;
procedure
ShowUnitDetails
(
UnitId
:
string
);
procedure
ShowUnitDetails
(
UnitId
:
string
);
procedure
SetHeaderTitle
(
const
title
:
string
);
procedure
SetHeaderTitle
(
const
title
:
string
);
...
@@ -64,6 +63,13 @@ type
...
@@ -64,6 +63,13 @@ type
procedure
HideArchiveModal
;
procedure
HideArchiveModal
;
procedure
ShowArchiveModal
(
const
titleText
:
string
);
procedure
ShowArchiveModal
(
const
titleText
:
string
);
// WebSocket push handlers — called by Module.Websocket when the server broadcasts
procedure
HandleWsBadgeCounts
(
aData
:
TJSObject
);
procedure
HandleWsUnitMap
(
aData
:
TJSObject
);
procedure
HandleWsComplaintMap
(
aData
:
TJSObject
);
procedure
HandleWsUnitList
(
aData
:
TJSObject
);
procedure
HandleWsComplaintList
(
aData
:
TJSObject
);
type
TActivePanel
=
(
apNone
,
apMap
,
apUnits
,
apComplaints
);
type
TActivePanel
=
(
apNone
,
apMap
,
apUnits
,
apComplaints
);
var
var
FActivePanel
:
TActivePanel
;
FActivePanel
:
TActivePanel
;
...
@@ -151,9 +157,19 @@ begin
...
@@ -151,9 +157,19 @@ begin
SetActiveNavButton
(
'view.main.btnmap'
);
SetActiveNavButton
(
'view.main.btnmap'
);
SetActivePanel
(
apMap
);
SetActivePanel
(
apMap
);
// Initial badge counts still loaded via HTTP so the UI is populated immediately.
RefreshBadgesAsync
;
RefreshBadgesAsync
;
// Polling timers are replaced by WebSocket server-push broadcasts.
tmrBadgeCounts
.
Enabled
:=
False
;
tmrGlobalRefresh
.
Enabled
:=
False
;
dmWebsocket
:=
TdmWebsocket
.
Create
(
Self
);
dmWebsocket
:=
TdmWebsocket
.
Create
(
Self
);
dmWebsocket
.
OnBadgeCounts
:=
HandleWsBadgeCounts
;
dmWebsocket
.
OnUnitMap
:=
HandleWsUnitMap
;
dmWebsocket
.
OnComplaintMap
:=
HandleWsComplaintMap
;
dmWebsocket
.
OnUnitList
:=
HandleWsUnitList
;
dmWebsocket
.
OnComplaintList
:=
HandleWsComplaintList
;
dmWebsocket
.
Connect
(
DMConnection
.
WsUrl
);
dmWebsocket
.
Connect
(
DMConnection
.
WsUrl
);
end
;
end
;
...
@@ -444,6 +460,49 @@ begin
...
@@ -444,6 +460,49 @@ begin
end
;
end
;
// ---------------------------------------------------------------------------
// WebSocket push handlers
// ---------------------------------------------------------------------------
procedure
TFViewMain
.
HandleWsBadgeCounts
(
aData
:
TJSObject
);
var
el
:
TJSElement
;
begin
el
:=
Document
.
getElementById
(
'view.main.badgecomplaints'
);
if
Assigned
(
el
)
then
TJSHtmlElement
(
el
).
innerText
:=
string
(
aData
[
'BadgeComplaints'
]);
el
:=
Document
.
getElementById
(
'view.main.badgeunits'
);
if
Assigned
(
el
)
then
TJSHtmlElement
(
el
).
innerText
:=
string
(
aData
[
'BadgeUnits'
]);
end
;
procedure
TFViewMain
.
HandleWsUnitMap
(
aData
:
TJSObject
);
begin
if
Assigned
(
FMapForm
)
then
FMapForm
.
ApplyWsUnitMapData
(
TJSArray
(
aData
[
'data'
]));
end
;
procedure
TFViewMain
.
HandleWsComplaintMap
(
aData
:
TJSObject
);
begin
if
Assigned
(
FMapForm
)
then
FMapForm
.
ApplyWsComplaintMapData
(
TJSArray
(
aData
[
'data'
]));
end
;
procedure
TFViewMain
.
HandleWsUnitList
(
aData
:
TJSObject
);
begin
if
Assigned
(
FUnitsForm
)
then
FUnitsForm
.
ApplyWsData
(
aData
);
end
;
procedure
TFViewMain
.
HandleWsComplaintList
(
aData
:
TJSObject
);
begin
if
Assigned
(
FComplaintsForm
)
then
FComplaintsForm
.
ApplyWsData
(
aData
);
end
;
// ---------------------------------------------------------------------------
procedure
TFViewMain
.
tmrBadgeCountsTimer
(
Sender
:
TObject
);
procedure
TFViewMain
.
tmrBadgeCountsTimer
(
Sender
:
TObject
);
begin
begin
console
.
log
(
'Badges Refreshed'
);
console
.
log
(
'Badges Refreshed'
);
...
...
webEMIMobile/View.Map.pas
View file @
ee79d138
...
@@ -40,6 +40,8 @@ type
...
@@ -40,6 +40,8 @@ type
FGeoJsonLoadStep
:
Integer
;
FGeoJsonLoadStep
:
Integer
;
[
async
]
procedure
LoadPointsAsync
(
showBusy
:
Boolean
);
[
async
]
procedure
LoadPointsAsync
(
showBusy
:
Boolean
);
procedure
PlaceUnitMarkers
(
aData
:
TJSArray
);
procedure
PlaceComplaintMarkers
(
aData
:
TJSArray
);
procedure
UpdateDeviceLocation
(
lat
,
lng
:
Double
);
procedure
UpdateDeviceLocation
(
lat
,
lng
:
Double
);
procedure
StartDeviceLocation
;
procedure
StartDeviceLocation
;
procedure
ApplyPendingUnitFocus
;
procedure
ApplyPendingUnitFocus
;
...
@@ -52,6 +54,9 @@ type
...
@@ -52,6 +54,9 @@ type
procedure
FocusUnit
(
const
unitId
:
string
);
procedure
FocusUnit
(
const
unitId
:
string
);
procedure
FocusComplaint
(
const
complaintId
:
string
);
procedure
FocusComplaint
(
const
complaintId
:
string
);
procedure
RefreshData
;
procedure
RefreshData
;
// Called by View.Main when the server broadcasts a WebSocket push.
procedure
ApplyWsUnitMapData
(
aData
:
TJSArray
);
procedure
ApplyWsComplaintMapData
(
aData
:
TJSArray
);
end
;
end
;
var
var
...
@@ -262,22 +267,7 @@ end;
...
@@ -262,22 +267,7 @@ end;
[
async
]
procedure
TFViewMap
.
LoadPointsAsync
(
showBusy
:
Boolean
);
[
async
]
procedure
TFViewMap
.
LoadPointsAsync
(
showBusy
:
Boolean
);
var
var
resp
:
TXDataClientResponse
;
resp
:
TXDataClientResponse
;
root
,
item
,
uo
:
TJSObject
;
root
:
TJSObject
;
units
:
TJSArray
;
i
,
ui
:
Integer
;
m
:
TTMSFNCMapsMarker
;
lat
,
lng
:
Double
;
uName
,
unitBadge
,
agencyType
,
agencyId
,
agencyName
,
agency
:
string
;
unitId
,
callType
,
priorityText
,
statusText
:
string
;
complaintId
,
codeDesc
,
priority
,
priorityBadge
,
complaintStatusKey
,
business
,
address
:
string
;
pngName
,
iconUrl
,
rowsHtml
:
string
;
officer1Lname
,
officer1Fname
,
officer1Empnum
:
string
;
officer2Lname
,
officer2Fname
,
officer2Empnum
:
string
;
officer1Display
,
officer2Display
:
string
;
updateTimeText
:
string
;
canShowDetails
:
Boolean
;
canShowDetailsText
:
string
;
detailsBtnHtml
:
string
;
unitsData
:
TJSArray
;
unitsData
:
TJSArray
;
complaintsData
:
TJSArray
;
complaintsData
:
TJSArray
;
begin
begin
...
@@ -317,23 +307,52 @@ begin
...
@@ -317,23 +307,52 @@ begin
Console
.
Log
(
'Complaints XData error: '
+
E
.
ErrorResult
.
ErrorMessage
);
Console
.
Log
(
'Complaints XData error: '
+
E
.
ErrorResult
.
ErrorMessage
);
end
;
end
;
// ---
Swap Markers (no blank map while loading) ---------------------
------
// ---
Place markers (BeginUpdate wraps both so the map redraws once)
------
lfMap
.
BeginUpdate
;
lfMap
.
BeginUpdate
;
try
try
for
i
:=
lfMap
.
Markers
.
Count
-
1
downto
0
do
PlaceUnitMarkers
(
unitsData
);
begin
PlaceComplaintMarkers
(
complaintsData
);
m
:=
lfMap
.
Markers
[
i
];
finally
lfMap
.
EndUpdate
;
end
;
if
StartsText
(
'unit|'
,
m
.
DataString
)
or
StartsText
(
'complaint|'
,
m
.
DataString
)
then
if
mapFilters
<>
nil
then
lfMap
.
Markers
.
Delete
(
i
);
mapFilters
.
Apply
;
ApplyPendingUnitFocus
;
ApplyPendingComplaintFocus
;
finally
if
showBusy
then
HideSpinner
(
'spinner'
);
FLoadingPoints
:=
False
;
end
;
end
;
end
;
// Unit markers
procedure
TFViewMap
.
PlaceUnitMarkers
(
aData
:
TJSArray
);
if
unitsData
<>
nil
then
var
begin
item
:
TJSObject
;
for
i
:=
0
to
unitsData
.
Length
-
1
do
i
:
Integer
;
m
:
TTMSFNCMapsMarker
;
lat
,
lng
:
Double
;
uName
,
unitBadge
,
agencyType
,
agencyId
,
agencyName
,
agency
:
string
;
unitId
,
callType
,
priorityText
,
statusText
,
updateTimeText
:
string
;
officer1Lname
,
officer1Fname
,
officer1Empnum
:
string
;
officer2Lname
,
officer2Fname
,
officer2Empnum
:
string
;
officer1Display
,
officer2Display
:
string
;
iconUrl
,
canShowDetailsText
,
detailsBtnHtml
:
string
;
canShowDetails
:
Boolean
;
begin
// Remove stale unit markers
for
i
:=
lfMap
.
Markers
.
Count
-
1
downto
0
do
if
StartsText
(
'unit|'
,
lfMap
.
Markers
[
i
].
DataString
)
then
lfMap
.
Markers
.
Delete
(
i
);
if
aData
=
nil
then
Exit
;
for
i
:=
0
to
aData
.
Length
-
1
do
begin
begin
item
:=
TJSObject
(
units
Data
[
i
]);
item
:=
TJSObject
(
a
Data
[
i
]);
lat
:=
Double
(
item
[
'Lat'
]);
lat
:=
Double
(
item
[
'Lat'
]);
lng
:=
Double
(
item
[
'Lng'
]);
lng
:=
Double
(
item
[
'Lng'
]);
...
@@ -341,27 +360,18 @@ begin
...
@@ -341,27 +360,18 @@ begin
unitId
:=
GetJsonString
(
item
,
'UnitId'
);
unitId
:=
GetJsonString
(
item
,
'UnitId'
);
unitBadge
:=
GetJsonString
(
item
,
'UnitBadge'
);
unitBadge
:=
GetJsonString
(
item
,
'UnitBadge'
);
if
Trim
(
unitBadge
)
=
''
then
if
Trim
(
unitBadge
)
=
''
then
unitBadge
:=
uName
;
unitBadge
:=
uName
;
if
Trim
(
unitBadge
)
=
''
then
unitBadge
:=
unitId
;
if
Trim
(
unitBadge
)
=
''
then
unitBadge
:=
unitId
;
agencyId
:=
GetJsonString
(
item
,
'Agency'
);
agencyId
:=
GetJsonString
(
item
,
'Agency'
);
agencyName
:=
GetJsonString
(
item
,
'AgencyName'
);
agencyName
:=
GetJsonString
(
item
,
'AgencyName'
);
agency
:=
agencyName
;
agency
:=
agencyName
;
if
Trim
(
agency
)
=
''
then
if
Trim
(
agency
)
=
''
then
agency
:=
agencyId
;
agency
:=
agencyId
;
agencyType
:=
UpperCase
(
Trim
(
GetJsonString
(
item
,
'AgencyType'
)));
agencyType
:=
UpperCase
(
Trim
(
GetJsonString
(
item
,
'AgencyType'
)));
if
agencyType
=
'FIR'
then
iconUrl
:=
'assets/markers/car_fire.png'
if
agencyType
=
'FIR'
then
else
if
agencyType
=
'EMS'
then
iconUrl
:=
'assets/markers/car_ems.png'
iconUrl
:=
'assets/markers/car_fire.png'
else
iconUrl
:=
'assets/markers/car_police.png'
;
else
if
agencyType
=
'EMS'
then
iconUrl
:=
'assets/markers/car_ems.png'
else
if
agencyType
=
'POL'
then
iconUrl
:=
'assets/markers/car_police.png'
else
iconUrl
:=
'assets/markers/car_police.png'
;
callType
:=
GetJsonString
(
item
,
'CallType'
);
callType
:=
GetJsonString
(
item
,
'CallType'
);
priorityText
:=
GetJsonString
(
item
,
'Priority'
);
priorityText
:=
GetJsonString
(
item
,
'Priority'
);
...
@@ -371,7 +381,6 @@ begin
...
@@ -371,7 +381,6 @@ begin
officer1Lname
:=
GetJsonString
(
item
,
'Officer1Lname'
);
officer1Lname
:=
GetJsonString
(
item
,
'Officer1Lname'
);
officer1Fname
:=
GetJsonString
(
item
,
'Officer1Fname'
);
officer1Fname
:=
GetJsonString
(
item
,
'Officer1Fname'
);
officer1Empnum
:=
GetJsonString
(
item
,
'Officer1Empnum'
);
officer1Empnum
:=
GetJsonString
(
item
,
'Officer1Empnum'
);
officer2Lname
:=
GetJsonString
(
item
,
'Officer2Lname'
);
officer2Lname
:=
GetJsonString
(
item
,
'Officer2Lname'
);
officer2Fname
:=
GetJsonString
(
item
,
'Officer2Fname'
);
officer2Fname
:=
GetJsonString
(
item
,
'Officer2Fname'
);
officer2Empnum
:=
GetJsonString
(
item
,
'Officer2Empnum'
);
officer2Empnum
:=
GetJsonString
(
item
,
'Officer2Empnum'
);
...
@@ -382,13 +391,11 @@ begin
...
@@ -382,13 +391,11 @@ begin
canShowDetails
:=
SameText
(
Trim
(
canShowDetailsText
),
'true'
);
canShowDetails
:=
SameText
(
Trim
(
canShowDetailsText
),
'true'
);
if
canShowDetails
then
if
canShowDetails
then
begin
detailsBtnHtml
:=
detailsBtnHtml
:=
'<button type="button" class="btn btn-primary btn-sm px-2 py-1" '
+
'<button type="button" class="btn btn-primary btn-sm px-2 py-1" '
+
'onclick="window.showUnitDetails('''
+
unitId
+
''')">'
+
'onclick="window.showUnitDetails('''
+
unitId
+
''')">'
+
'Details'
+
'Details'
+
'</button>'
;
'</button>'
end
else
else
detailsBtnHtml
:=
''
;
detailsBtnHtml
:=
''
;
...
@@ -396,25 +403,23 @@ begin
...
@@ -396,25 +403,23 @@ begin
if
Trim
(
officer1Lname
+
officer1Fname
+
officer1Empnum
)
<>
''
then
if
Trim
(
officer1Lname
+
officer1Fname
+
officer1Empnum
)
<>
''
then
begin
begin
officer1Display
:=
Trim
(
officer1Lname
);
officer1Display
:=
Trim
(
officer1Lname
);
if
Trim
(
officer1Fname
)
<>
''
then
if
Trim
(
officer1Fname
)
<>
''
then
officer1Display
:=
officer1Display
+
', '
+
Trim
(
officer1Fname
);
officer1Display
:=
officer1Display
+
', '
+
Trim
(
officer1Fname
);
if
Trim
(
officer1Empnum
)
<>
''
then
officer1Display
:=
officer1Display
+
' ('
+
Trim
(
officer1Empnum
)
+
')'
;
if
Trim
(
officer1Empnum
)
<>
''
then
officer1Display
:=
officer1Display
+
' ('
+
Trim
(
officer1Empnum
)
+
')'
;
end
;
end
;
officer2Display
:=
''
;
officer2Display
:=
''
;
if
Trim
(
officer2Lname
+
officer2Fname
+
officer2Empnum
)
<>
''
then
if
Trim
(
officer2Lname
+
officer2Fname
+
officer2Empnum
)
<>
''
then
begin
begin
officer2Display
:=
Trim
(
officer2Lname
);
officer2Display
:=
Trim
(
officer2Lname
);
if
Trim
(
officer2Fname
)
<>
''
then
if
Trim
(
officer2Fname
)
<>
''
then
officer2Display
:=
officer2Display
+
', '
+
Trim
(
officer2Fname
);
officer2Display
:=
officer2Display
+
', '
+
Trim
(
officer2Fname
);
if
Trim
(
officer2Empnum
)
<>
''
then
officer2Display
:=
officer2Display
+
' ('
+
Trim
(
officer2Empnum
)
+
')'
;
if
Trim
(
officer2Empnum
)
<>
''
then
officer2Display
:=
officer2Display
+
' ('
+
Trim
(
officer2Empnum
)
+
')'
;
end
;
end
;
m
:=
lfMap
.
Markers
.
Add
;
m
:=
lfMap
.
Markers
.
Add
;
m
.
Latitude
:=
lat
;
m
.
Latitude
:=
lat
;
m
.
Longitude
:=
lng
;
m
.
Longitude
:=
lng
;
m
.
DataString
:=
'unit|'
+
unitId
+
'|'
+
StringReplace
(
unitBadge
,
'|'
,
'/'
,
[
rfReplaceAll
]);
m
.
IconURL
:=
iconUrl
;
m
.
Title
:=
m
.
Title
:=
'<span class="emi-marker-meta" data-marker-type="unit" data-unit-badge="'
+
'<span class="emi-marker-meta" data-marker-type="unit" data-unit-badge="'
+
...
@@ -423,78 +428,57 @@ begin
...
@@ -423,78 +428,57 @@ begin
HtmlAttrEncode
(
'Unit '
+
unitBadge
)
+
HtmlAttrEncode
(
'Unit '
+
unitBadge
)
+
'" style="display:none"></span>'
+
'" style="display:none"></span>'
+
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">'
+
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">'
+
'<div class="fw-semibold small">'
+
'<div class="fw-semibold small"><span class="fw-bold">Unit:</span> '
+
uName
+
'</div>'
+
'<span class="fw-bold">Unit:</span> '
+
uName
+
IfThen
(
agency
<>
''
,
'<div class="small"><span class="fw-bold">Agency:</span> '
+
agency
+
'</div>'
,
''
)
+
'</div>'
+
IfThen
(
Trim
(
callType
)
<>
''
,
'<div class="small"><span class="fw-bold">Call Type:</span> '
+
callType
+
'</div>'
,
''
)
+
IfThen
(
agency
<>
''
,
IfThen
(
Trim
(
priorityText
)
<>
''
,
'<div class="small"><span class="fw-bold">Priority:</span> '
+
priorityText
+
'</div>'
,
''
)
+
'<div class="small"><span class="fw-bold">Agency:</span> '
+
agency
+
'</div>'
,
IfThen
(
Trim
(
statusText
)
<>
''
,
'<div class="small"><span class="fw-bold">Status:</span> '
+
statusText
+
'</div>'
,
''
)
+
''
IfThen
(
Trim
(
officer1Display
)
<>
''
,
'<div class="small"><span class="fw-bold">Officer 1:</span> '
+
officer1Display
+
'</div>'
,
''
)
+
)
+
IfThen
(
Trim
(
officer2Display
)
<>
''
,
'<div class="small"><span class="fw-bold">Officer 2:</span> '
+
officer2Display
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
callType
)
<>
''
,
'<div class="small"><span class="fw-bold">Call Type:</span> '
+
callType
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
priorityText
)
<>
''
,
'<div class="small"><span class="fw-bold">Priority:</span> '
+
priorityText
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
statusText
)
<>
''
,
'<div class="small"><span class="fw-bold">Status:</span> '
+
statusText
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
officer1Display
)
<>
''
,
'<div class="small"><span class="fw-bold">Officer 1:</span> '
+
officer1Display
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
officer2Display
)
<>
''
,
'<div class="small"><span class="fw-bold">Officer 2:</span> '
+
officer2Display
+
'</div>'
,
''
)
+
IfThen
(
Trim
(
updateTimeText
)
<>
''
,
IfThen
(
Trim
(
updateTimeText
)
<>
''
,
'<div class="small mb-1"><span class="fw-bold">Updated:</span> '
+
updateTimeText
+
'</div>'
,
'<div class="small mb-1"><span class="fw-bold">Updated:</span> '
+
updateTimeText
+
'</div>'
,
'<div class="small mb-1"></div>'
'<div class="small mb-1"></div>'
)
+
)
+
IfThen
(
detailsBtnHtml
<>
''
,
'<div class="d-flex justify-content-end mt-0">'
+
detailsBtnHtml
+
'</div>'
,
''
)
+
IfThen
(
detailsBtnHtml
<>
''
,
'<div class="d-flex justify-content-end mt-0">'
+
detailsBtnHtml
+
'</div>'
,
''
)
+
'</div>'
;
'</div>'
;
end
;
end
;
m
.
DataString
:=
'unit|'
+
unitId
+
'|'
+
StringReplace
(
unitBadge
,
'|'
,
'/'
,
[
rfReplaceAll
]);
procedure
TFViewMap
.
PlaceComplaintMarkers
(
aData
:
TJSArray
);
m
.
IconURL
:=
iconUrl
;
var
item
,
uo
:
TJSObject
;
units
:
TJSArray
;
i
,
ui
:
Integer
;
m
:
TTMSFNCMapsMarker
;
lat
,
lng
:
Double
;
complaintId
,
codeDesc
,
priority
,
priorityBadge
,
complaintStatusKey
:
string
;
agencyId
,
agencyName
,
agency
,
business
,
address
:
string
;
pngName
,
iconUrl
,
rowsHtml
:
string
;
begin
// Remove stale complaint markers
for
i
:=
lfMap
.
Markers
.
Count
-
1
downto
0
do
if
StartsText
(
'complaint|'
,
lfMap
.
Markers
[
i
].
DataString
)
then
lfMap
.
Markers
.
Delete
(
i
);
end
;
if
aData
=
nil
then
end
;
Exit
;
// Complaint markers
for
i
:=
0
to
aData
.
Length
-
1
do
if
complaintsData
<>
nil
then
begin
for
i
:=
0
to
complaintsData
.
Length
-
1
do
begin
begin
item
:=
TJSObject
(
complaints
Data
[
i
]);
item
:=
TJSObject
(
a
Data
[
i
]);
complaintId
:=
GetJsonString
(
item
,
'ComplaintId'
);
complaintId
:=
GetJsonString
(
item
,
'ComplaintId'
);
codeDesc
:=
GetJsonString
(
item
,
'DispatchCodeDesc'
);
codeDesc
:=
GetJsonString
(
item
,
'DispatchCodeDesc'
);
agencyId
:=
GetJsonString
(
item
,
'Agency'
);
agencyId
:=
GetJsonString
(
item
,
'Agency'
);
agencyName
:=
GetJsonString
(
item
,
'AgencyName'
);
agencyName
:=
GetJsonString
(
item
,
'AgencyName'
);
agency
:=
agencyName
;
agency
:=
agencyName
;
if
Trim
(
agency
)
=
''
then
if
Trim
(
agency
)
=
''
then
agency
:=
agencyId
;
agency
:=
agencyId
;
priority
:=
GetJsonString
(
item
,
'Priority'
);
priority
:=
GetJsonString
(
item
,
'Priority'
);
priorityBadge
:=
GetJsonString
(
item
,
'PriorityBadge'
);
priorityBadge
:=
GetJsonString
(
item
,
'PriorityBadge'
);
if
Trim
(
priorityBadge
)
=
''
then
if
Trim
(
priorityBadge
)
=
''
then
priorityBadge
:=
'?'
;
priorityBadge
:=
'?'
;
complaintStatusKey
:=
LowerCase
(
Trim
(
GetJsonString
(
item
,
'ComplaintStatusKey'
)));
complaintStatusKey
:=
LowerCase
(
Trim
(
GetJsonString
(
item
,
'ComplaintStatusKey'
)));
if
complaintStatusKey
=
''
then
if
complaintStatusKey
=
''
then
complaintStatusKey
:=
'notattached'
;
complaintStatusKey
:=
'notattached'
;
business
:=
GetJsonString
(
item
,
'Business'
);
business
:=
GetJsonString
(
item
,
'Business'
);
address
:=
GetJsonString
(
item
,
'Address'
);
address
:=
GetJsonString
(
item
,
'Address'
);
lat
:=
Double
(
item
[
'Lat'
]);
lat
:=
Double
(
item
[
'Lat'
]);
lng
:=
Double
(
item
[
'Lng'
]);
lng
:=
Double
(
item
[
'Lng'
]);
...
@@ -509,13 +493,11 @@ begin
...
@@ -509,13 +493,11 @@ begin
rowsHtml
:=
''
;
rowsHtml
:=
''
;
units
:=
TJSArray
(
item
[
'Units'
]);
units
:=
TJSArray
(
item
[
'Units'
]);
if
Assigned
(
units
)
and
(
units
.
Length
>
0
)
then
if
Assigned
(
units
)
and
(
units
.
Length
>
0
)
then
begin
begin
for
ui
:=
0
to
units
.
Length
-
1
do
for
ui
:=
0
to
units
.
Length
-
1
do
begin
begin
uo
:=
TJSObject
(
units
[
ui
]);
uo
:=
TJSObject
(
units
[
ui
]);
rowsHtml
:=
rowsHtml
+
rowsHtml
:=
rowsHtml
+
'<tr>'
+
'<tr>'
+
'<td>'
+
GetJsonString
(
uo
,
'Unit'
)
+
'</td>'
+
'<td>'
+
GetJsonString
(
uo
,
'Unit'
)
+
'</td>'
+
...
@@ -530,7 +512,6 @@ begin
...
@@ -530,7 +512,6 @@ begin
m
:=
lfMap
.
Markers
.
Add
;
m
:=
lfMap
.
Markers
.
Add
;
m
.
Latitude
:=
lat
;
m
.
Latitude
:=
lat
;
m
.
Longitude
:=
lng
;
m
.
Longitude
:=
lng
;
m
.
DataString
:=
'complaint|'
+
complaintId
;
m
.
DataString
:=
'complaint|'
+
complaintId
;
m
.
IconURL
:=
iconUrl
;
m
.
IconURL
:=
iconUrl
;
...
@@ -543,40 +524,17 @@ begin
...
@@ -543,40 +524,17 @@ begin
HtmlAttrEncode
(
'Complaint '
+
complaintId
+
IfThen
(
Trim
(
codeDesc
)
<>
''
,
' - '
+
codeDesc
,
''
))
+
HtmlAttrEncode
(
'Complaint '
+
complaintId
+
IfThen
(
Trim
(
codeDesc
)
<>
''
,
' - '
+
codeDesc
,
''
))
+
'" style="display:none"></span>'
+
'" style="display:none"></span>'
+
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">'
+
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">'
+
'<div class="fw-semibold small">'
+
'<div class="fw-semibold small"><span class="fw-bold">Complaint:</span> '
+
complaintId
+
'</div>'
+
'<span class="fw-bold">Complaint:</span> '
+
complaintId
+
'<div class="small"><span class="fw-bold">Priority:</span> '
+
priority
+
'</div>'
+
'</div>'
+
'<div class="small"><span class="fw-bold">Dispatch Code:</span> '
+
codeDesc
+
'</div>'
+
'<div class="small">'
+
'<div class="small"><span class="fw-bold">Agency:</span> '
+
agency
+
'</div>'
+
'<span class="fw-bold">Priority:</span> '
+
priority
+
IfThen
(
Trim
(
business
)
<>
''
,
'<div class="small"><span class="fw-bold">Business:</span> '
+
business
+
'</div>'
,
''
)
+
'</div>'
+
'<div class="small mb-1"><span class="fw-bold">Address:</span> '
+
address
+
'</div>'
+
'<div class="small">'
+
'<span class="fw-bold">Dispatch Code:</span> '
+
codeDesc
+
'</div>'
+
'<div class="small">'
+
'<span class="fw-bold">Agency:</span> '
+
agency
+
'</div>'
+
IfThen
(
Trim
(
business
)
<>
''
,
'<div class="small">'
+
'<span class="fw-bold">Business:</span> '
+
business
+
'</div>'
,
''
)
+
'<div class="small mb-1">'
+
'<span class="fw-bold">Address:</span> '
+
address
+
'</div>'
+
'<table class="table table-sm table-bordered mb-1 emi-tip-table">'
+
'<table class="table table-sm table-bordered mb-1 emi-tip-table">'
+
'<colgroup>'
+
'<colgroup>'
+
'<col style="width:34%">'
+
'<col style="width:34%"><col style="width:33%"><col style="width:33%">'
+
'<col style="width:33%">'
+
'<col style="width:33%">'
+
'</colgroup>'
+
'</colgroup>'
+
'<thead class="table-light">'
+
'<thead class="table-light"><tr><th>Unit</th><th>Status</th><th>Updated</th></tr></thead>'
+
'<tr>'
+
'<th>Unit</th>'
+
'<th>Status</th>'
+
'<th>Updated</th>'
+
'</tr>'
+
'</thead>'
+
'<tbody>'
+
rowsHtml
+
'</tbody>'
+
'<tbody>'
+
rowsHtml
+
'</tbody>'
+
'</table>'
+
'</table>'
+
'<div class="d-flex justify-content-end mt-0">'
+
'<div class="d-flex justify-content-end mt-0">'
+
...
@@ -587,21 +545,41 @@ begin
...
@@ -587,21 +545,41 @@ begin
'</div>'
+
'</div>'
+
'</div>'
;
'</div>'
;
end
;
end
;
end
;
end
;
// --- WebSocket push entry points -------------------------------------------
procedure
TFViewMap
.
ApplyWsUnitMapData
(
aData
:
TJSArray
);
begin
// Skip if the map is still initialising or an HTTP load is in flight.
if
(
not
Assigned
(
mapFilters
))
or
FLoadingPoints
then
Exit
;
lfMap
.
BeginUpdate
;
try
PlaceUnitMarkers
(
aData
);
finally
finally
lfMap
.
EndUpdate
;
lfMap
.
EndUpdate
;
end
;
end
;
if
mapFilters
<>
nil
then
mapFilters
.
Apply
;
mapFilters
.
Apply
;
ApplyPendingUnitFocus
;
ApplyPendingUnitFocus
;
ApplyPendingComplaintFocus
;
end
;
procedure
TFViewMap
.
ApplyWsComplaintMapData
(
aData
:
TJSArray
);
begin
if
(
not
Assigned
(
mapFilters
))
or
FLoadingPoints
then
Exit
;
lfMap
.
BeginUpdate
;
try
PlaceComplaintMarkers
(
aData
);
finally
finally
if
showBusy
then
lfMap
.
EndUpdate
;
HideSpinner
(
'spinner'
);
FLoadingPoints
:=
False
;
end
;
end
;
mapFilters
.
Apply
;
ApplyPendingComplaintFocus
;
end
;
end
;
...
...
webEMIMobile/View.Units.pas
View file @
ee79d138
...
@@ -41,6 +41,7 @@ type
...
@@ -41,6 +41,7 @@ type
procedure
HandleListClick
(
e
:
TJSMouseEvent
);
procedure
HandleListClick
(
e
:
TJSMouseEvent
);
public
public
procedure
RefreshData
;
procedure
RefreshData
;
procedure
ApplyWsData
(
aRespObj
:
TJSObject
);
end
;
end
;
var
var
...
@@ -166,5 +167,19 @@ begin
...
@@ -166,5 +167,19 @@ begin
GetUnits
;
GetUnits
;
end
;
end
;
procedure
TFViewUnits
.
ApplyWsData
(
aRespObj
:
TJSObject
);
var
unitCount
:
Integer
;
begin
if
FLoading
then
Exit
;
xdwdsUnits
.
Close
;
xdwdsUnits
.
SetJsonData
(
aRespObj
[
'data'
]);
xdwdsUnits
.
Open
;
unitCount
:=
Integer
(
aRespObj
[
'count'
]);
lblEntries
.
Caption
:=
Format
(
'%d units'
,
[
unitCount
]);
end
;
end
.
end
.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment