Commit 5c3199e6 by Mac Stephens

Add user location and filtering functionality, Improve map marker handling fix…

Add user location and filtering functionality, Improve map marker handling fix list scrolling and details navigation; default detail summaries expanded; add dynamic main navbar title.
parent dc203ba1
[Settings] [Settings]
LogFileNum=579 LogFileNum=583
webClientVersion=0.1.0 webClientVersion=0.1.0
...@@ -2,31 +2,6 @@ ...@@ -2,31 +2,6 @@
<!-- Header / controls (non-scrolling) --> <!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<!-- Local navbar (Complaints) -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="complaints_title" class="navbar-brand mb-0 h5 text-white">Complaints</span>
</div>
<div class="col">
<button id="complaints_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="complaints_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="complaints_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"> <div class="bg-light border-bottom py-2">
......
...@@ -113,46 +113,25 @@ begin ...@@ -113,46 +113,25 @@ begin
if FLoading then Exit; if FLoading then Exit;
FLoading := True; FLoading := True;
console.log('GetComplaints: Invoking API...');
try try
try try
xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', [])); xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', []));
console.log('RawInvoke returned:', xdcResponse.Result);
respObj := TJSObject(xdcResponse.Result); respObj := TJSObject(xdcResponse.Result);
xdwdsComplaints.Close; xdwdsComplaints.Close;
console.log('Dataset closed');
xdwdsComplaints.SetJsonData(respObj['data']); xdwdsComplaints.SetJsonData(respObj['data']);
console.log('JsonData set on dataset:', respObj['data']);
xdwdsComplaints.Open; xdwdsComplaints.Open;
console.log('PriorityColor field name = ' +
xdwdsComplaintsPriorityColor.FieldName +
' sample value = ' +
xdwdsComplaintsPriorityColor.AsString);
if xdwdsComplaints.RecordCount > 0 then
begin
console.log('First record - Complaint:' + xdwdsComplaints.FieldByName('Complaint').AsString);
end;
complaintsCount := Integer(respObj['count']); complaintsCount := Integer(respObj['count']);
lblEntries.Caption := Format('%d active complaints', [complaintsCount]); lblEntries.Caption := Format('%d active complaints', [complaintsCount]);
console.log('Label updated:' + lblEntries.Caption);
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
begin begin
console.log('XData exception:' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage); Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end; end;
end; end;
finally finally
console.log('GetComplaints complete');
end; end;
HideSpinner('spinner'); HideSpinner('spinner');
end; end;
...@@ -161,7 +140,6 @@ end; ...@@ -161,7 +140,6 @@ end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject); procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin begin
GetComplaints; GetComplaints;
console.log('tmrRefreshTimer fired');
end; end;
end. end.
......
...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain ...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain
OnClick = lblUsersClick OnClick = lblUsersClick
Caption = 'Users' Caption = 'Users'
end end
object lblMainTitle: TWebLabel
Left = 131
Top = 31
Width = 61
Height = 15
ElementID = 'lbl_main_title'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel object WebPanel1: TWebPanel
Left = 136 Left = 136
Top = 110 Top = 110
......
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column vh-100">
<!-- Top Nav --> <!-- Top Nav -->
<nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0"> <nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
<div class="container-fluid"> <div class="container-fluid">
<!-- Left: Font button -->
<button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm">
font
</button>
<!-- Center: App title --> <!-- App title + current view title -->
<a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a> <div class="d-flex align-items-center ms-3">
<a id="view.main.apptitle" class="navbar-brand fw-bold text-light mb-0 me-1" href="index.html">emiMobile</a>
<span class="navbar-brand text-light mb-0 mx-0">-</span>
<span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span>
</div>
<!-- Right: Connection label --> <!-- Right: Connection label -->
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span> <span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span>
</div> </div>
</nav> </nav>
<!-- Main content: fills space between navbars --> <!-- Main content: fills space between navbars -->
...@@ -46,10 +46,10 @@ ...@@ -46,10 +46,10 @@
<!-- Spinner --> <!-- Spinner -->
<div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none"> <div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller"> <div class="lds-roller">
<div></div><div></div><div></div><div></div> <div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div> <div></div><div></div><div></div><div></div>
</div> </div>
</div> </div>
<!-- Error modal --> <!-- Error modal -->
......
...@@ -24,6 +24,7 @@ type ...@@ -24,6 +24,7 @@ type
btnComplaints: TWebButton; btnComplaints: TWebButton;
btnUnits: TWebButton; btnUnits: TWebButton;
tmrBadgeCounts: TWebTimer; tmrBadgeCounts: TWebTimer;
lblMainTitle: TWebLabel;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject); procedure mnuLogoutClick(Sender: TObject);
procedure wllblUserProfileClick(Sender: TObject); procedure wllblUserProfileClick(Sender: TObject);
...@@ -90,16 +91,18 @@ begin ...@@ -90,16 +91,18 @@ begin
lblUsers.Visible := false; lblUsers.Visible := false;
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
ShowForm(TFViewMap); ShowForm(TFViewMap);
SetHeaderTitle('Map');
RefreshBadgesAsync; RefreshBadgesAsync;
end; end;
procedure TFViewMain.SetHeaderTitle(const title: string); procedure TFViewMain.SetHeaderTitle(const title: string);
var var el: TJSElement;
el: TJSElement;
begin begin
el := Document.getElementById('view.main.lbltitle'); el := Document.getElementById('lbl_main_title');
if el <> nil then if el <> nil then
TJSHtmlElement(el).innerText := title; el.innerHTML := title
else
console.log('SetHeaderTitle: lbl_main_title not found');
end; end;
......
...@@ -2,72 +2,6 @@ object FViewMap: TFViewMap ...@@ -2,72 +2,6 @@ object FViewMap: TFViewMap
Width = 475 Width = 475
Height = 802 Height = 802
ElementFont = efCSS ElementFont = efCSS
object btnMenu: TWebButton
Left = 62
Top = 66
Width = 41
Height = 25
Caption = 'Menu'
ChildOrder = 1
ElementID = 'map.btnmenu'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnAlerts: TWebButton
Left = 148
Top = 66
Width = 35
Height = 25
Caption = 'Alerts'
ChildOrder = 1
ElementID = 'map.btnalerts'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnGroups: TWebButton
Left = 194
Top = 66
Width = 41
Height = 25
Caption = 'Groups'
ChildOrder = 1
ElementID = 'map.btngroups'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnLocate: TWebButton
Left = 246
Top = 66
Width = 39
Height = 25
Caption = 'Locate'
ChildOrder = 1
ElementID = 'map.btnlocate'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilters: TWebButton
Left = 297
Top = 66
Width = 35
Height = 25
Caption = 'Filters'
ChildOrder = 1
ElementID = 'map.btnfilters'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnDisplay: TWebButton
Left = 351
Top = 66
Width = 40
Height = 25
Caption = 'Display'
ChildOrder = 1
ElementID = 'map.btndisplay'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object pnlMap: TWebPanel object pnlMap: TWebPanel
Left = 62 Left = 62
Top = 120 Top = 120
...@@ -76,7 +10,7 @@ object FViewMap: TFViewMap ...@@ -76,7 +10,7 @@ object FViewMap: TFViewMap
ElementID = 'map_pnlmap' ElementID = 'map_pnlmap'
ChildOrder = 7 ChildOrder = 7
ElementPosition = epIgnore ElementPosition = epIgnore
TabOrder = 6 TabOrder = 0
object lfMap: TTMSFNCLeaflet object lfMap: TTMSFNCLeaflet
Left = 0 Left = 0
Top = 0 Top = 0
......
<div id="map.root" class="d-flex flex-column h-100 w-100"> <div id="map.root" class="d-flex flex-column h-100 w-100">
<nav class="d-flex gap-2 bg-primary p-2 overflow-x-auto border-bottom border-primary shadow-sm flex-shrink-0"> <!-- New: Offcanvas -->
<div class="offcanvas offcanvas-top"
<button id="map.btnmenu" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> tabindex="-1"
<i class="fa fa-bars me-2"></i><span class="d-none d-sm-inline">Menu</span> id="map_filters_offcanvas"
</button> aria-labelledby="map_filters_offcanvas_label"
style="--bs-offcanvas-width: 280px;">
<button id="map.btnalerts" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <div class="offcanvas-header">
<i class="fa fa-exclamation-circle me-2"></i><span class="d-none d-sm-inline">Alerts</span> <h5 class="offcanvas-title" id="map_filters_offcanvas_label">Map Filters</h5>
</button> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<button id="map.btngroups" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<i class="fa fa-users me-2"></i><span class="d-none d-sm-inline">Groups</span> <div class="offcanvas-body">
</button> <div class="mb-3">
<div class="form-check">
<button id="map.btnlocate" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <input class="form-check-input" type="checkbox" id="map_filter_units" checked>
<i class="fa fa-location-arrow me-2"></i><span class="d-none d-sm-inline">Locate</span> <label class="form-check-label" for="map_filter_units">Show Units</label>
</button> </div>
<div class="form-check">
<button id="map.btnfilters" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <input class="form-check-input" type="checkbox" id="map_filter_complaints" checked>
<i class="fa fa-sliders-h me-2"></i><span class="d-none d-sm-inline">Filter</span> <label class="form-check-label" for="map_filter_complaints">Show Complaints</label>
</button> </div>
</div>
<button id="map.btndisplay" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill">
<i class="fa fa-sun me-2"></i><span class="d-none d-sm-inline">Display</span> <div class="d-grid gap-2 mt-4">
</button> <button type="button" class="btn btn-primary" id="map_filters_apply" data-bs-dismiss="offcanvas">
</nav> Apply
</button>
<button type="button" class="btn btn-outline-secondary" id="map_filters_reset">
Reset
</button>
</div>
<div class="mt-3 small text-muted">
<span class="fw-semibold">Active:</span>
<span id="map_filters_summary"></span>
</div>
</div>
</div>
<div class="flex-grow-1 position-relative" style="min-height: 0;"> <div class="flex-grow-1 position-relative" style="min-height:0;">
<div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div> <div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div>
<button id="btn_map_filters"
type="button"
class="btn btn-primary position-absolute top-0 end-0 m-2 shadow"
style="z-index:1000;"
data-bs-toggle="offcanvas"
data-bs-target="#map_filters_offcanvas"
aria-controls="map_filters_offcanvas">
<i class="fa fa-sliders-h me-1"></i>Filters
</button>
</div> </div>
</div> </div>
...@@ -8,16 +8,10 @@ uses ...@@ -8,16 +8,10 @@ uses
WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils, WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client, VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
XData.Web.Connection, ConnectionModule, Utils; XData.Web.Connection, ConnectionModule, Utils, uMapFilters;
type type
TFViewMap = class(TWebForm) TFViewMap = class(TWebForm)
btnMenu: TWebButton;
btnAlerts: TWebButton;
btnGroups: TWebButton;
btnLocate: TWebButton;
btnFilters: TWebButton;
btnDisplay: TWebButton;
pnlMap: TWebPanel; pnlMap: TWebPanel;
lfMap: TTMSFNCLeaflet; lfMap: TTMSFNCLeaflet;
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
...@@ -31,12 +25,18 @@ type ...@@ -31,12 +25,18 @@ type
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrRefreshTimer(Sender: TObject); procedure tmrRefreshTimer(Sender: TObject);
private private
userLocationMarker: TTMSFNCMapsMarker;
geoWatchId: Integer;
FUnitsLoaded: Boolean; FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean; FComplaintsLoaded: Boolean;
FZoomPending: Boolean; FZoomPending: Boolean;
FLoadingPoints: Boolean; FLoadingPoints: Boolean;
mapFilters: TMapFilters;
[async] procedure LoadPointsAsync; [async] procedure LoadPointsAsync;
function CarIconForDistrict(const DistrictCode: string): string; function CarIconForDistrict(const DistrictCode: string): string;
procedure UpdateDeviceLocation(lat, lng: Double);
procedure StartDeviceLocation;
procedure StopDeviceLocation;
public public
end; end;
...@@ -77,9 +77,82 @@ begin ...@@ -77,9 +77,82 @@ begin
}; };
end; end;
{$ENDIF} {$ENDIF}
StartDeviceLocation;
end; end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
if userLocationMarker = nil then
begin
userLocationMarker := lfMap.Markers.Add;
userLocationMarker.DataString := 'device';
userLocationMarker.IconURL := 'assets/markers/location_dot.png';
userLocationMarker.Title := ''; // keeps popup code from binding anything for this marker
end;
userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng;
end;
procedure TFViewMap.StartDeviceLocation;
begin
asm
const self = this;
if (!navigator.geolocation) {
console.log('Geolocation not supported');
return;
}
const onPos = function (pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
try {
self.UpdateDeviceLocation(lat, lng);
} catch (e) {
console.log('UpdateDeviceLocation failed', e);
}
};
const onErr = function (err) {
console.log('Geolocation error:', err && err.message ? err.message : err);
};
if (self.geoWatchId != null && self.geoWatchId !== 0) {
try { navigator.geolocation.clearWatch(self.geoWatchId); } catch(e) {}
self.geoWatchId = 0;
}
self.geoWatchId = navigator.geolocation.watchPosition(onPos, onErr, {
enableHighAccuracy: true,
maximumAge: 5000,
timeout: 15000
});
end;
end;
procedure TFViewMap.StopDeviceLocation;
begin
asm
const self = this;
if (self.geoWatchId != null && self.geoWatchId !== 0 && navigator.geolocation) {
try { navigator.geolocation.clearWatch(self.geoWatchId); } catch(e) {}
self.geoWatchId = 0;
}
end;
end;
[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var var
i: Integer; i: Integer;
...@@ -136,6 +209,12 @@ begin ...@@ -136,6 +209,12 @@ begin
end; end;
await(LoadPointsAsync); await(LoadPointsAsync);
if mapFilters = nil then
begin
mapFilters := TMapFilters.Create(lfMap);
mapFilters.Init;
end;
end; end;
function TFViewMap.CarIconForDistrict(const DistrictCode: string): string; function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
...@@ -162,7 +241,7 @@ end; ...@@ -162,7 +241,7 @@ end;
var var
resp: TXDataClientResponse; resp: TXDataClientResponse;
root, item, uo: TJSObject; root, item, uo: TJSObject;
data, units: TJSArray; units: TJSArray;
i, ui: Integer; i, ui: Integer;
m: TTMSFNCMapsMarker; m: TTMSFNCMapsMarker;
lat, lng: Double; lat, lng: Double;
...@@ -177,33 +256,70 @@ var ...@@ -177,33 +256,70 @@ var
canShowDetails: Boolean; canShowDetails: Boolean;
canShowDetailsText: string; canShowDetailsText: string;
detailsBtnHtml: string; detailsBtnHtml: string;
unitsData: TJSArray;
complaintsData: TJSArray;
begin begin
if FLoadingPoints then
Exit;
FLoadingPoints := True;
ShowSpinner('spinner'); ShowSpinner('spinner');
FUnitsLoaded := False;
FComplaintsLoaded := False;
// --- Units ---------------------------------------------------------------
try try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', [])); FUnitsLoaded := False;
root := TJSObject(resp.Result); FComplaintsLoaded := False;
data := TJSArray(root['data']);
unitsData := nil;
complaintsData := nil;
// --- Fetch Units ---------------------------------------------------------
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
unitsData := TJSArray(root['data']);
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
if data <> nil then // --- Fetch Complaints ----------------------------------------------------
begin try
lfMap.BeginUpdate; resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
try root := TJSObject(resp.Result);
for i := 0 to data.Length - 1 do complaintsData := TJSArray(root['data']);
FComplaintsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
// --- Swap Markers (no blank map while loading) ---------------------------
lfMap.BeginUpdate;
try
// Delete old unit/complaint markers right before adding new ones
for i := lfMap.Markers.Count - 1 downto 0 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, 'unit') or StartsText('complaint|', m.DataString) then
lfMap.Markers.Delete(i);
end;
// Add unit markers
if unitsData <> nil then
begin
for i := 0 to unitsData.Length - 1 do
begin begin
item := TJSObject(data[i]); item := TJSObject(unitsData[i]);
lat := Double(item['Lat']); lat := Double(item['Lat']);
lng := Double(item['Lng']); lng := Double(item['Lng']);
uName := string(item['UnitName']); uName := string(item['UnitName']);
dist := string(item['District']); dist := string(item['District']);
unitId := string(item['UnitId']); unitId := string(item['UnitId']);
callType := string(item['CallType']); callType := string(item['CallType']);
priorityText := string(item['Priority']); priorityText := string(item['Priority']);
statusText := string(item['Status']); statusText := string(item['Status']);
updateTimeText := string(item['UpdateTime']); updateTimeText := string(item['UpdateTime']);
officer1Lname := string(item['Officer1Lname']); officer1Lname := string(item['Officer1Lname']);
...@@ -228,9 +344,7 @@ begin ...@@ -228,9 +344,7 @@ begin
'</button>'; '</button>';
end end
else else
begin
detailsBtnHtml := ''; detailsBtnHtml := '';
end;
officer1Display := ''; officer1Display := '';
if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then
...@@ -253,7 +367,7 @@ begin ...@@ -253,7 +367,7 @@ begin
end; end;
m := lfMap.Markers.Add; m := lfMap.Markers.Add;
m.Latitude := lat; m.Latitude := lat;
m.Longitude := lng; m.Longitude := lng;
m.Title := m.Title :=
...@@ -296,37 +410,21 @@ begin ...@@ -296,37 +410,21 @@ begin
'</div>'; '</div>';
m.DataString := 'unit'; m.DataString := 'unit';
m.IconURL := CarIconForDistrict(dist); m.IconURL := CarIconForDistrict(dist);
end; end;
finally
lfMap.EndUpdate;
end; end;
end;
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
// --- Complaints ---------------------------------------------------------- // Add complaint markers
try if complaintsData <> nil then
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', [])); begin
root := TJSObject(resp.Result); for i := 0 to complaintsData.Length - 1 do
data := TJSArray(root['data']);
if data <> nil then
begin
lfMap.BeginUpdate;
try
for i := 0 to data.Length - 1 do
begin begin
item := TJSObject(data[i]); item := TJSObject(complaintsData[i]);
complaintId := string(item['ComplaintId']); complaintId := string(item['ComplaintId']);
codeDesc := string(item['DispatchCodeDesc']); codeDesc := string(item['DispatchCodeDesc']);
dispatchDist := string(item['DispatchDistrict']); dispatchDist := string(item['DispatchDistrict']);
priority := string(item['Priority']); priority := string(item['Priority']);
lat := Double(item['Lat']); lat := Double(item['Lat']);
lng := Double(item['Lng']); lng := Double(item['Lng']);
...@@ -351,8 +449,8 @@ begin ...@@ -351,8 +449,8 @@ begin
rowsHtml := rowsHtml + rowsHtml := rowsHtml +
'<tr>' + '<tr>' +
'<td>' + string(uo['Unit']) + '</td>' + '<td>' + string(uo['Unit']) + '</td>' +
'<td>' + string(uo['Status']) + '</td>' + '<td>' + string(uo['Status']) + '</td>' +
'<td>' + string(uo['Updated']) + '</td>' + '<td>' + string(uo['Updated']) + '</td>' +
'</tr>'; '</tr>';
end; end;
...@@ -360,11 +458,13 @@ begin ...@@ -360,11 +458,13 @@ begin
else else
rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>'; rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';
// Complaint Markers
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.IconURL := iconUrl;
m.Title := m.Title :=
'<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">' +
...@@ -404,30 +504,33 @@ begin ...@@ -404,30 +504,33 @@ begin
'</button>' + '</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
m.IconURL := iconUrl;
end; end;
finally
lfMap.EndUpdate;
end; end;
finally
lfMap.EndUpdate;
end; end;
FComplaintsLoaded := True; if mapFilters <> nil then
except mapFilters.Apply;
on E: EXDataClientRequestException do finally
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage); HideSpinner('spinner');
FLoadingPoints := False;
end; end;
HideSpinner('spinner');
end; end;
procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string); procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
begin begin
ACustomizeMarker := ACustomizeMarker :=
'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 + 'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 + 'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 +
'var ds = (o && o.datastring) ? o.datastring : "";' + #13#10 +
'if (ds === "device") {' + #13#10 +
' try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
' try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
' return;' + #13#10 +
'}' + #13#10 +
'o.tooltipHtml = rawTitle;' + #13#10 + 'o.tooltipHtml = rawTitle;' + #13#10 +
'o.title = "";' + #13#10 + 'o.title = "";' + #13#10 +
'var t = o.tooltipHtml || "";' + #13#10 + 'var t = o.tooltipHtml || "";' + #13#10 +
...@@ -521,7 +624,7 @@ begin ...@@ -521,7 +624,7 @@ begin
// --- Marker Badge ------------------------------------------------------------ // --- Marker Badge ------------------------------------------------------------
'.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+ '.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
'.emi-marker-img{display:block;}'+#13#10+ '.emi-marker-img{display:block;}'+#13#10+
'.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}'; '.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}' + #13#10;
end; end;
procedure TFViewMap.tmrRefreshTimer(Sender: TObject); procedure TFViewMap.tmrRefreshTimer(Sender: TObject);
......
...@@ -61,10 +61,6 @@ ...@@ -61,10 +61,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -2,31 +2,6 @@ ...@@ -2,31 +2,6 @@
<!-- Header / controls (non-scrolling) --> <!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<!-- Local navbar (Units) -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="units_title" class="navbar-brand mb-0 h5 text-white">Units</span>
</div>
<div class="col">
<button id="units_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="units_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="units_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"> <div class="bg-light border-bottom py-2">
......
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;
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;
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);
end;
procedure TMapFilters.ResetUi;
begin
SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True);
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 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: (currently none) but you can set m.DataString := 'complaint|<id>'
showMarker := True;
if SameText(ds, 'unit') then
showMarker := FShowUnits
else if StartsText('complaint', LowerCase(ds)) then
showMarker := FShowComplaints;
// 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.
...@@ -22,7 +22,8 @@ uses ...@@ -22,7 +22,8 @@ uses
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html}, View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html}, View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html}; View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas';
{$R *.res} {$R *.res}
......
...@@ -186,6 +186,7 @@ ...@@ -186,6 +186,7 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="uMapFilters.pas"/>
<None Include="index.html"/> <None Include="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
......
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