Commit f5a72c13 by Mac Stephens

Implement map focus navigation (View On Map from lists/details), add toggleable…

Implement map focus navigation (View On Map from lists/details), add toggleable hidden location dot, remove map auto-center in favor of GeoJSON center, convert warnings to responsive cards on mobile, and normalize dispatch district display
parent 5c3199e6
...@@ -342,7 +342,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -342,7 +342,7 @@ object ApiDatabaseModule: TApiDatabaseModule
' cm.REMARKS' ' cm.REMARKS'
'FROM CFS_MEMOS cm' 'FROM CFS_MEMOS cm'
'WHERE cm.CFSID = :CFSID' 'WHERE cm.CFSID = :CFSID'
'ORDER BY cm.TIMESTAMP ASC') 'ORDER BY cm.TIMESTAMP DESC')
ReadOnly = True ReadOnly = True
Left = 196 Left = 196
Top = 248 Top = 248
...@@ -350,7 +350,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -350,7 +350,7 @@ object ApiDatabaseModule: TApiDatabaseModule
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'CFSID' Name = 'CFSID'
Value = Null Value = nil
end> end>
object uqCFSMemosMEMO_ID: TFloatField object uqCFSMemosMEMO_ID: TFloatField
FieldName = 'MEMO_ID' FieldName = 'MEMO_ID'
......
[Settings] [Settings]
LogFileNum=583 LogFileNum=592
webClientVersion=0.1.0 webClientVersion=0.1.0
...@@ -335,6 +335,45 @@ object FViewComplaintDetails: TFViewComplaintDetails ...@@ -335,6 +335,45 @@ object FViewComplaintDetails: TFViewComplaintDetails
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnRemarksClick OnClick = btnRemarksClick
end end
object lstWarnings: TWebDBListControl
Left = 596
Top = 352
Width = 191
Height = 65
ElementID = 'lst_warnings'
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
ChildOrder = 12
DefaultItemClassName = 'list-group-item'
DefaultItemLinkClassName = 'list-group-link'
ElementFont = efCSS
ElementListClassName = 'list-group'
Items = <>
Style = lsListGroup
DataSource = wdsWarnings
ItemTemplate =
'<div class="card mb-2"> <div class="card-body py-2"> <div cl' +
'ass="d-flex justify-content-between gap-2"> <div class="fw-' +
'semibold">(%CodeDesc%)</div> <div class="text-muted small t' +
'ext-end">(%ADDRESS%)</div> </div> <div class="small mt-1">' +
'(%NOTES%)</div> </div></div>'
ListSource = wdsWarnings
end
object btnComplaintViewOnMap: TWebButton
Left = 510
Top = 430
Width = 96
Height = 25
Caption = 'View On Map'
ChildOrder = 1
ElementID = 'btn_complaint_view_on_map'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnComplaintViewOnMapClick
end
object xdwcComplaintDetails: TXDataWebClient object xdwcComplaintDetails: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 378 Left = 378
...@@ -413,8 +452,8 @@ object FViewComplaintDetails: TFViewComplaintDetails ...@@ -413,8 +452,8 @@ object FViewComplaintDetails: TFViewComplaintDetails
Top = 352 Top = 352
end end
object xdwdsWarnings: TXDataWebDataSet object xdwdsWarnings: TXDataWebDataSet
Left = 638 Left = 634
Top = 352 Top = 492
object xdwdsWarningsCodeDesc: TStringField object xdwdsWarningsCodeDesc: TStringField
FieldName = 'CodeDesc' FieldName = 'CodeDesc'
end end
...@@ -427,7 +466,7 @@ object FViewComplaintDetails: TFViewComplaintDetails ...@@ -427,7 +466,7 @@ object FViewComplaintDetails: TFViewComplaintDetails
end end
object wdsWarnings: TWebDataSource object wdsWarnings: TWebDataSource
DataSet = xdwdsWarnings DataSet = xdwdsWarnings
Left = 740 Left = 728
Top = 352 Top = 492
end end
end end
...@@ -80,12 +80,19 @@ ...@@ -80,12 +80,19 @@
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body p-0"> <div class="card-body p-0">
<div id="tbl_remarks"></div> <div id="tbl_remarks"></div>
<div id="tbl_history" class="d-none"></div> <div id="tbl_history" class="tab-hidden"></div>
<div id="tbl_contacts" class="d-none"></div> <div id="tbl_warnings" class="d-none d-md-block"></div>
<div id="tbl_warnings" class="d-none"></div> <div id="lst_warnings" class="d-block d-md-none"></div>
<div id="tbl_contacts" class="tab-hidden"></div>
</div> </div>
</div> </div>
</div> </div>
<button id="btn_complaint_view_on_map"
type="button"
class="btn btn-primary btn-sm shadow position-fixed"
style="right: 12px; bottom: 72px; z-index: 1040;">
View On Map
</button>
</div> </div>
...@@ -9,7 +9,7 @@ uses ...@@ -9,7 +9,7 @@ uses
ConnectionModule, ConnectionModule,
Utils, Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls, Utils, Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls,
Data.DB, WEBLib.DB, XData.Web.JsonDataset, XData.Web.Dataset, Data.DB, WEBLib.DB, XData.Web.JsonDataset, XData.Web.Dataset,
Vcl.StdCtrls, WEBLib.StdCtrls; Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.Lists;
type type
TFViewComplaintDetails = class(TWebForm) TFViewComplaintDetails = class(TWebForm)
...@@ -52,12 +52,15 @@ type ...@@ -52,12 +52,15 @@ type
btnE911: TWebButton; btnE911: TWebButton;
btnREM: TWebButton; btnREM: TWebButton;
btnUnt: TWebButton; btnUnt: TWebButton;
lstWarnings: TWebDBListControl;
btnComplaintViewOnMap: TWebButton;
procedure btnRemarksClick(Sender: TObject); procedure btnRemarksClick(Sender: TObject);
procedure btnHistoryClick(Sender: TObject); procedure btnHistoryClick(Sender: TObject);
procedure btnContactsClick(Sender: TObject); procedure btnContactsClick(Sender: TObject);
procedure btnWarningsClick(Sender: TObject); procedure btnWarningsClick(Sender: TObject);
procedure btnCmpClick(Sender: TObject); procedure btnCmpClick(Sender: TObject);
procedure btnComplaintViewOnMapClick(Sender: TObject);
procedure btnE911Click(Sender: TObject); procedure btnE911Click(Sender: TObject);
procedure btnREMClick(Sender: TObject); procedure btnREMClick(Sender: TObject);
procedure btnUntClick(Sender: TObject); procedure btnUntClick(Sender: TObject);
...@@ -243,9 +246,9 @@ begin ...@@ -243,9 +246,9 @@ begin
Exit; Exit;
if hidden then if hidden then
TJSElement(el).classList.add('d-none') TJSElement(el).classList.add('tab-hidden')
else else
TJSElement(el).classList.remove('d-none'); TJSElement(el).classList.remove('tab-hidden');
end; end;
procedure TFViewComplaintDetails.UpdateToggleButtonCss(const buttonId: string; isOn: Boolean); procedure TFViewComplaintDetails.UpdateToggleButtonCss(const buttonId: string; isOn: Boolean);
...@@ -349,13 +352,16 @@ begin ...@@ -349,13 +352,16 @@ begin
SetHiddenById('tbl_remarks', tabName <> 'remarks'); SetHiddenById('tbl_remarks', tabName <> 'remarks');
SetHiddenById('tbl_history', tabName <> 'history'); SetHiddenById('tbl_history', tabName <> 'history');
SetHiddenById('tbl_contacts', tabName <> 'contacts'); SetHiddenById('tbl_contacts', tabName <> 'contacts');
SetHiddenById('tbl_warnings', tabName <> 'warnings'); SetHiddenById('tbl_warnings', tabName <> 'warnings');
SetHiddenById('lst_warnings', tabName <> 'warnings');
SetHiddenById('row_remarks_toggles', tabName <> 'remarks'); SetHiddenById('row_remarks_toggles', tabName <> 'remarks');
UpdateTabButtonCss(tabName); UpdateTabButtonCss(tabName);
end; end;
procedure TFViewComplaintDetails.ApplyRemarksFilters; procedure TFViewComplaintDetails.ApplyRemarksFilters;
var var
filteredArr: TJSArray; filteredArr: TJSArray;
...@@ -496,6 +502,12 @@ begin ...@@ -496,6 +502,12 @@ begin
ApplyRemarksFilters; ApplyRemarksFilters;
end; end;
procedure TFViewComplaintDetails.btnComplaintViewOnMapClick(Sender: TObject);
begin
if Assigned(FViewMain) then
FViewMain.ShowMapFocusComplaint(FComplaintId);
end;
procedure TFViewComplaintDetails.btnE911Click(Sender: TObject); procedure TFViewComplaintDetails.btnE911Click(Sender: TObject);
begin begin
FShowE911 := not FShowE911; FShowE911 := not FShowE911;
......
...@@ -24,49 +24,6 @@ object FViewComplaints: TFViewComplaints ...@@ -24,49 +24,6 @@ object FViewComplaints: TFViewComplaints
Visible = False Visible = False
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
end end
object btnGroup: TWebButton
Left = 180
Top = 110
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 242
Top = 110
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnRefresh: TWebButton
Left = 114
Top = 110
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object dblComplaintsList: TWebDBListControl object dblComplaintsList: TWebDBListControl
Left = 36 Left = 36
Top = 148 Top = 148
...@@ -85,19 +42,23 @@ object FViewComplaints: TFViewComplaints ...@@ -85,19 +42,23 @@ object FViewComplaints: TFViewComplaints
Style = lsListGroup Style = lsListGroup
DataSource = wdsComplaints DataSource = wdsComplaints
ItemTemplate = ItemTemplate =
'<div class="list-section-header small fw-semibold bg-secondary t' + '<div class="list-section-header small fw-semibold bg-secondary ' +
'ext-white rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><div cl' + 'text-white rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><div ' +
'ass="card border shadow-sm" style="--bs-card-bg:(%PriorityCo' + 'class="card border shadow-sm position-relative" style=" --bs' +
'lor%);--bs-card-color:(%PriorityTextColor%);"> <div class="card' + '-card-bg: (%PriorityColor%); --bs-card-color: (%PriorityTextC' +
'-body py-2 px-3"> <div class="fw-bold text-uppercase small"> ' + 'olor%); "> <div class="card-body py-2 px-3"> <div class="fw' +
' (%Priority%): (%DispatchCodeDesc%) </div> <div class=' + '-bold text-uppercase small"> (%Priority%): (%DispatchCodeDe' +
'"small">(%Address%)</div> <div class="small text-opacity-75 d' + 'sc%) </div> <div class="small">(%Address%)</div> <div c' +
'-flex align-items-center"> <span> (%Complaint%): (%S' + 'lass="small text-opacity-75"> (%Complaint%): (%Status%)&nbs' +
'tatus%)&nbsp;&nbsp;(%DistrictSector%) </span> <button ' + 'p;&nbsp;(%DistrictSector%) </div> <div class="small text-o' +
'type="button" class="btn btn-primary btn-sm ms-auto' + 'pacity-75">(%DateReported%)</div> </div> <div class="position-' +
' complaint-details-btn" data-id="(%ComplaintId%)"> ' + 'absolute top-50 end-0 translate-middle-y pe-2 d-flex flex-column' +
' Details </button> </div> <div class="small tex' + ' gap-1"> <button type="button" class="btn btn-prima' +
't-opacity-75">(%DateReported%)</div> </div></div>' 'ry btn-sm complaint-details-btn" data-id="(%ComplaintId%)" ' +
' > Details </button> <button type="button" ' +
' class="btn btn-secondary btn-sm btn-complaint-map" data-i' +
'd="(%ComplaintId%)" > View On Map </button> </div></' +
'div>'
ListSource = wdsComplaints ListSource = wdsComplaints
end end
object xdwcComplaints: TXDataWebClient object xdwcComplaints: TXDataWebClient
......
...@@ -24,9 +24,6 @@ type ...@@ -24,9 +24,6 @@ type
xdwdsComplaintsDispatchDistrict: TStringField; xdwdsComplaintsDispatchDistrict: TStringField;
xdwdsComplaintsDateReported: TStringField; xdwdsComplaintsDateReported: TStringField;
lblEntries: TWebLabel; lblEntries: TWebLabel;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsComplaints: TWebDataSource; wdsComplaints: TWebDataSource;
xdwdsComplaintsComplaintId: TStringField; xdwdsComplaintsComplaintId: TStringField;
xdwdsComplaintsDistrictHeader: TStringField; xdwdsComplaintsDistrictHeader: TStringField;
...@@ -77,20 +74,52 @@ begin ...@@ -77,20 +74,52 @@ begin
end; end;
procedure TFViewComplaints.HandleListClick(e: TJSMouseEvent); procedure TFViewComplaints.HandleListClick(e: TJSMouseEvent);
var el: TJSElement; id: string; var
el: TJSElement;
btn: TJSElement;
complaintId: string;
begin begin
btn := nil;
el := TJSElement(e.target); el := TJSElement(e.target);
if (el is TJSHtmlElement) and TJSHtmlElement(el).classList.contains('complaint-details-btn') then
asm
btn = (el && el.closest) ? el.closest('.complaint-details-btn') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin begin
id := string(TJSHtmlElement(el).dataset['id']); // from data-id="(%ComplaintId%)" complaintId := string(TJSHtmlElement(btn).getAttribute('data-id'));
e.preventDefault; e.preventDefault;
e.stopPropagation; e.stopPropagation;
asm asm
if (window.showComplaintDetails) window.showComplaintDetails(id); if (window.showComplaintDetails) window.showComplaintDetails(complaintId);
end; end;
end
else
begin
btn := nil;
asm
btn = (el && el.closest) ? el.closest('.btn-complaint-map') : null;
end; end;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
complaintId := string(TJSHtmlElement(btn).getAttribute('data-id'));
e.preventDefault;
e.stopPropagation;
asm
try {
pas['View.Main'].FViewMain.ShowMapFocusComplaint(complaintId);
} catch (e) {
console.log('ShowMapFocusComplaint failed', e);
}
end;
end;
end;
end;
procedure TFViewComplaints.WebFormDestroy(Sender: TObject); procedure TFViewComplaints.WebFormDestroy(Sender: TObject);
...@@ -98,7 +127,7 @@ begin ...@@ -98,7 +127,7 @@ begin
Document.removeEventListener('click', @HandleListClick); Document.removeEventListener('click', @HandleListClick);
end; end;
//HTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property //Note: jjHTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property
procedure TFViewComplaints.btnRefreshClick(Sender: TObject); procedure TFViewComplaints.btnRefreshClick(Sender: TObject);
begin begin
GetComplaints; GetComplaints;
...@@ -133,6 +162,7 @@ begin ...@@ -133,6 +162,7 @@ begin
end; end;
finally finally
end; end;
FLoading := False;
HideSpinner('spinner'); HideSpinner('spinner');
end; end;
......
...@@ -80,7 +80,7 @@ object FViewMain: TFViewMain ...@@ -80,7 +80,7 @@ object FViewMain: TFViewMain
object lblMainTitle: TWebLabel object lblMainTitle: TWebLabel
Left = 131 Left = 131
Top = 31 Top = 31
Width = 61 Width = 3
Height = 15 Height = 15
ElementID = 'lbl_main_title' ElementID = 'lbl_main_title'
ElementFont = efCSS ElementFont = efCSS
......
...@@ -56,6 +56,8 @@ type ...@@ -56,6 +56,8 @@ type
procedure ShowUserForm(Info: string); procedure ShowUserForm(Info: string);
procedure ShowComplaintDetails(ComplaintId: string); procedure ShowComplaintDetails(ComplaintId: string);
procedure SetActiveNavButton(const BtnId: string); procedure SetActiveNavButton(const BtnId: string);
procedure ShowMapFocusUnit(const unitId: string);
procedure ShowMapFocusComplaint(const complaintId: string);
end; end;
var var
...@@ -290,4 +292,41 @@ begin ...@@ -290,4 +292,41 @@ begin
end; end;
procedure TFViewMain.ShowMapFocusUnit(const unitId: string);
var
pendingUnitId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingUnitId := unitId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusUnit(pendingUnitId);
end, 50);
end;
procedure TFViewMain.ShowMapFocusComplaint(const complaintId: string);
var
pendingComplaintId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingComplaintId := complaintId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusComplaint(pendingComplaintId);
end, 50);
end;
end. end.
...@@ -48,6 +48,18 @@ object FViewMap: TFViewMap ...@@ -48,6 +48,18 @@ object FViewMap: TFViewMap
HeadLinks = <> HeadLinks = <>
end end
end end
object btnFindLocation: TWebButton
Left = 90
Top = 74
Width = 61
Height = 25
Caption = 'Location'
ChildOrder = 1
ElementID = 'btn_find_location'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFindLocationClick
end
object httpReqGeoJson: TWebHttpRequest object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText ResponseType = rtText
URL = 'assets/bpddistricts-updated.geojson' URL = 'assets/bpddistricts-updated.geojson'
...@@ -66,4 +78,10 @@ object FViewMap: TFViewMap ...@@ -66,4 +78,10 @@ object FViewMap: TFViewMap
Left = 358 Left = 358
Top = 696 Top = 696
end end
object tmrLocate: TWebTimer
Enabled = False
Interval = 100
Left = 174
Top = 74
end
end end
<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">
<!-- New: Offcanvas --> <!-- New: offcavnvas is a bootstrap class that adds an easy slide in modal -->
<div class="offcanvas offcanvas-top" <div class="offcanvas offcanvas-end"
tabindex="-1" tabindex="-1"
id="map_filters_offcanvas" id="map_filters_offcanvas"
aria-labelledby="map_filters_offcanvas_label" aria-labelledby="map_filters_offcanvas_label"
...@@ -21,8 +21,11 @@ ...@@ -21,8 +21,11 @@
<input class="form-check-input" type="checkbox" id="map_filter_complaints" checked> <input class="form-check-input" type="checkbox" id="map_filter_complaints" checked>
<label class="form-check-label" for="map_filter_complaints">Show Complaints</label> <label class="form-check-label" for="map_filter_complaints">Show Complaints</label>
</div> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_location">
<label class="form-check-label" for="map_filter_location">Show Location</label>
</div>
</div> </div>
<div class="d-grid gap-2 mt-4"> <div class="d-grid gap-2 mt-4">
<button type="button" class="btn btn-primary" id="map_filters_apply" data-bs-dismiss="offcanvas"> <button type="button" class="btn btn-primary" id="map_filters_apply" data-bs-dismiss="offcanvas">
Apply Apply
...@@ -38,10 +41,19 @@ ...@@ -38,10 +41,19 @@
</div> </div>
</div> </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>
<!-- Locate (bottom-right) -->
<button id="btn_find_location"
type="button"
class="btn btn-primary border shadow position-absolute bottom-0 end-0 me-2 mb-4"
style="z-index:1000;">
<i class="fa fa-crosshairs"></i>
<span class="d-none d-sm-inline">Locate</span>
</button>
<!-- Filters (top-right) -->
<button id="btn_map_filters" <button id="btn_map_filters"
type="button" type="button"
class="btn btn-primary position-absolute top-0 end-0 m-2 shadow" class="btn btn-primary position-absolute top-0 end-0 m-2 shadow"
...@@ -49,9 +61,9 @@ ...@@ -49,9 +61,9 @@
data-bs-toggle="offcanvas" data-bs-toggle="offcanvas"
data-bs-target="#map_filters_offcanvas" data-bs-target="#map_filters_offcanvas"
aria-controls="map_filters_offcanvas"> aria-controls="map_filters_offcanvas">
<i class="fa fa-sliders-h me-1"></i>Filters <i class="fa fa-sliders-h"></i>
<span class="d-none d-sm-inline">Filters</span>
</button> </button>
</div> </div>
</div> </div>
...@@ -17,6 +17,8 @@ type ...@@ -17,6 +17,8 @@ type
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
xdwcMap: TXDataWebClient; xdwcMap: TXDataWebClient;
tmrRefresh: TWebTimer; tmrRefresh: TWebTimer;
btnFindLocation: TWebButton;
tmrLocate: TWebTimer;
procedure lfMapMapInitialized(Sender: TObject); procedure lfMapMapInitialized(Sender: TObject);
[async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
...@@ -24,6 +26,7 @@ type ...@@ -24,6 +26,7 @@ type
var ACustomizeMarker: string); var ACustomizeMarker: string);
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrRefreshTimer(Sender: TObject); procedure tmrRefreshTimer(Sender: TObject);
procedure btnFindLocationClick(Sender: TObject);
private private
userLocationMarker: TTMSFNCMapsMarker; userLocationMarker: TTMSFNCMapsMarker;
geoWatchId: Integer; geoWatchId: Integer;
...@@ -31,13 +34,20 @@ type ...@@ -31,13 +34,20 @@ type
FComplaintsLoaded: Boolean; FComplaintsLoaded: Boolean;
FZoomPending: Boolean; FZoomPending: Boolean;
FLoadingPoints: Boolean; FLoadingPoints: Boolean;
FDeviceCentered: Boolean;
mapFilters: TMapFilters; mapFilters: TMapFilters;
FPendingUnitId: string;
FPendingComplaintId: string;
[async] procedure LoadPointsAsync; [async] procedure LoadPointsAsync;
function CarIconForDistrict(const DistrictCode: string): string; function CarIconForDistrict(const DistrictCode: string): string;
procedure UpdateDeviceLocation(lat, lng: Double); procedure UpdateDeviceLocation(lat, lng: Double);
procedure StartDeviceLocation; procedure StartDeviceLocation;
procedure StopDeviceLocation; procedure StopDeviceLocation;
procedure ApplyPendingUnitFocus;
procedure ApplyPendingComplaintFocus;
public public
procedure FocusUnit(const unitId: string);
procedure FocusComplaint(const complaintId: string);
end; end;
var var
...@@ -55,8 +65,8 @@ begin ...@@ -55,8 +65,8 @@ begin
ShowSpinner('spinner'); ShowSpinner('spinner');
FUnitsLoaded := False; FUnitsLoaded := False;
FComplaintsLoaded := False; FComplaintsLoaded := False;
FDeviceCentered := False;
httpReqGeoJson.Execute; httpReqGeoJson.Execute;
{$IFNDEF WIN32}
asm asm
window.showComplaintDetails = function (id) { window.showComplaintDetails = function (id) {
console.log('JS bridge showComplaintDetails called, id=', id); console.log('JS bridge showComplaintDetails called, id=', id);
...@@ -76,8 +86,6 @@ begin ...@@ -76,8 +86,6 @@ begin
} }
}; };
end; end;
{$ENDIF}
StartDeviceLocation; StartDeviceLocation;
end; end;
...@@ -87,13 +95,15 @@ begin ...@@ -87,13 +95,15 @@ begin
if userLocationMarker = nil then if userLocationMarker = nil then
begin begin
userLocationMarker := lfMap.Markers.Add; userLocationMarker := lfMap.Markers.Add;
userLocationMarker.Title := '<div class="small fw-semibold">You are here</div>';
userLocationMarker.DataString := 'device'; userLocationMarker.DataString := 'device';
userLocationMarker.IconURL := 'assets/markers/location_dot.png'; userLocationMarker.IconURL := 'assets/markers/location_dot.png';
userLocationMarker.Title := ''; // keeps popup code from binding anything for this marker userLocationMarker.Visible := False;
end; end;
userLocationMarker.Latitude := lat; userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng; userLocationMarker.Longitude := lng;
end; end;
...@@ -152,7 +162,6 @@ begin ...@@ -152,7 +162,6 @@ begin
end; end;
[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var var
i: Integer; i: Integer;
...@@ -164,7 +173,7 @@ begin ...@@ -164,7 +173,7 @@ begin
lfMap.Polygons.Clear; lfMap.Polygons.Clear;
Console.Log('GeoJSON len=' + AResponse.Length.ToString); Console.Log('GeoJSON len=' + AResponse.Length.ToString);
lfMap.LoadGeoJSONFromText(AResponse, True, False); lfMap.LoadGeoJSONFromText(AResponse, True, Trim(FPendingUnitId) = '');
Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString); Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString);
for i := 0 to lfMap.Polygons.Count - 1 do for i := 0 to lfMap.Polygons.Count - 1 do
...@@ -301,7 +310,7 @@ begin ...@@ -301,7 +310,7 @@ begin
begin begin
m := lfMap.Markers[i]; m := lfMap.Markers[i];
if SameText(m.DataString, 'unit') or StartsText('complaint|', m.DataString) then if StartsText('unit|', m.DataString) or StartsText('complaint|', m.DataString) then
lfMap.Markers.Delete(i); lfMap.Markers.Delete(i);
end; end;
...@@ -409,7 +418,8 @@ begin ...@@ -409,7 +418,8 @@ begin
) + ) +
'</div>'; '</div>';
m.DataString := 'unit'; m.DataString := 'unit|' + unitId;
console.log('Unit marker ds=' + m.DataString);
m.IconURL := CarIconForDistrict(dist); m.IconURL := CarIconForDistrict(dist);
end; end;
end; end;
...@@ -512,6 +522,9 @@ begin ...@@ -512,6 +522,9 @@ begin
if mapFilters <> nil then if mapFilters <> nil then
mapFilters.Apply; mapFilters.Apply;
ApplyPendingUnitFocus;
ApplyPendingComplaintFocus;
finally finally
HideSpinner('spinner'); HideSpinner('spinner');
FLoadingPoints := False; FLoadingPoints := False;
...@@ -635,5 +648,92 @@ begin ...@@ -635,5 +648,92 @@ begin
LoadPointsAsync; LoadPointsAsync;
end; end;
procedure TFViewMap.btnFindLocationClick(Sender: TObject);
var
coord: TTMSFNCMapsCoordinateRec;
begin
if userLocationMarker = nil then
Exit;
coord := CreateCoordinate(userLocationMarker.Latitude, userLocationMarker.Longitude);
lfMap.SetCenterCoordinate(coord);
end;
procedure TFViewMap.FocusUnit(const unitId: string);
begin
FPendingUnitId := Trim(unitId);
if mapFilters <> nil then
LoadPointsAsync;
end;
procedure TFViewMap.FocusComplaint(const complaintId: string);
begin
FPendingComplaintId := Trim(complaintId);
if mapFilters <> nil then
LoadPointsAsync;
end;
procedure TFViewMap.ApplyPendingUnitFocus;
var
i: Integer;
m: TTMSFNCMapsMarker;
coord: TTMSFNCMapsCoordinateRec;
targetDs: string;
found: Boolean;
begin
if Trim(FPendingUnitId) = '' then
Exit;
targetDs := 'unit|' + FPendingUnitId;
found := False;
for i := 0 to lfMap.Markers.Count - 1 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, targetDs) then
begin
coord := CreateCoordinate(m.Latitude, m.Longitude);
lfMap.SetCenterCoordinate(coord);
found := True;
Break;
end;
end;
if found then
FPendingUnitId := '';
end;
procedure TFViewMap.ApplyPendingComplaintFocus;
var
i: Integer;
m: TTMSFNCMapsMarker;
coord: TTMSFNCMapsCoordinateRec;
targetDs: string;
found: Boolean;
begin
if Trim(FPendingComplaintId) = '' then
Exit;
targetDs := 'complaint|' + FPendingComplaintId;
found := False;
for i := 0 to lfMap.Markers.Count - 1 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, targetDs) then
begin
coord := CreateCoordinate(m.Latitude, m.Longitude);
lfMap.SetCenterCoordinate(coord);
found := True;
Break;
end;
end;
if found then
FPendingComplaintId := '';
end;
end. end.
...@@ -52,6 +52,20 @@ object FViewUnitDetails: TFViewUnitDetails ...@@ -52,6 +52,20 @@ object FViewUnitDetails: TFViewUnitDetails
end> end>
DataSource = wdsUnitLogs DataSource = wdsUnitLogs
end end
object btnUnitViewOnMap: TWebButton
Left = 370
Top = 326
Width = 96
Height = 25
Caption = 'View On Map'
ChildOrder = 1
ElementID = 'btn_unit_view_on_map'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnUnitViewOnMapClick
end
object xdwcUnitDetails: TXDataWebClient object xdwcUnitDetails: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 150 Left = 150
......
...@@ -87,5 +87,14 @@ ...@@ -87,5 +87,14 @@
</div> </div>
</div> </div>
<button id="btn_unit_view_on_map"
type="button"
class="btn btn-primary btn-sm shadow position-fixed"
style="right: 12px; bottom: 72px; z-index: 1040;">
View On Map
</button>
</div> </div>
...@@ -9,7 +9,8 @@ uses ...@@ -9,7 +9,8 @@ uses
ConnectionModule, ConnectionModule,
Utils, Utils,
Data.DB, WEBLib.DB, Data.DB, WEBLib.DB,
Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls; Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls, Vcl.StdCtrls,
WEBLib.StdCtrls;
type type
TFViewUnitDetails = class(TWebForm) TFViewUnitDetails = class(TWebForm)
...@@ -38,6 +39,8 @@ type ...@@ -38,6 +39,8 @@ type
xdwdsUnitLogsComplaint: TStringField; xdwdsUnitLogsComplaint: TStringField;
xdwdsUnitLogsLog: TStringField; xdwdsUnitLogsLog: TStringField;
xdwdsUnitLogsLogTime: TStringField; xdwdsUnitLogsLogTime: TStringField;
btnUnitViewOnMap: TWebButton;
procedure btnUnitViewOnMapClick(Sender: TObject);
private private
FUnitId: string; FUnitId: string;
FLoading: Boolean; FLoading: Boolean;
...@@ -67,6 +70,13 @@ uses ...@@ -67,6 +70,13 @@ uses
{$R *.dfm} {$R *.dfm}
procedure TFViewUnitDetails.btnUnitViewOnMapClick(Sender: TObject);
begin
if Assigned(FViewMain) then
FViewMain.ShowMapFocusUnit(FUnitId);
console.log('btnViewOnMapClick fired, FUnitId=' + FUnitId);
end;
class function TFViewUnitDetails.CreateForm(AElementID, UnitId: string): TWebForm; class function TFViewUnitDetails.CreateForm(AElementID, UnitId: string): TWebForm;
procedure AfterCreate(AForm: TObject); procedure AfterCreate(AForm: TObject);
......
...@@ -39,67 +39,26 @@ object FViewUnits: TFViewUnits ...@@ -39,67 +39,26 @@ object FViewUnits: TFViewUnits
Style = lsListGroup Style = lsListGroup
DataSource = wdsUnits DataSource = wdsUnits
ItemTemplate = ItemTemplate =
'<div class="list-section-header small fw-semibold bg-body-second' + '<div class="list-section-header small fw-semibold bg-body-secon' +
'ary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><di' + 'dary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><d' +
'v class="card border shadow-sm position-relative"> <div class="' + 'iv class="card border shadow-sm position-relative"> <div class=' +
'card-body py-2 px-3"> <!-- Unit + Status --> <div class="f' + '"card-body py-2 px-3"> <!-- Unit + Status --> <div class="' +
'w-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%St' + 'fw-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%S' +
'atus%) </div> <!-- Location --> <div class="small text-' + 'tatus%) </div> <!-- Location --> <div class="small text' +
'body-secondary mb-1"> (%Location%) </div> <!-- Call t' + '-body-secondary mb-1">(%Location%)</div> <!-- Call type --> ' +
'ype --> <div class="small">(%CallType%)</div> <!-- Divider' + ' <div class="small">(%CallType%)</div> <!-- Divider line -->' +
' line --> <hr class="unit-divider my-1" style="width:80px;mar' + ' <hr class="unit-divider my-1" style="width: 80px; margin-lef' +
'gin-left:0;"> <!-- Officers --> <div class="small officer1' + 't: 0" /> <!-- Officers --> <div class="small officer1">(%O' +
'">(%Officer1%)</div> <div class="small officer2">(%Officer2%)' + 'fficer1%)</div> <div class="small officer2">(%Officer2%)</div' +
'</div> </div> <!-- Vertically centered Details button on the r' + '> </div> <!-- Vertically centered Details button on the right ' +
'ight --> <div class="position-absolute top-50 end-0 translate-m' + '--> <div class="position-absolute top-50 end-0 translate-middle' +
'iddle-y pe-2"> <button type="button" class="btn bt' + '-y pe-2"> <button type="button" class="btn btn-prim' +
'n-primary btn-sm btn-unit-details" data-unitid="(%Uni' + 'ary btn-sm btn-unit-details" data-unitid="(%UnitId%)" > ' +
'tId%)"> Details </button> </div></div>' ' Details </button> <button type="button" class=' +
'"btn btn-secondary btn-sm btn-unit-map" data-unitid="(%Un' +
'itId%)"> View On Map </button> </div></div>'
ListSource = wdsUnits ListSource = wdsUnits
end end
object btnRefresh: TWebButton
Left = 110
Top = 82
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object btnGroup: TWebButton
Left = 186
Top = 82
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 250
Top = 82
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object wdsUnits: TWebDataSource object wdsUnits: TWebDataSource
AutoEdit = False AutoEdit = False
DataSet = xdwdsUnits DataSet = xdwdsUnits
......
...@@ -13,9 +13,6 @@ uses ...@@ -13,9 +13,6 @@ uses
type type
TFViewUnits = class(TWebForm) TFViewUnits = class(TWebForm)
dblUnitsList: TWebDBListControl; dblUnitsList: TWebDBListControl;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsUnits: TWebDataSource; wdsUnits: TWebDataSource;
xdwdsUnits: TXDataWebDataSet; xdwdsUnits: TXDataWebDataSet;
xdwcUnits: TXDataWebClient; xdwcUnits: TXDataWebClient;
...@@ -93,6 +90,29 @@ begin ...@@ -93,6 +90,29 @@ begin
asm asm
if (window.showUnitDetails) window.showUnitDetails(unitId); if (window.showUnitDetails) window.showUnitDetails(unitId);
end; end;
end
else
begin
btn := nil;
asm
btn = (el && el.closest) ? el.closest('.btn-unit-map') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
unitId := string(TJSHtmlElement(btn).getAttribute('data-unitid'));
e.preventDefault;
e.stopPropagation;
asm
try {
pas['View.Main'].FViewMain.ShowMapFocusUnit(unitId);
} catch (e) {
console.log('ShowMapFocusUnit failed', e);
}
end;
end;
end; end;
end; end;
...@@ -140,6 +160,7 @@ begin ...@@ -140,6 +160,7 @@ begin
end; end;
end; end;
finally finally
FLoading := False;
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
console.log('GetUnits complete'); console.log('GetUnits complete');
end; end;
......
...@@ -69,4 +69,6 @@ html, body { ...@@ -69,4 +69,6 @@ html, body {
overflow: hidden; overflow: hidden;
} }
.tab-hidden { display: none !important; }
...@@ -13,6 +13,8 @@ type ...@@ -13,6 +13,8 @@ type
FMap: TTMSFNCLeaflet; FMap: TTMSFNCLeaflet;
FShowUnits: Boolean; FShowUnits: Boolean;
FShowComplaints: Boolean; FShowComplaints: Boolean;
FShowLocation: Boolean;
function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean; function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
procedure SetCheckBoxChecked(const elementId: string; checked: Boolean); procedure SetCheckBoxChecked(const elementId: string; checked: Boolean);
...@@ -43,6 +45,7 @@ begin ...@@ -43,6 +45,7 @@ begin
FShowUnits := True; FShowUnits := True;
FShowComplaints := True; FShowComplaints := True;
FShowLocation := False;
end; end;
procedure TMapFilters.Init; procedure TMapFilters.Init;
...@@ -119,12 +122,14 @@ procedure TMapFilters.ReadUi; ...@@ -119,12 +122,14 @@ procedure TMapFilters.ReadUi;
begin begin
FShowUnits := GetCheckBoxChecked('map_filter_units', True); FShowUnits := GetCheckBoxChecked('map_filter_units', True);
FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True); FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True);
FShowLocation := GetCheckBoxChecked('map_filter_location', False);
end; end;
procedure TMapFilters.ResetUi; procedure TMapFilters.ResetUi;
begin begin
SetCheckBoxChecked('map_filter_units', True); SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True); SetCheckBoxChecked('map_filter_complaints', True);
SetCheckBoxChecked('map_filter_location', False);
end; end;
procedure TMapFilters.UpdateSummary; procedure TMapFilters.UpdateSummary;
...@@ -143,6 +148,13 @@ begin ...@@ -143,6 +148,13 @@ begin
summaryText := summaryText + 'Complaints'; summaryText := summaryText + 'Complaints';
end; end;
if FShowLocation then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Location';
end;
if summaryText = '' then if summaryText = '' then
summaryText := 'None'; summaryText := 'None';
...@@ -168,13 +180,15 @@ begin ...@@ -168,13 +180,15 @@ begin
// Note: Map form sets: // Note: Map form sets:
// - units: m.DataString := 'unit' // - units: m.DataString := 'unit'
// - complaints: (currently none) but you can set m.DataString := 'complaint|<id>' // - complaints: m.DataString := 'complaint'
showMarker := True; showMarker := True;
if SameText(ds, 'unit') then if SameText(ds, 'unit') then
showMarker := FShowUnits showMarker := FShowUnits
else if StartsText('complaint', LowerCase(ds)) then else if StartsText('complaint', LowerCase(ds)) then
showMarker := FShowComplaints; showMarker := FShowComplaints
else if SameText(ds, 'device') then
showMarker := FShowLocation;
// Note: TTMSFNCMapsMarker supports visibility toggling // Note: TTMSFNCMapsMarker supports visibility toggling
m.Visible := showMarker; m.Visible := showMarker;
......
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