Commit 1d985813 by Mac Stephens

big map update, added markers, also added timers to the complaints and units list for auto refresh.

parent 731ef776
......@@ -5,7 +5,7 @@ interface
uses
System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider,
PostgreSQLUniProvider, System.Variants, System.Generics.Collections, System.IniFiles,
Common.Logging, Vcl.Forms, OracleUniProvider;
Common.Logging, Vcl.Forms, OracleUniProvider, System.Character;
type
TApiDatabaseModule = class(TDataModule)
......@@ -14,9 +14,9 @@ type
UniQuery1: TUniQuery;
OracleUniProvider1: TOracleUniProvider;
uqBooking: TUniQuery;
uqUnitsCurrent: TUniQuery;
uqDISUnitsActive: TUniQuery;
uqCFSActive: TUniQuery;
uqMapUnits: TUniQuery;
uqUnitList: TUniQuery;
uqComplaintUnits: TUniQuery;
uqCFSMemos: TUniQuery;
uqComplaintList: TUniQuery;
uqComplaintDetails: TUniQuery;
......@@ -78,56 +78,69 @@ type
uqComplaintDetailsDATERESPONDED: TDateTimeField;
uqComplaintDetailsDATEARRIVED: TDateTimeField;
uqComplaintDetailsDATECLEARED: TDateTimeField;
uqCFSActiveCOMPLAINTID: TFloatField;
uqCFSActiveUNITID: TFloatField;
uqCFSActiveUNITNAME: TStringField;
uqCFSActiveDATEDISPATCHED: TDateTimeField;
uqCFSActiveDATERESPONDED: TDateTimeField;
uqCFSActiveDATEARRIVED: TDateTimeField;
uqCFSActiveDATECLEARED: TDateTimeField;
uqCFSActiveLOCATION: TStringField;
uqComplaintUnitsCOMPLAINTID: TFloatField;
uqComplaintUnitsUNITID: TFloatField;
uqComplaintUnitsUNITNAME: TStringField;
uqComplaintUnitsDATEDISPATCHED: TDateTimeField;
uqComplaintUnitsDATERESPONDED: TDateTimeField;
uqComplaintUnitsDATEARRIVED: TDateTimeField;
uqComplaintUnitsDATECLEARED: TDateTimeField;
uqComplaintUnitsLOCATION: TStringField;
uqCFSMemosMEMO_ID: TFloatField;
uqCFSMemosCFSID: TFloatField;
uqCFSMemosMEMO_TYPE: TFloatField;
uqCFSMemosTIMESTAMP: TDateTimeField;
uqCFSMemosBADGE_NUMBER: TStringField;
uqCFSMemosREMARKS: TStringField;
uqUnitsCurrentENTRYID: TFloatField;
uqUnitsCurrentUNITID: TFloatField;
uqUnitsCurrentUNITNAME: TStringField;
uqUnitsCurrentUNIT_DISTRICT: TStringField;
uqUnitsCurrentGPS_LATITUDE: TFloatField;
uqUnitsCurrentGPS_LONGITUDE: TFloatField;
uqDISUnitsActiveUNITID: TFloatField;
uqDISUnitsActiveUNITNAME: TStringField;
uqDISUnitsActiveCARNUMBER_DESC: TStringField;
uqDISUnitsActiveDISTRICT_DESC: TStringField;
uqDISUnitsActiveSECTOR_DESC: TStringField;
uqDISUnitsActiveOFFICER1_EMPNUM: TStringField;
uqDISUnitsActiveOFFICER1_LAST_NAME: TStringField;
uqDISUnitsActiveOFFICER1_FIRST_NAME: TStringField;
uqDISUnitsActiveOFFICER1_MI: TStringField;
uqDISUnitsActiveOFFICER2_EMPNUM: TStringField;
uqDISUnitsActiveOFFICER2_LAST_NAME: TStringField;
uqDISUnitsActiveOFFICER2_FIRST_NAME: TStringField;
uqDISUnitsActiveOFFICER2_MI: TStringField;
uqDISUnitsActiveLOCATION: TStringField;
uqDISUnitsActiveCOMPLAINT: TStringField;
uqDISUnitsActiveENTRYID: TFloatField;
uqDISUnitsActiveGPS_LATITUDE: TFloatField;
uqDISUnitsActiveGPS_LONGITUDE: TFloatField;
uqMapUnitsENTRYID: TFloatField;
uqMapUnitsUNITID: TFloatField;
uqMapUnitsUNITNAME: TStringField;
uqMapUnitsUNIT_DISTRICT: TStringField;
uqMapUnitsGPS_LATITUDE: TFloatField;
uqMapUnitsGPS_LONGITUDE: TFloatField;
uqComplaintListcomplaintNumber: TStringField;
uqComplaintListPRIORITY_COLOR: TFloatField;
uqComplaintListDISTRICT_DESC: TStringField;
uqComplaintListSECTOR_DESC: TStringField;
uqDISUnitsActiveUNITSTATUS: TFloatField;
uqDISUnitsActiveUNIT_STATUS_DESC: TStringField;
uqUnitListUNITID: TFloatField;
uqUnitListUNITNAME: TStringField;
uqUnitListCARNUMBER_DESC: TStringField;
uqUnitListDISTRICT_DESC: TStringField;
uqUnitListSECTOR_DESC: TStringField;
uqUnitListOFFICER1_EMPNUM: TStringField;
uqUnitListOFFICER1_LAST_NAME: TStringField;
uqUnitListOFFICER1_FIRST_NAME: TStringField;
uqUnitListOFFICER1_MI: TStringField;
uqUnitListOFFICER2_EMPNUM: TStringField;
uqUnitListOFFICER2_LAST_NAME: TStringField;
uqUnitListOFFICER2_FIRST_NAME: TStringField;
uqUnitListOFFICER2_MI: TStringField;
uqUnitListLOCATION: TStringField;
uqUnitListCOMPLAINT: TStringField;
uqUnitListUNITSTATUS: TFloatField;
uqUnitListUNIT_STATUS_DESC: TStringField;
uqUnitListENTRYID: TFloatField;
uqUnitListGPS_LATITUDE: TFloatField;
uqUnitListGPS_LONGITUDE: TFloatField;
uqUnitListCALL_TYPE: TStringField;
uqMapComplaints: TUniQuery;
uqMapComplaintsCOMPLAINTID: TFloatField;
uqMapComplaintsDISPATCHDISTRICT: TStringField;
uqMapComplaintsLNG: TFloatField;
uqMapComplaintsLAT: TFloatField;
uqMapComplaintsDISPATCH_CODE_DESC: TStringField;
uqMapComplaintsPRIORITY: TStringField;
uqMapComplaintsDISPATCHCODECATEGORY: TStringField;
uqMapComplaintspriorityKey: TStringField;
uqMapComplaintspngName: TStringField;
procedure DataModuleCreate(Sender: TObject);
procedure uqComplaintListCalcFields(DataSet: TDataSet);
procedure uqMapComplaintsCalcFields(DataSet: TDataSet);
private
{ Private declarations }
public
{ Public declarations }
function DerivePriorityKeyFromPriorityString(const priorityString: string): string;
function HandleUniqueFilenames(const category: string): string;
class procedure ExecSQL(const SQL: string);
end;
......@@ -214,4 +227,59 @@ begin
end;
procedure TApiDatabaseModule.uqMapComplaintsCalcFields(DataSet: TDataSet);
var
rawCategory: string;
rawPriority: string;
derivedPriorityKey: string;
computedPngName: string;
begin
rawCategory := DataSet.FieldByName('DISPATCHCODECATEGORY').AsString;
rawPriority := DataSet.FieldByName('PRIORITY').AsString;
derivedPriorityKey := DerivePriorityKeyFromPriorityString(rawPriority);
DataSet.FieldByName('priorityKey').AsString := derivedPriorityKey;
if Trim(rawCategory) = '' then
computedPngName := 'default.png'
else
computedPngName := Format('%s_%s.png', [HandleUniqueFilenames(rawCategory), derivedPriorityKey]);
DataSet.FieldByName('pngName').AsString := computedPngName;
end;
function TApiDatabaseModule.HandleUniqueFilenames(const category: string): string;
var
i: Integer;
ch: Char;
lowered, resultBuilder: string;
begin
lowered := Trim(LowerCase(category));
resultBuilder := '';
for i := 1 to Length(lowered) do
begin
ch := lowered[i];
if ch.IsLetterOrDigit then
resultBuilder := resultBuilder + ch
else
resultBuilder := resultBuilder + '_';
end;
Result := resultBuilder;
end;
function TApiDatabaseModule.DerivePriorityKeyFromPriorityString(const priorityString: string): string;
var
firstChar: Char;
begin
if priorityString <> '' then
begin
firstChar := priorityString[1]; // handles "3J", "3P", etc.
if (firstChar >= '1') and (firstChar <= '4') then
Exit(string(firstChar));
if (firstChar >= '5') and (firstChar <= '9') then
Exit('5-9');
end;
Result := '5-9';
end;
end.
......@@ -18,6 +18,8 @@ type
['{4FCB7FAF-44E5-49D6-9C0F-EE44BFB33313}']
[HttpGet] function GetComplaintList: TJSONObject;
[HttpGet] function GetUnitList: TJSONObject;
[HttpGet] function GetComplaintMap: TJSONObject;
[HttpGet] function GetUnitMap: TJSONObject;
end;
implementation
......
......@@ -19,6 +19,8 @@ type
public
function GetComplaintList: TJSONObject;
function GetUnitList: TJSONObject;
function GetComplaintMap: TJSONObject;
function GetUnitMap: TJSONObject;
end;
implementation
......@@ -40,6 +42,111 @@ begin
Logger.Log(3, 'ApiDatabaseModule destroyed');
end;
function TApiService.GetComplaintMap: TJSONObject;
var
data: TJSONArray;
emitted: Integer;
item: TJSONObject;
begin
Logger.Log(3, '---TApiService.GetComplaintMap initiated');
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
data := TJSONArray.Create;
try
emitted := 0;
with ApiDB.uqMapComplaints do
begin
Open;
First;
while not Eof do
begin
if ApiDB.uqMapComplaintsLAT.IsNull or ApiDB.uqMapComplaintsLNG.IsNull then
begin
Next;
Continue;
end;
item := TJSONObject.Create;
item.AddPair('ComplaintId', ApiDB.uqMapComplaintsCOMPLAINTID.AsString);
item.AddPair('DispatchDistrict', ApiDB.uqMapComplaintsDISPATCHDISTRICT.AsString);
item.AddPair('DispatchCodeDesc', ApiDB.uqMapComplaintsDISPATCH_CODE_DESC.AsString);
item.AddPair('DispatchCodeCategory', ApiDB.uqMapComplaintsDISPATCHCODECATEGORY.AsString);
item.AddPair('Priority', ApiDB.uqMapComplaintsPRIORITY.AsString);
item.AddPair('priorityKey', ApiDB.uqMapComplaints.FieldByName('priorityKey').AsString);
item.AddPair('pngName', ApiDB.uqMapComplaints.FieldByName('pngName').AsString);
item.AddPair('Lat', TJSONNumber.Create(ApiDB.uqMapComplaintsLAT.AsFloat));
item.AddPair('Lng', TJSONNumber.Create(ApiDB.uqMapComplaintsLNG.AsFloat));
data.AddElement(item);
Inc(emitted);
Next;
end;
end;
Result.AddPair('count', TJSONNumber.Create(data.Count));
Result.AddPair('returned', TJSONNumber.Create(emitted));
Result.AddPair('data', data);
except
data.Free;
Logger.Log(3, '---TApiService.GetComplaintMap End (error)');
raise EXDataHttpException.Create(500, 'Failed to load complaint map');
end;
Logger.Log(3, '---TApiService.GetComplaintMap End');
end;
function TApiService.GetUnitMap: TJSONObject;
var
data: TJSONArray;
begin
Logger.Log(3, '---TApiService.GetUnitMap initiated');
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
data := TJSONArray.Create;
try
with ApiDB.uqMapUnits do
begin
Open;
First;
while not Eof do
begin
// skip rows without coordinates
if (not FieldByName('GPS_LATITUDE').IsNull) and (not FieldByName('GPS_LONGITUDE').IsNull) then
begin
var item := TJSONObject.Create;
item.AddPair('UnitId', ApiDB.uqMapUnitsUNITID.AsString);
item.AddPair('UnitName', ApiDB.uqMapUnitsUNITNAME.AsString);
item.AddPair('District', ApiDB.uqMapUnitsUNIT_DISTRICT.AsString);
item.AddPair('Lat', TJSONNumber.Create(ApiDB.uqMapUnitsGPS_LATITUDE.AsFloat));
item.AddPair('Lng', TJSONNumber.Create(ApiDB.uqMapUnitsGPS_LONGITUDE.AsFloat));
data.AddElement(item);
end;
Next;
end;
end;
Result.AddPair('count', TJSONNumber.Create(data.Count));
Result.AddPair('returned', TJSONNumber.Create(data.Count));
Result.AddPair('data', data);
except
data.Free;
Logger.Log(3, '---TApiService.GetUnitMap End (error)');
raise EXDataHttpException.Create(500, 'Failed to load unit map');
end;
Logger.Log(3, '---TApiService.GetUnitMap End');
end;
function TApiService.GetComplaintList: TJSONObject;
var
data: TJSONArray;
......@@ -56,6 +163,7 @@ begin
with ApiDB.uqComplaintList do
begin
Open;
(FieldByName('DATEREPORTED') as TDateTimeField).DisplayFormat := 'yyyy-mm-dd hh:nn:ss';
First;
while not Eof do
begin
......@@ -132,7 +240,7 @@ begin
data := TJSONArray.Create;
try
lastDistrict := '';
with ApiDB.uqDISUnitsActive do
with ApiDB.uqUnitList do
begin
Open;
First;
......@@ -140,33 +248,38 @@ begin
begin
var item := TJSONObject.Create;
// Group header: show once when district changes (e.g., "1", "A")
var curDistrict := ApiDB.uqDISUnitsActiveDISTRICT_DESC.AsString;
if not SameText(curDistrict, lastDistrict) then
item.AddPair('DistrictHeader', curDistrict);
lastDistrict := curDistrict;
// Group header: show once when district changes
var curDistrict := ApiDB.uqUnitListDISTRICT_DESC.AsString;
var header := IfThen(curDistrict <> '', curDistrict + ' District', '');
if (header <> '') and not SameText(header, lastDistrict) then
item.AddPair('DistrictHeader', header);
lastDistrict := header;
// Core unit identity
item.AddPair('UnitId', ApiDB.uqDISUnitsActiveUNITID.AsString);
item.AddPair('UnitName', ApiDB.uqDISUnitsActiveUNITNAME.AsString);
item.AddPair('CarNumberDesc', ApiDB.uqDISUnitsActiveCARNUMBER_DESC.AsString);
item.AddPair('District', curDistrict);
item.AddPair('Sector', ApiDB.uqDISUnitsActiveSECTOR_DESC.AsString);
item.AddPair('UnitId', ApiDB.uqUnitListUNITID.AsString);
item.AddPair('UnitName', ApiDB.uqUnitListUNITNAME.AsString);
item.AddPair('CarNumberDesc', ApiDB.uqUnitListCARNUMBER_DESC.AsString);
item.AddPair('District', curDistrict);
item.AddPair('Sector', ApiDB.uqUnitListSECTOR_DESC.AsString);
item.AddPair('CallType', ApiDB.uqUnitListCALL_TYPE.AsString);
// Current assignment (if any)
item.AddPair('Location', ApiDB.uqDISUnitsActiveLOCATION.AsString);
item.AddPair('Complaint', ApiDB.uqDISUnitsActiveCOMPLAINT.AsString);
item.AddPair('Location', ApiDB.uqUnitListLOCATION.AsString);
item.AddPair('Complaint', ApiDB.uqUnitListCOMPLAINT.AsString);
// Status: default to "Available" when no active CFS row
var statusDesc := ApiDB.uqDISUnitsActiveUNIT_STATUS_DESC.AsString;
var statusDesc := ApiDB.uqUnitListUNIT_STATUS_DESC.AsString;
if statusDesc = '' then
statusDesc := 'Available';
item.AddPair('Status', statusDesc);
// Officers (LAST, FIRST [MI])
var o1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_LAST_NAME.AsString);
var f1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_FIRST_NAME.AsString);
var m1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_MI.AsString);
var o1 := Trim(ApiDB.uqUnitListOFFICER1_LAST_NAME.AsString);
var f1 := Trim(ApiDB.uqUnitListOFFICER1_FIRST_NAME.AsString);
var m1 := Trim(ApiDB.uqUnitListOFFICER1_MI.AsString);
if o1 <> '' then
begin
if f1 <> '' then o1 := o1 + ', ' + f1;
......@@ -174,9 +287,9 @@ begin
item.AddPair('Officer1', o1);
end;
var o2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_LAST_NAME.AsString);
var f2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_FIRST_NAME.AsString);
var m2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_MI.AsString);
var o2 := Trim(ApiDB.uqUnitListOFFICER2_LAST_NAME.AsString);
var f2 := Trim(ApiDB.uqUnitListOFFICER2_FIRST_NAME.AsString);
var m2 := Trim(ApiDB.uqUnitListOFFICER2_MI.AsString);
if o2 <> '' then
begin
if f2 <> '' then o2 := o2 + ', ' + f2;
......@@ -189,9 +302,9 @@ begin
end;
end;
Result.AddPair('count', TJSONNumber.Create(data.Count));
Result.AddPair('count', TJSONNumber.Create(data.Count));
Result.AddPair('returned', TJSONNumber.Create(data.Count));
Result.AddPair('data', data);
Result.AddPair('data', data);
except
data.Free;
Logger.Log(3, '---TApiService.GetUnitList End (error)');
......
......@@ -70,11 +70,4 @@ object FMain: TFMain
Left = 256
Top = 402
end
object tmrTwilio: TTimer
Enabled = False
Interval = 30000
OnTimer = tmrTwilioTimer
Left = 146
Top = 416
end
end
......@@ -18,11 +18,9 @@ type
initTimer: TTimer;
btnAuthSwaggerUI: TButton;
ExeInfo1: TExeInfo;
tmrTwilio: TTimer;
procedure btnApiSwaggerUIClick(Sender: TObject);
procedure btnDataClick(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure tmrTwilioTimer(Sender: TObject);
procedure ContactFormData(AText: String);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure initTimerTimer(Sender: TObject);
......@@ -130,12 +128,6 @@ begin
Logger.Log(1, LogValue('--Database->Password', IniEntries.DatabasePassword, IniEntries.DatabasePasswordFromIni));
Logger.Log(1, '');
Logger.Log(1, '--- Twilio ---');
Logger.Log(1, LogValue('--Twilio->AccountSID', IniEntries.TwilioSID, IniEntries.TwilioSIDFromIni));
Logger.Log(1, LogValue('--Twilio->AuthHeader', IniEntries.TwilioAuthHeader, IniEntries.TwilioAuthHeaderFromIni));
Logger.Log(1, '');
try
AuthServerModule := TAuthServerModule.Create(Self);
AuthServerModule.StartAuthServer(ServerConfig.url, AUTH_MODEL);
......@@ -145,19 +137,6 @@ begin
AppServerModule := TAppServerModule.Create(Self);
AppServerModule.StartAppServer(ServerConfig.url);
if IniEntries.TwilioUpdateTime > 0 then
begin
TwilioDataModule := TTwilioDataModule.Create(Self);
tmrTwilio.Interval := IniEntries.TwilioUpdateTime * 60000;
tmrTwilio.Enabled := True;
Logger.Log(1, Format('Twilio polling enabled every %d minutes.', [IniEntries.TwilioUpdateTime]));
end
else
begin
tmrTwilio.Enabled := False;
Logger.Log(1, 'Twilio polling disabled (TwilioUpdateTime = 0)');
end;
except
on E: Exception do
Logger.Log(2, 'Failed to start server modules: ' + E.Message);
......@@ -165,16 +144,5 @@ begin
end;
procedure TFMain.tmrTwilioTimer(Sender: TObject);
begin
tmrTwilio.Enabled := False;
Logger.Log(4, 'tmrTwilioTimer ---start');
TwilioDataModule := TTwilioDataModule.Create(Self);
TwilioDataModule.UpdateDB;
TwilioDataModule.Free;
Logger.Log(4, 'tmrTwilioTimer ---end (interval: ' + tmrTwilio.Interval.ToString + ' ms)');
tmrTwilio.Enabled := True;
end;
end.
[Settings]
LogFileNum=451
LogFileNum=484
webClientVersion=0.1.0
TwilioUpdateTime=0
[Database]
Server=192.168.102.130
--Server=192.168.198.129
--Server=192.168.75.133
Database=envoy_db
Username=postgres
Password=postgreSQL
......
unit Markers;
interface
function SvgPinDataURL(const Hex: string): string; // complaint: square badge + notch, fill = Hex
function SvgCarDataURL(const Hex: string): string; // unit: badge + notch with car pictogram, fill = Hex
implementation
uses
SysUtils;
function URLEncodeSVG(const S: string): string;
var
R: string;
begin
R := S;
R := StringReplace(R, '%', '%25', [rfReplaceAll]);
R := StringReplace(R, '#', '%23', [rfReplaceAll]);
R := StringReplace(R, '<', '%3C', [rfReplaceAll]);
R := StringReplace(R, '>', '%3E', [rfReplaceAll]);
R := StringReplace(R, '"', '%22', [rfReplaceAll]);
R := StringReplace(R, '''', '%27', [rfReplaceAll]);
R := StringReplace(R, ' ', '%20', [rfReplaceAll]);
Result := 'data:image/svg+xml;charset=utf-8,' + R;
end;
function NormalizeHex(const Hex: string; const Fallback: string): string;
var
c: string;
begin
c := Trim(Hex);
if (Length(c) = 7) and (c[1] = '#') then
Result := c
else
Result := Fallback;
end;
function ForegroundForFill(const Hex: string): string;
var
c: string;
r, g, b: Integer;
lum: Double;
begin
c := UpperCase(Hex);
if (Length(c) = 7) and (c[1] = '#') then
begin
r := StrToIntDef('$' + Copy(c, 2, 2), 0);
g := StrToIntDef('$' + Copy(c, 4, 2), 0);
b := StrToIntDef('$' + Copy(c, 6, 2), 0);
lum := 0.2126 * r + 0.7152 * g + 0.0722 * b; // 0..255
if lum > 170 then
Result := '#111111'
else
Result := '#FFFFFF';
end
else
Result := '#FFFFFF';
end;
function SvgPinDataURL(const Hex: string): string;
var
fill, svg: string;
begin
fill := NormalizeHex(Hex, '#2563EB');
svg :=
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 38">' +
'<rect x="1" y="1" rx="6" ry="6" width="28" height="28" fill="' + fill + '"/>' +
'<path d="M15,34 l6,-8 h-12 z" fill="' + fill + '"/>' +
'</svg>';
Result := URLEncodeSVG(svg);
end;
function SvgCarDataURL(const Hex: string): string;
var
fill, fg, svg: string;
begin
fill := NormalizeHex(Hex, '#2563EB');
fg := ForegroundForFill(fill);
svg :=
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 38">' +
'<rect x="1" y="1" rx="6" ry="6" width="28" height="28" fill="' + fill + '"/>' +
'<path d="M15,34 l6,-8 h-12 z" fill="' + fill + '"/>' +
// minimalist car pictogram (fits 28x28 body)
'<path fill="' + fg + '" d="M7 20h16l-2-6a3 3 0 0 0-2.8-2H11.8A3 3 0 0 0 9 14l-2 6zm-1 1h-1a1 1 0 0 0 0 2h1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2h8v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2h1a1 1 0 0 0 0-2h-1zm4-7h10l1 3H9l1-3zM10 22a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm10 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>' +
'</svg>';
Result := URLEncodeSVG(svg);
end;
end.
......@@ -61,37 +61,78 @@ begin
end;
// ...uses JS, Web...
var
GIsBusy: Boolean = False;
procedure ShowSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
SpinnerElement, Overlay: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then
Overlay := TJSHTMLElement(document.getElementById('screenlock'));
// ensure overlay exists
if Overlay = nil then
begin
// Move spinner to the <body> if it's not already there
asm
if (SpinnerElement.parentNode !== document.body) {
document.body.appendChild(SpinnerElement);
}
end;
Overlay := TJSHTMLElement(document.createElement('div'));
Overlay.id := 'screenlock';
document.body.appendChild(Overlay);
end;
// move both to <body> and show
if Assigned(Overlay) then
begin
asm if (Overlay.parentNode !== document.body) { document.body.appendChild(Overlay); } end;
Overlay.classList.remove('d-none');
Overlay.classList.add('d-block','position-fixed','top-0','start-0','w-100','h-100');
Overlay.style.setProperty('z-index','1060');
Overlay.style.setProperty('background','rgba(0,0,0,.15)');
Overlay.style.setProperty('pointer-events','auto');
end;
if Assigned(SpinnerElement) then
begin
asm if (SpinnerElement.parentNode !== document.body) { document.body.appendChild(SpinnerElement); } end;
SpinnerElement.classList.remove('d-none');
SpinnerElement.classList.add('d-block');
SpinnerElement.classList.add('d-block','position-fixed','top-50','start-50','translate-middle');
SpinnerElement.style.setProperty('z-index','1065');
end;
document.body.setAttribute('aria-busy','true');
GIsBusy := True;
end;
procedure HideSpinner(SpinnerID: string);
var
SpinnerElement: TJSHTMLElement;
SpinnerElement, Overlay: TJSHTMLElement;
begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
Overlay := TJSHTMLElement(document.getElementById('screenlock'));
if Assigned(SpinnerElement) then
begin
SpinnerElement.classList.remove('d-block');
SpinnerElement.classList.add('d-none');
end;
if Assigned(Overlay) then
begin
Overlay.classList.remove('d-block');
Overlay.classList.add('d-none');
end;
document.body.removeAttribute('aria-busy');
GIsBusy := False;
end;
function IsBusy: Boolean;
begin
Result := GIsBusy;
end;
procedure ShowErrorModal(msg: string);
begin
......
......@@ -84,16 +84,15 @@ object FViewComplaints: TFViewComplaints
Style = lsListGroup
DataSource = wdsComplaints
ItemTemplate =
'<div class="list-section-header small fw-semibold bg-body-second' +
'ary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><di' +
'v class="card border shadow-sm" style="--bs-card-bg:(%Priori' +
'tyColor%); --bs-card-color:(%PriorityTextColor%);"> <div class=' +
'"card-body py-2 px-3"> <div class="fw-bold text-uppercase sma' +
'll">(%Priority%): (%DispatchCodeDesc%)</div> <div class="smal' +
'l">(%Address%)</div> <div class="small text-opacity-75">(%Com' +
'plaint%): (%Status%)&nbsp;&nbsp;(%DistrictSector%)</div> <div' +
' class="small text-opacity-75">(%DateReported%)</div> </div></d' +
'iv>'
'<div class="list-section-header small fw-semibold bg-secondary t' +
'ext-white rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><div cl' +
'ass="card border shadow-sm" style="--bs-card-bg:(%PriorityCo' +
'lor%); --bs-card-color:(%PriorityTextColor%);"> <div class="car' +
'd-body py-2 px-3"> <div class="fw-bold text-uppercase small">' +
'(%Priority%): (%DispatchCodeDesc%)</div> <div class="small">(' +
'%Address%)</div> <div class="small text-opacity-75">(%Complai' +
'nt%): (%Status%)&nbsp;&nbsp;(%DistrictSector%)</div> <div cla' +
'ss="small text-opacity-75">(%DateReported%)</div> </div></div>'
ListSource = wdsComplaints
end
object xdwcComplaints: TXDataWebClient
......@@ -154,4 +153,10 @@ object FViewComplaints: TFViewComplaints
Left = 156
Top = 410
end
object tmrRefresh: TWebTimer
Interval = 30000
OnTimer = tmrRefreshTimer
Left = 164
Top = 44
end
end
......@@ -60,4 +60,3 @@
</div>
</div>
......@@ -34,9 +34,12 @@ type
xdwdsComplaintsPriorityColor: TStringField;
xdwdsComplaintsPriorityTextColor: TStringField;
xdwdsComplaintsDistrictSector: TStringField;
tmrRefresh: TWebTimer;
procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject);
private
FLoading: Boolean;
[async] procedure GetComplaints;
public
end;
......@@ -53,7 +56,10 @@ begin
console.log('WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected);
ShowSpinner('spinner');
tmrRefresh.Enabled := False;
GetComplaints;
tmrRefresh.Enabled := True;
end;
......@@ -69,8 +75,11 @@ var
respObj: TJSObject;
complaintsCount: Integer;
begin
if FLoading then Exit;
FLoading := True;
console.log('GetComplaints: Invoking API...');
Utils.ShowSpinner('spinner');
try
try
......@@ -107,11 +116,18 @@ begin
end;
end;
finally
Utils.HideSpinner('spinner');
console.log('GetComplaints complete');
end;
HideSpinner('spinner');
end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin
GetComplaints;
console.log('tmrRefreshTimer fired');
end;
end.
......@@ -46,10 +46,10 @@
<!-- Spinner -->
<div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller">
<div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div>
</div>
<div class="lds-roller">
<div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div>
</div>
</div>
<!-- Error modal -->
......
......@@ -82,11 +82,14 @@ object FViewMap: TFViewMap
Top = 0
Width = 335
Height = 555
AdaptToStyle = True
Align = alClient
ParentDoubleBuffered = False
DoubleBuffered = True
TabStop = False
TabOrder = 0
OnCustomizeCSS = lfMapCustomizeCSS
OnCustomizeMarker = lfMapCustomizeMarker
OnMapInitialized = lfMapMapInitialized
Polylines = <>
Polygons = <>
......@@ -102,16 +105,30 @@ object FViewMap: TFViewMap
#39'_blank'#39'>OpenStreetMap</a>'
HeatMaps = <>
LocalFileAccess = True
TileLayers = <>
TileLayers = <
item
URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
Opacity = 1.000000000000000000
end>
ElementContainers = <>
HeadLinks = <>
end
end
object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText
URL = 'assets/bpddistricts.geojson'
URL = 'assets/bpddistricts-updated.geojson'
OnResponse = httpReqGeoJsonResponse
Left = 114
Top = 696
end
object xdwcMap: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 232
Top = 696
end
object tmrRefresh: TWebTimer
Interval = 30000
Left = 358
Top = 696
end
end
object FViewUnits: TFViewUnits
Width = 359
Height = 480
ElementFont = efCSS
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
......@@ -38,15 +39,23 @@ object FViewUnits: TFViewUnits
Style = lsListGroup
DataSource = wdsUnits
ItemTemplate =
'<div class="list-section-header small fw-semibold bg-body-second' +
'ary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><di' +
'v class="card border shadow-sm"> <div class="card-body py-2 px-' +
'3"> <div class="d-flex justify-content-between align-items-ba' +
'seline"> <div class="fw-bold fs-6">(%UnitName%)</div> ' +
'<div class="small text-end text-body-secondary ms-3 text-truncat' +
'e">(%Location%)</div> </div> <div class="small">(%Status%)' +
'</div> <div class="small">(%Officer1%)</div> <div class="s' +
'mall">(%Officer2%)</div> </div></div>'
'<div class="list-section-header small fw-semibold bg-body-secon' +
'dary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><d' +
'iv class="card border shadow-sm position-relative"> <div class=' +
'"card-body py-2 px-3"> <!-- Unit + Status --> <div class="' +
'fw-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%S' +
'tatus%) </div> <!-- Location --> <div class="small text' +
'-body-secondary mb-1"> (%Location%) </div> <!-- Call ' +
'type --> <div class="small">(%CallType%)</div> <!-- Divide' +
'r line (we'#39'll auto-hide if no officers) --> <hr class="unit-d' +
'ivider my-1" style="width: 80px; margin-left: 0;"> <!-- Offic' +
'ers --> <div class="small officer1">(%Officer1%)</div> <di' +
'v class="small officer2">(%Officer2%)</div> </div> <!-- Floati' +
'ng details button (solid, info icon) --> <div class="position-a' +
'bsolute top-50 end-0 translate-middle-y pe-2"> <button class=' +
'"btn btn-secondary btn-sm btn-unit-details" data-unit' +
'id="(%UnitId%)"> <i class="fa fa-info"></i> </button> <' +
'/div></div>'
ListSource = wdsUnits
end
object btnRefresh: TWebButton
......@@ -123,10 +132,19 @@ object FViewUnits: TFViewUnits
object xdwdsUnitsOfficer2: TStringField
FieldName = 'Officer2'
end
object xdwdsUnitsCallType: TStringField
FieldName = 'CallType'
end
end
object xdwcUnits: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 58
Top = 410
end
object tmrRefresh: TWebTimer
Interval = 30000
OnTimer = tmrRefreshTimer
Left = 172
Top = 22
end
end
......@@ -27,9 +27,13 @@ type
xdwdsUnitsStatus: TStringField;
xdwdsUnitsOfficer1: TStringField;
xdwdsUnitsOfficer2: TStringField;
xdwdsUnitsCallType: TStringField;
tmrRefresh: TWebTimer;
procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject);
private
FLoading: Boolean;
[async] procedure GetUnits;
public
......@@ -43,14 +47,36 @@ implementation
{$R *.dfm}
procedure TFViewUnits.WebFormCreate(Sender: TObject);
begin
console.log('Units.WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected);
tmrRefresh.Enabled := False;
GetUnits;
tmrRefresh.Enabled := True;
{$IFNDEF WIN32}
asm
var root = pas.TFViewUnits(Self).dblUnitsList.ElementHandle;
if (root && !root.__emiDelegated) {
root.__emiDelegated = true;
root.addEventListener('click', function (e) {
// Look for a click on, or inside, the details button
var btn = e.target && e.target.closest('.btn-unit-details');
if (!btn || !root.contains(btn)) return;
e.preventDefault();
e.stopPropagation();
var unitId = btn.getAttribute('data-unitid') || '';
pas.TFViewUnits(Self).OpenUnitDetails(unitId);
}, { passive: true });
}
end;
{$ENDIF}
end;
procedure TFViewUnits.btnRefreshClick(Sender: TObject);
......@@ -64,6 +90,9 @@ var
respObj: TJSObject;
unitCount: Integer;
begin
if FLoading then Exit;
FLoading := True;
console.log('GetUnits: Invoking API...');
Utils.ShowSpinner('spinner');
try
......@@ -98,5 +127,10 @@ begin
end;
end;
procedure TFViewUnits.tmrRefreshTimer(Sender: TObject);
begin
GetUnits;
end;
end.
......@@ -158,3 +158,4 @@ span.card {
......@@ -6,7 +6,7 @@
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>emiMobile</title>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<!-- jQuery -->
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js" type="text/javascript"></script>
......@@ -19,17 +19,13 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" type="text/javascript"></script>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"/>
<!-- Leaflet -->
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" type="text/javascript"></script>
<link href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" rel="stylesheet"/>
<!-- App bundle -->
<!-- App -->
<script src="$(ProjectName).js" type="text/javascript"></script>
<!-- App styles -->
<style></style>
<link href="css/app.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<script type="text/javascript">rtl.run();</script>
......
......@@ -97,6 +97,7 @@
<TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger>
<VerInfo_Release>3</VerInfo_Release>
<TMSWebBrowser>3</TMSWebBrowser>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
......
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