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>
...@@ -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