Commit 1d985813 by Mac Stephens

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

parent 731ef776
...@@ -29,7 +29,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -29,7 +29,7 @@ object ApiDatabaseModule: TApiDatabaseModule
Left = 374 Left = 374
Top = 222 Top = 222
end end
object uqUnitsCurrent: TUniQuery object uqMapUnits: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
SQL.Strings = ( SQL.Strings = (
'SELECT' 'SELECT'
...@@ -41,30 +41,30 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -41,30 +41,30 @@ object ApiDatabaseModule: TApiDatabaseModule
' uc.GPS_LONGITUDE' ' uc.GPS_LONGITUDE'
'FROM UNITS_CURRENT@AVL_LINK uc') 'FROM UNITS_CURRENT@AVL_LINK uc')
ReadOnly = True ReadOnly = True
Left = 462 Left = 366
Top = 324 Top = 306
object uqUnitsCurrentENTRYID: TFloatField object uqMapUnitsENTRYID: TFloatField
FieldName = 'ENTRYID' FieldName = 'ENTRYID'
end end
object uqUnitsCurrentUNITID: TFloatField object uqMapUnitsUNITID: TFloatField
FieldName = 'UNITID' FieldName = 'UNITID'
end end
object uqUnitsCurrentUNITNAME: TStringField object uqMapUnitsUNITNAME: TStringField
FieldName = 'UNITNAME' FieldName = 'UNITNAME'
ReadOnly = True ReadOnly = True
end end
object uqUnitsCurrentUNIT_DISTRICT: TStringField object uqMapUnitsUNIT_DISTRICT: TStringField
FieldName = 'UNIT_DISTRICT' FieldName = 'UNIT_DISTRICT'
Size = 8 Size = 8
end end
object uqUnitsCurrentGPS_LATITUDE: TFloatField object uqMapUnitsGPS_LATITUDE: TFloatField
FieldName = 'GPS_LATITUDE' FieldName = 'GPS_LATITUDE'
end end
object uqUnitsCurrentGPS_LONGITUDE: TFloatField object uqMapUnitsGPS_LONGITUDE: TFloatField
FieldName = 'GPS_LONGITUDE' FieldName = 'GPS_LONGITUDE'
end end
end end
object uqDISUnitsActive: TUniQuery object uqUnitList: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
SQL.Strings = ( SQL.Strings = (
'SELECT dua.UNITID, dua.UNITNAME,' 'SELECT dua.UNITID, dua.UNITNAME,'
...@@ -83,10 +83,11 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -83,10 +83,11 @@ object ApiDatabaseModule: TApiDatabaseModule
' ca.COMPLAINT,' ' ca.COMPLAINT,'
' ca.UNITSTATUS,' ' ca.UNITSTATUS,'
' cus.CODE_DESC AS UNIT_STATUS_DESC,' ' cus.CODE_DESC AS UNIT_STATUS_DESC,'
' cdc.CODE_DESC AS CALL_TYPE,'
' uc.ENTRYID,' ' uc.ENTRYID,'
' uc.GPS_LATITUDE,' ' uc.GPS_LATITUDE,'
' uc.GPS_LONGITUDE' ' uc.GPS_LONGITUDE'
'FROM DIS_UNITS_ACTIVE dua' 'FROM DIS_UNIT_ACTIVE dua'
'LEFT JOIN CD_UNIT_NUMBER cun ON cun.AGENCYCODE = dua.CAR' + 'LEFT JOIN CD_UNIT_NUMBER cun ON cun.AGENCYCODE = dua.CAR' +
'NUMBER' 'NUMBER'
...@@ -109,106 +110,122 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -109,106 +110,122 @@ object ApiDatabaseModule: TApiDatabaseModule
'LEFT JOIN CD_UNITSTATUS cus ON ca.UNITSTATUS = cus.COD' + 'LEFT JOIN CD_UNITSTATUS cus ON ca.UNITSTATUS = cus.COD' +
'E' 'E'
'LEFT JOIN COMPLAINT_ACTIVE cap ON cap.COMPLAINTID = ca.COM' +
'PLAINTID'
'LEFT JOIN CD_DISPATCHCODES cdc ON cdc.CODE = cap.DI' +
'SPATCHCODE'
'LEFT JOIN UNITS_CURRENT@AVL_LINK uc ON dua.UNITID = uc.UNIT' + 'LEFT JOIN UNITS_CURRENT@AVL_LINK uc ON dua.UNITID = uc.UNIT' +
'ID' 'ID'
'FETCH FIRST 100 ROWS ONLY') 'ORDER BY'
' CASE WHEN LENGTH(cd.CODE_DESC) = 1 THEN 0 ELSE 1 END,'
' cd.CODE_DESC,'
' dua.UNITNAME'
''
'')
ReadOnly = True ReadOnly = True
Left = 462 Left = 446
Top = 374 Top = 394
object uqDISUnitsActiveUNITID: TFloatField object uqUnitListUNITID: TFloatField
FieldName = 'UNITID' FieldName = 'UNITID'
end end
object uqDISUnitsActiveUNITNAME: TStringField object uqUnitListUNITNAME: TStringField
FieldName = 'UNITNAME' FieldName = 'UNITNAME'
Size = 10 Size = 10
end end
object uqDISUnitsActiveCARNUMBER_DESC: TStringField object uqUnitListCARNUMBER_DESC: TStringField
FieldName = 'CARNUMBER_DESC' FieldName = 'CARNUMBER_DESC'
ReadOnly = True ReadOnly = True
Size = 120 Size = 120
end end
object uqDISUnitsActiveDISTRICT_DESC: TStringField object uqUnitListDISTRICT_DESC: TStringField
FieldName = 'DISTRICT_DESC' FieldName = 'DISTRICT_DESC'
ReadOnly = True ReadOnly = True
Size = 120 Size = 120
end end
object uqDISUnitsActiveSECTOR_DESC: TStringField object uqUnitListSECTOR_DESC: TStringField
FieldName = 'SECTOR_DESC' FieldName = 'SECTOR_DESC'
ReadOnly = True ReadOnly = True
Size = 120 Size = 120
end end
object uqDISUnitsActiveOFFICER1_EMPNUM: TStringField object uqUnitListOFFICER1_EMPNUM: TStringField
FieldName = 'OFFICER1_EMPNUM' FieldName = 'OFFICER1_EMPNUM'
ReadOnly = True ReadOnly = True
Size = 10 Size = 10
end end
object uqDISUnitsActiveOFFICER1_LAST_NAME: TStringField object uqUnitListOFFICER1_LAST_NAME: TStringField
FieldName = 'OFFICER1_LAST_NAME' FieldName = 'OFFICER1_LAST_NAME'
ReadOnly = True ReadOnly = True
Size = 45 Size = 45
end end
object uqDISUnitsActiveOFFICER1_FIRST_NAME: TStringField object uqUnitListOFFICER1_FIRST_NAME: TStringField
FieldName = 'OFFICER1_FIRST_NAME' FieldName = 'OFFICER1_FIRST_NAME'
ReadOnly = True ReadOnly = True
Size = 30 Size = 30
end end
object uqDISUnitsActiveOFFICER1_MI: TStringField object uqUnitListOFFICER1_MI: TStringField
FieldName = 'OFFICER1_MI' FieldName = 'OFFICER1_MI'
ReadOnly = True ReadOnly = True
Size = 1 Size = 1
end end
object uqDISUnitsActiveOFFICER2_EMPNUM: TStringField object uqUnitListOFFICER2_EMPNUM: TStringField
FieldName = 'OFFICER2_EMPNUM' FieldName = 'OFFICER2_EMPNUM'
ReadOnly = True ReadOnly = True
Size = 10 Size = 10
end end
object uqDISUnitsActiveOFFICER2_LAST_NAME: TStringField object uqUnitListOFFICER2_LAST_NAME: TStringField
FieldName = 'OFFICER2_LAST_NAME' FieldName = 'OFFICER2_LAST_NAME'
ReadOnly = True ReadOnly = True
Size = 45 Size = 45
end end
object uqDISUnitsActiveOFFICER2_FIRST_NAME: TStringField object uqUnitListOFFICER2_FIRST_NAME: TStringField
FieldName = 'OFFICER2_FIRST_NAME' FieldName = 'OFFICER2_FIRST_NAME'
ReadOnly = True ReadOnly = True
Size = 30 Size = 30
end end
object uqDISUnitsActiveOFFICER2_MI: TStringField object uqUnitListOFFICER2_MI: TStringField
FieldName = 'OFFICER2_MI' FieldName = 'OFFICER2_MI'
ReadOnly = True ReadOnly = True
Size = 1 Size = 1
end end
object uqDISUnitsActiveLOCATION: TStringField object uqUnitListLOCATION: TStringField
FieldName = 'LOCATION' FieldName = 'LOCATION'
ReadOnly = True ReadOnly = True
Size = 30 Size = 30
end end
object uqDISUnitsActiveCOMPLAINT: TStringField object uqUnitListCOMPLAINT: TStringField
FieldName = 'COMPLAINT' FieldName = 'COMPLAINT'
ReadOnly = True ReadOnly = True
Size = 10 Size = 10
end end
object uqDISUnitsActiveENTRYID: TFloatField object uqUnitListUNITSTATUS: TFloatField
FieldName = 'UNITSTATUS'
ReadOnly = True
end
object uqUnitListUNIT_STATUS_DESC: TStringField
FieldName = 'UNIT_STATUS_DESC'
ReadOnly = True
end
object uqUnitListENTRYID: TFloatField
FieldName = 'ENTRYID' FieldName = 'ENTRYID'
ReadOnly = True ReadOnly = True
end end
object uqDISUnitsActiveGPS_LATITUDE: TFloatField object uqUnitListGPS_LATITUDE: TFloatField
FieldName = 'GPS_LATITUDE' FieldName = 'GPS_LATITUDE'
ReadOnly = True ReadOnly = True
end end
object uqDISUnitsActiveGPS_LONGITUDE: TFloatField object uqUnitListGPS_LONGITUDE: TFloatField
FieldName = 'GPS_LONGITUDE' FieldName = 'GPS_LONGITUDE'
ReadOnly = True ReadOnly = True
end end
object uqDISUnitsActiveUNITSTATUS: TFloatField object uqUnitListCALL_TYPE: TStringField
FieldName = 'UNITSTATUS' FieldName = 'CALL_TYPE'
ReadOnly = True
end
object uqDISUnitsActiveUNIT_STATUS_DESC: TStringField
FieldName = 'UNIT_STATUS_DESC'
ReadOnly = True ReadOnly = True
Size = 60
end end
end end
object uqCFSActive: TUniQuery object uqComplaintUnits: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
SQL.Strings = ( SQL.Strings = (
'SELECT' 'SELECT'
...@@ -224,37 +241,37 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -224,37 +241,37 @@ object ApiDatabaseModule: TApiDatabaseModule
'WHERE ca.COMPLAINTID = :COMPLAINTID' 'WHERE ca.COMPLAINTID = :COMPLAINTID'
'ORDER BY ca.DATEDISPATCHED') 'ORDER BY ca.DATEDISPATCHED')
ReadOnly = True ReadOnly = True
Left = 278 Left = 256
Top = 318 Top = 308
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'COMPLAINTID' Name = 'COMPLAINTID'
Value = Null Value = Null
end> end>
object uqCFSActiveCOMPLAINTID: TFloatField object uqComplaintUnitsCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID' FieldName = 'COMPLAINTID'
end end
object uqCFSActiveUNITID: TFloatField object uqComplaintUnitsUNITID: TFloatField
FieldName = 'UNITID' FieldName = 'UNITID'
end end
object uqCFSActiveUNITNAME: TStringField object uqComplaintUnitsUNITNAME: TStringField
FieldName = 'UNITNAME' FieldName = 'UNITNAME'
Size = 10 Size = 10
end end
object uqCFSActiveDATEDISPATCHED: TDateTimeField object uqComplaintUnitsDATEDISPATCHED: TDateTimeField
FieldName = 'DATEDISPATCHED' FieldName = 'DATEDISPATCHED'
end end
object uqCFSActiveDATERESPONDED: TDateTimeField object uqComplaintUnitsDATERESPONDED: TDateTimeField
FieldName = 'DATERESPONDED' FieldName = 'DATERESPONDED'
end end
object uqCFSActiveDATEARRIVED: TDateTimeField object uqComplaintUnitsDATEARRIVED: TDateTimeField
FieldName = 'DATEARRIVED' FieldName = 'DATEARRIVED'
end end
object uqCFSActiveDATECLEARED: TDateTimeField object uqComplaintUnitsDATECLEARED: TDateTimeField
FieldName = 'DATECLEARED' FieldName = 'DATECLEARED'
end end
object uqCFSActiveLOCATION: TStringField object uqComplaintUnitsLOCATION: TStringField
FieldName = 'LOCATION' FieldName = 'LOCATION'
Size = 30 Size = 30
end end
...@@ -273,8 +290,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -273,8 +290,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'WHERE cm.CFSID = :CFSID' 'WHERE cm.CFSID = :CFSID'
'ORDER BY cm.TIMESTAMP ASC') 'ORDER BY cm.TIMESTAMP ASC')
ReadOnly = True ReadOnly = True
Left = 282 Left = 284
Top = 376 Top = 388
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
...@@ -335,7 +352,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -335,7 +352,7 @@ object ApiDatabaseModule: TApiDatabaseModule
' ct.DATERESPONDED,' ' ct.DATERESPONDED,'
' ct.DATEARRIVED,' ' ct.DATEARRIVED,'
' ct.DATECLEARED,' ' ct.DATECLEARED,'
' cp.COLOR AS PRIORITY_COLOR,' ' cp.MOBILE_COLOR AS PRIORITY_COLOR,'
' cd.CODE_DESC AS DISTRICT_DESC,' ' cd.CODE_DESC AS DISTRICT_DESC,'
' cs.CODE_DESC AS SECTOR_DESC' ' cs.CODE_DESC AS SECTOR_DESC'
'FROM COMPLAINT_ACTIVE ca' 'FROM COMPLAINT_ACTIVE ca'
...@@ -355,7 +372,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -355,7 +372,7 @@ object ApiDatabaseModule: TApiDatabaseModule
'ORDER BY ca.DISPATCHDISTRICT, ct.DATEREPORTED DESC, ca.PRIORITY ' + 'ORDER BY ca.DISPATCHDISTRICT, ct.DATEREPORTED DESC, ca.PRIORITY ' +
'DESC' 'DESC'
'FETCH FIRST 10 ROWS ONLY') '')
ReadOnly = True ReadOnly = True
OnCalcFields = uqComplaintListCalcFields OnCalcFields = uqComplaintListCalcFields
Left = 76 Left = 76
...@@ -651,10 +668,92 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -651,10 +668,92 @@ object ApiDatabaseModule: TApiDatabaseModule
Port = 1521 Port = 1521
Username = 'ENTCAD' Username = 'ENTCAD'
Server = 'BUFENTCAD' Server = 'BUFENTCAD'
Connected = True
LoginPrompt = False LoginPrompt = False
Left = 76 Left = 76
Top = 244 Top = 244
EncryptedPassword = 'BAFFB1FFABFFBCFFBEFFBBFF' EncryptedPassword = 'BAFFB1FFABFFBCFFBEFFBBFF'
end end
object uqMapComplaints: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'SELECT'
' ca.COMPLAINTID,'
' ca.DISPATCHDISTRICT,'
' ca.PRIORITY AS PRIORITY,'
' cdc.MOBILE_MAP_CATEGORY AS DISPATCHCODECATEGORY,'
''
' CASE '
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YC' +
'OORD, NULL), NULL, NULL),'
' 4326'
' ).sdo_point.x'
' END AS LNG,'
''
' CASE '
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YC' +
'OORD, NULL), NULL, NULL),'
' 4326'
' ).sdo_point.y'
' END AS LAT,'
''
' cdc.CODE_DESC AS DISPATCH_CODE_DESC'
'FROM COMPLAINT_ACTIVE ca'
'JOIN COMPLAINT_TIMES ct'
' ON ct.COMPLAINTID = ca.COMPLAINTID'
'LEFT JOIN CD_DISPATCHCODES cdc'
' ON cdc.CODE = ca.DISPATCHCODE'
'WHERE ca.COMPLAINT IS NOT NULL '
' AND ca.XCOORD IS NOT NULL '
' AND ca.YCOORD IS NOT NULL;'
''
'')
ReadOnly = True
OnCalcFields = uqMapComplaintsCalcFields
Left = 476
Top = 308
object uqMapComplaintsCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID'
Required = True
end
object uqMapComplaintsDISPATCHDISTRICT: TStringField
FieldName = 'DISPATCHDISTRICT'
Size = 6
end
object uqMapComplaintsLNG: TFloatField
FieldName = 'LNG'
end
object uqMapComplaintsLAT: TFloatField
FieldName = 'LAT'
end
object uqMapComplaintsDISPATCH_CODE_DESC: TStringField
FieldName = 'DISPATCH_CODE_DESC'
ReadOnly = True
Size = 60
end
object uqMapComplaintsPRIORITY: TStringField
FieldName = 'PRIORITY'
Size = 6
end
object uqMapComplaintsDISPATCHCODECATEGORY: TStringField
FieldName = 'DISPATCHCODECATEGORY'
ReadOnly = True
Size = 50
end
object uqMapComplaintspriorityKey: TStringField
FieldKind = fkCalculated
FieldName = 'priorityKey'
Calculated = True
end
object uqMapComplaintspngName: TStringField
FieldKind = fkCalculated
FieldName = 'pngName'
Calculated = True
end
end
end end
...@@ -5,7 +5,7 @@ interface ...@@ -5,7 +5,7 @@ interface
uses uses
System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider, System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider,
PostgreSQLUniProvider, System.Variants, System.Generics.Collections, System.IniFiles, PostgreSQLUniProvider, System.Variants, System.Generics.Collections, System.IniFiles,
Common.Logging, Vcl.Forms, OracleUniProvider; Common.Logging, Vcl.Forms, OracleUniProvider, System.Character;
type type
TApiDatabaseModule = class(TDataModule) TApiDatabaseModule = class(TDataModule)
...@@ -14,9 +14,9 @@ type ...@@ -14,9 +14,9 @@ type
UniQuery1: TUniQuery; UniQuery1: TUniQuery;
OracleUniProvider1: TOracleUniProvider; OracleUniProvider1: TOracleUniProvider;
uqBooking: TUniQuery; uqBooking: TUniQuery;
uqUnitsCurrent: TUniQuery; uqMapUnits: TUniQuery;
uqDISUnitsActive: TUniQuery; uqUnitList: TUniQuery;
uqCFSActive: TUniQuery; uqComplaintUnits: TUniQuery;
uqCFSMemos: TUniQuery; uqCFSMemos: TUniQuery;
uqComplaintList: TUniQuery; uqComplaintList: TUniQuery;
uqComplaintDetails: TUniQuery; uqComplaintDetails: TUniQuery;
...@@ -78,56 +78,69 @@ type ...@@ -78,56 +78,69 @@ type
uqComplaintDetailsDATERESPONDED: TDateTimeField; uqComplaintDetailsDATERESPONDED: TDateTimeField;
uqComplaintDetailsDATEARRIVED: TDateTimeField; uqComplaintDetailsDATEARRIVED: TDateTimeField;
uqComplaintDetailsDATECLEARED: TDateTimeField; uqComplaintDetailsDATECLEARED: TDateTimeField;
uqCFSActiveCOMPLAINTID: TFloatField; uqComplaintUnitsCOMPLAINTID: TFloatField;
uqCFSActiveUNITID: TFloatField; uqComplaintUnitsUNITID: TFloatField;
uqCFSActiveUNITNAME: TStringField; uqComplaintUnitsUNITNAME: TStringField;
uqCFSActiveDATEDISPATCHED: TDateTimeField; uqComplaintUnitsDATEDISPATCHED: TDateTimeField;
uqCFSActiveDATERESPONDED: TDateTimeField; uqComplaintUnitsDATERESPONDED: TDateTimeField;
uqCFSActiveDATEARRIVED: TDateTimeField; uqComplaintUnitsDATEARRIVED: TDateTimeField;
uqCFSActiveDATECLEARED: TDateTimeField; uqComplaintUnitsDATECLEARED: TDateTimeField;
uqCFSActiveLOCATION: TStringField; uqComplaintUnitsLOCATION: TStringField;
uqCFSMemosMEMO_ID: TFloatField; uqCFSMemosMEMO_ID: TFloatField;
uqCFSMemosCFSID: TFloatField; uqCFSMemosCFSID: TFloatField;
uqCFSMemosMEMO_TYPE: TFloatField; uqCFSMemosMEMO_TYPE: TFloatField;
uqCFSMemosTIMESTAMP: TDateTimeField; uqCFSMemosTIMESTAMP: TDateTimeField;
uqCFSMemosBADGE_NUMBER: TStringField; uqCFSMemosBADGE_NUMBER: TStringField;
uqCFSMemosREMARKS: TStringField; uqCFSMemosREMARKS: TStringField;
uqUnitsCurrentENTRYID: TFloatField; uqMapUnitsENTRYID: TFloatField;
uqUnitsCurrentUNITID: TFloatField; uqMapUnitsUNITID: TFloatField;
uqUnitsCurrentUNITNAME: TStringField; uqMapUnitsUNITNAME: TStringField;
uqUnitsCurrentUNIT_DISTRICT: TStringField; uqMapUnitsUNIT_DISTRICT: TStringField;
uqUnitsCurrentGPS_LATITUDE: TFloatField; uqMapUnitsGPS_LATITUDE: TFloatField;
uqUnitsCurrentGPS_LONGITUDE: TFloatField; uqMapUnitsGPS_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;
uqComplaintListcomplaintNumber: TStringField; uqComplaintListcomplaintNumber: TStringField;
uqComplaintListPRIORITY_COLOR: TFloatField; uqComplaintListPRIORITY_COLOR: TFloatField;
uqComplaintListDISTRICT_DESC: TStringField; uqComplaintListDISTRICT_DESC: TStringField;
uqComplaintListSECTOR_DESC: TStringField; uqComplaintListSECTOR_DESC: TStringField;
uqDISUnitsActiveUNITSTATUS: TFloatField; uqUnitListUNITID: TFloatField;
uqDISUnitsActiveUNIT_STATUS_DESC: TStringField; 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 DataModuleCreate(Sender: TObject);
procedure uqComplaintListCalcFields(DataSet: TDataSet); procedure uqComplaintListCalcFields(DataSet: TDataSet);
procedure uqMapComplaintsCalcFields(DataSet: TDataSet);
private private
{ Private declarations } { Private declarations }
public public
{ Public declarations } function DerivePriorityKeyFromPriorityString(const priorityString: string): string;
function HandleUniqueFilenames(const category: string): string;
class procedure ExecSQL(const SQL: string); class procedure ExecSQL(const SQL: string);
end; end;
...@@ -214,4 +227,59 @@ begin ...@@ -214,4 +227,59 @@ begin
end; 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. end.
...@@ -18,6 +18,8 @@ type ...@@ -18,6 +18,8 @@ type
['{4FCB7FAF-44E5-49D6-9C0F-EE44BFB33313}'] ['{4FCB7FAF-44E5-49D6-9C0F-EE44BFB33313}']
[HttpGet] function GetComplaintList: TJSONObject; [HttpGet] function GetComplaintList: TJSONObject;
[HttpGet] function GetUnitList: TJSONObject; [HttpGet] function GetUnitList: TJSONObject;
[HttpGet] function GetComplaintMap: TJSONObject;
[HttpGet] function GetUnitMap: TJSONObject;
end; end;
implementation implementation
......
...@@ -19,6 +19,8 @@ type ...@@ -19,6 +19,8 @@ type
public public
function GetComplaintList: TJSONObject; function GetComplaintList: TJSONObject;
function GetUnitList: TJSONObject; function GetUnitList: TJSONObject;
function GetComplaintMap: TJSONObject;
function GetUnitMap: TJSONObject;
end; end;
implementation implementation
...@@ -40,6 +42,111 @@ begin ...@@ -40,6 +42,111 @@ begin
Logger.Log(3, 'ApiDatabaseModule destroyed'); Logger.Log(3, 'ApiDatabaseModule destroyed');
end; 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; function TApiService.GetComplaintList: TJSONObject;
var var
data: TJSONArray; data: TJSONArray;
...@@ -56,6 +163,7 @@ begin ...@@ -56,6 +163,7 @@ begin
with ApiDB.uqComplaintList do with ApiDB.uqComplaintList do
begin begin
Open; Open;
(FieldByName('DATEREPORTED') as TDateTimeField).DisplayFormat := 'yyyy-mm-dd hh:nn:ss';
First; First;
while not Eof do while not Eof do
begin begin
...@@ -132,7 +240,7 @@ begin ...@@ -132,7 +240,7 @@ begin
data := TJSONArray.Create; data := TJSONArray.Create;
try try
lastDistrict := ''; lastDistrict := '';
with ApiDB.uqDISUnitsActive do with ApiDB.uqUnitList do
begin begin
Open; Open;
First; First;
...@@ -140,33 +248,38 @@ begin ...@@ -140,33 +248,38 @@ begin
begin begin
var item := TJSONObject.Create; var item := TJSONObject.Create;
// Group header: show once when district changes (e.g., "1", "A")
var curDistrict := ApiDB.uqDISUnitsActiveDISTRICT_DESC.AsString; // Group header: show once when district changes
if not SameText(curDistrict, lastDistrict) then var curDistrict := ApiDB.uqUnitListDISTRICT_DESC.AsString;
item.AddPair('DistrictHeader', curDistrict); var header := IfThen(curDistrict <> '', curDistrict + ' District', '');
lastDistrict := curDistrict; if (header <> '') and not SameText(header, lastDistrict) then
item.AddPair('DistrictHeader', header);
lastDistrict := header;
// Core unit identity // Core unit identity
item.AddPair('UnitId', ApiDB.uqDISUnitsActiveUNITID.AsString); item.AddPair('UnitId', ApiDB.uqUnitListUNITID.AsString);
item.AddPair('UnitName', ApiDB.uqDISUnitsActiveUNITNAME.AsString); item.AddPair('UnitName', ApiDB.uqUnitListUNITNAME.AsString);
item.AddPair('CarNumberDesc', ApiDB.uqDISUnitsActiveCARNUMBER_DESC.AsString); item.AddPair('CarNumberDesc', ApiDB.uqUnitListCARNUMBER_DESC.AsString);
item.AddPair('District', curDistrict); item.AddPair('District', curDistrict);
item.AddPair('Sector', ApiDB.uqDISUnitsActiveSECTOR_DESC.AsString); item.AddPair('Sector', ApiDB.uqUnitListSECTOR_DESC.AsString);
item.AddPair('CallType', ApiDB.uqUnitListCALL_TYPE.AsString);
// Current assignment (if any) // Current assignment (if any)
item.AddPair('Location', ApiDB.uqDISUnitsActiveLOCATION.AsString); item.AddPair('Location', ApiDB.uqUnitListLOCATION.AsString);
item.AddPair('Complaint', ApiDB.uqDISUnitsActiveCOMPLAINT.AsString); item.AddPair('Complaint', ApiDB.uqUnitListCOMPLAINT.AsString);
// Status: default to "Available" when no active CFS row // 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 if statusDesc = '' then
statusDesc := 'Available'; statusDesc := 'Available';
item.AddPair('Status', statusDesc); item.AddPair('Status', statusDesc);
// Officers (LAST, FIRST [MI]) // Officers (LAST, FIRST [MI])
var o1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_LAST_NAME.AsString); var o1 := Trim(ApiDB.uqUnitListOFFICER1_LAST_NAME.AsString);
var f1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_FIRST_NAME.AsString); var f1 := Trim(ApiDB.uqUnitListOFFICER1_FIRST_NAME.AsString);
var m1 := Trim(ApiDB.uqDISUnitsActiveOFFICER1_MI.AsString); var m1 := Trim(ApiDB.uqUnitListOFFICER1_MI.AsString);
if o1 <> '' then if o1 <> '' then
begin begin
if f1 <> '' then o1 := o1 + ', ' + f1; if f1 <> '' then o1 := o1 + ', ' + f1;
...@@ -174,9 +287,9 @@ begin ...@@ -174,9 +287,9 @@ begin
item.AddPair('Officer1', o1); item.AddPair('Officer1', o1);
end; end;
var o2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_LAST_NAME.AsString); var o2 := Trim(ApiDB.uqUnitListOFFICER2_LAST_NAME.AsString);
var f2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_FIRST_NAME.AsString); var f2 := Trim(ApiDB.uqUnitListOFFICER2_FIRST_NAME.AsString);
var m2 := Trim(ApiDB.uqDISUnitsActiveOFFICER2_MI.AsString); var m2 := Trim(ApiDB.uqUnitListOFFICER2_MI.AsString);
if o2 <> '' then if o2 <> '' then
begin begin
if f2 <> '' then o2 := o2 + ', ' + f2; if f2 <> '' then o2 := o2 + ', ' + f2;
......
...@@ -70,11 +70,4 @@ object FMain: TFMain ...@@ -70,11 +70,4 @@ object FMain: TFMain
Left = 256 Left = 256
Top = 402 Top = 402
end end
object tmrTwilio: TTimer
Enabled = False
Interval = 30000
OnTimer = tmrTwilioTimer
Left = 146
Top = 416
end
end end
...@@ -18,11 +18,9 @@ type ...@@ -18,11 +18,9 @@ type
initTimer: TTimer; initTimer: TTimer;
btnAuthSwaggerUI: TButton; btnAuthSwaggerUI: TButton;
ExeInfo1: TExeInfo; ExeInfo1: TExeInfo;
tmrTwilio: TTimer;
procedure btnApiSwaggerUIClick(Sender: TObject); procedure btnApiSwaggerUIClick(Sender: TObject);
procedure btnDataClick(Sender: TObject); procedure btnDataClick(Sender: TObject);
procedure btnExitClick(Sender: TObject); procedure btnExitClick(Sender: TObject);
procedure tmrTwilioTimer(Sender: TObject);
procedure ContactFormData(AText: String); procedure ContactFormData(AText: String);
procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure initTimerTimer(Sender: TObject); procedure initTimerTimer(Sender: TObject);
...@@ -130,12 +128,6 @@ begin ...@@ -130,12 +128,6 @@ begin
Logger.Log(1, LogValue('--Database->Password', IniEntries.DatabasePassword, IniEntries.DatabasePasswordFromIni)); Logger.Log(1, LogValue('--Database->Password', IniEntries.DatabasePassword, IniEntries.DatabasePasswordFromIni));
Logger.Log(1, ''); 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 try
AuthServerModule := TAuthServerModule.Create(Self); AuthServerModule := TAuthServerModule.Create(Self);
AuthServerModule.StartAuthServer(ServerConfig.url, AUTH_MODEL); AuthServerModule.StartAuthServer(ServerConfig.url, AUTH_MODEL);
...@@ -145,19 +137,6 @@ begin ...@@ -145,19 +137,6 @@ begin
AppServerModule := TAppServerModule.Create(Self); AppServerModule := TAppServerModule.Create(Self);
AppServerModule.StartAppServer(ServerConfig.url); 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 except
on E: Exception do on E: Exception do
Logger.Log(2, 'Failed to start server modules: ' + E.Message); Logger.Log(2, 'Failed to start server modules: ' + E.Message);
...@@ -165,16 +144,5 @@ begin ...@@ -165,16 +144,5 @@ begin
end; 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. end.
[Settings] [Settings]
LogFileNum=451 LogFileNum=484
webClientVersion=0.1.0 webClientVersion=0.1.0
TwilioUpdateTime=0 TwilioUpdateTime=0
[Database] [Database]
Server=192.168.102.130 Server=192.168.102.130
--Server=192.168.198.129 --Server=192.168.75.133
Database=envoy_db Database=envoy_db
Username=postgres Username=postgres
Password=postgreSQL 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 ...@@ -61,37 +61,78 @@ begin
end; end;
// ...uses JS, Web...
var
GIsBusy: Boolean = False;
procedure ShowSpinner(SpinnerID: string); procedure ShowSpinner(SpinnerID: string);
var var
SpinnerElement: TJSHTMLElement; SpinnerElement, Overlay: TJSHTMLElement;
begin begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID)); SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
if Assigned(SpinnerElement) then Overlay := TJSHTMLElement(document.getElementById('screenlock'));
// ensure overlay exists
if Overlay = nil then
begin begin
// Move spinner to the <body> if it's not already there Overlay := TJSHTMLElement(document.createElement('div'));
asm Overlay.id := 'screenlock';
if (SpinnerElement.parentNode !== document.body) { document.body.appendChild(Overlay);
document.body.appendChild(SpinnerElement);
}
end; 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.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; end;
document.body.setAttribute('aria-busy','true');
GIsBusy := True;
end; end;
procedure HideSpinner(SpinnerID: string); procedure HideSpinner(SpinnerID: string);
var var
SpinnerElement: TJSHTMLElement; SpinnerElement, Overlay: TJSHTMLElement;
begin begin
SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID)); SpinnerElement := TJSHTMLElement(document.getElementById(SpinnerID));
Overlay := TJSHTMLElement(document.getElementById('screenlock'));
if Assigned(SpinnerElement) then if Assigned(SpinnerElement) then
begin begin
SpinnerElement.classList.remove('d-block'); SpinnerElement.classList.remove('d-block');
SpinnerElement.classList.add('d-none'); SpinnerElement.classList.add('d-none');
end; 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; end;
function IsBusy: Boolean;
begin
Result := GIsBusy;
end;
procedure ShowErrorModal(msg: string); procedure ShowErrorModal(msg: string);
begin begin
......
...@@ -84,16 +84,15 @@ object FViewComplaints: TFViewComplaints ...@@ -84,16 +84,15 @@ object FViewComplaints: TFViewComplaints
Style = lsListGroup Style = lsListGroup
DataSource = wdsComplaints DataSource = wdsComplaints
ItemTemplate = ItemTemplate =
'<div class="list-section-header small fw-semibold bg-body-second' + '<div class="list-section-header small fw-semibold bg-secondary t' +
'ary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><di' + 'ext-white rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><div cl' +
'v class="card border shadow-sm" style="--bs-card-bg:(%Priori' + 'ass="card border shadow-sm" style="--bs-card-bg:(%PriorityCo' +
'tyColor%); --bs-card-color:(%PriorityTextColor%);"> <div class=' + 'lor%); --bs-card-color:(%PriorityTextColor%);"> <div class="car' +
'"card-body py-2 px-3"> <div class="fw-bold text-uppercase sma' + 'd-body py-2 px-3"> <div class="fw-bold text-uppercase small">' +
'll">(%Priority%): (%DispatchCodeDesc%)</div> <div class="smal' + '(%Priority%): (%DispatchCodeDesc%)</div> <div class="small">(' +
'l">(%Address%)</div> <div class="small text-opacity-75">(%Com' + '%Address%)</div> <div class="small text-opacity-75">(%Complai' +
'plaint%): (%Status%)&nbsp;&nbsp;(%DistrictSector%)</div> <div' + 'nt%): (%Status%)&nbsp;&nbsp;(%DistrictSector%)</div> <div cla' +
' class="small text-opacity-75">(%DateReported%)</div> </div></d' + 'ss="small text-opacity-75">(%DateReported%)</div> </div></div>'
'iv>'
ListSource = wdsComplaints ListSource = wdsComplaints
end end
object xdwcComplaints: TXDataWebClient object xdwcComplaints: TXDataWebClient
...@@ -154,4 +153,10 @@ object FViewComplaints: TFViewComplaints ...@@ -154,4 +153,10 @@ object FViewComplaints: TFViewComplaints
Left = 156 Left = 156
Top = 410 Top = 410
end end
object tmrRefresh: TWebTimer
Interval = 30000
OnTimer = tmrRefreshTimer
Left = 164
Top = 44
end
end end
...@@ -60,4 +60,3 @@ ...@@ -60,4 +60,3 @@
</div> </div>
</div> </div>
...@@ -34,9 +34,12 @@ type ...@@ -34,9 +34,12 @@ type
xdwdsComplaintsPriorityColor: TStringField; xdwdsComplaintsPriorityColor: TStringField;
xdwdsComplaintsPriorityTextColor: TStringField; xdwdsComplaintsPriorityTextColor: TStringField;
xdwdsComplaintsDistrictSector: TStringField; xdwdsComplaintsDistrictSector: TStringField;
tmrRefresh: TWebTimer;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject); procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject);
private private
FLoading: Boolean;
[async] procedure GetComplaints; [async] procedure GetComplaints;
public public
end; end;
...@@ -53,7 +56,10 @@ begin ...@@ -53,7 +56,10 @@ begin
console.log('WebFormCreate: Starting setup...'); console.log('WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True; DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected); console.log('API connection active:', DMConnection.ApiConnection.Connected);
ShowSpinner('spinner');
tmrRefresh.Enabled := False;
GetComplaints; GetComplaints;
tmrRefresh.Enabled := True;
end; end;
...@@ -69,8 +75,11 @@ var ...@@ -69,8 +75,11 @@ var
respObj: TJSObject; respObj: TJSObject;
complaintsCount: Integer; complaintsCount: Integer;
begin begin
if FLoading then Exit;
FLoading := True;
console.log('GetComplaints: Invoking API...'); console.log('GetComplaints: Invoking API...');
Utils.ShowSpinner('spinner');
try try
try try
...@@ -107,11 +116,18 @@ begin ...@@ -107,11 +116,18 @@ begin
end; end;
end; end;
finally finally
Utils.HideSpinner('spinner');
console.log('GetComplaints complete'); console.log('GetComplaints complete');
end; end;
HideSpinner('spinner');
end; end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin
GetComplaints;
console.log('tmrRefreshTimer fired');
end;
end. end.
...@@ -82,11 +82,14 @@ object FViewMap: TFViewMap ...@@ -82,11 +82,14 @@ object FViewMap: TFViewMap
Top = 0 Top = 0
Width = 335 Width = 335
Height = 555 Height = 555
AdaptToStyle = True
Align = alClient Align = alClient
ParentDoubleBuffered = False ParentDoubleBuffered = False
DoubleBuffered = True DoubleBuffered = True
TabStop = False TabStop = False
TabOrder = 0 TabOrder = 0
OnCustomizeCSS = lfMapCustomizeCSS
OnCustomizeMarker = lfMapCustomizeMarker
OnMapInitialized = lfMapMapInitialized OnMapInitialized = lfMapMapInitialized
Polylines = <> Polylines = <>
Polygons = <> Polygons = <>
...@@ -102,16 +105,30 @@ object FViewMap: TFViewMap ...@@ -102,16 +105,30 @@ object FViewMap: TFViewMap
#39'_blank'#39'>OpenStreetMap</a>' #39'_blank'#39'>OpenStreetMap</a>'
HeatMaps = <> HeatMaps = <>
LocalFileAccess = True LocalFileAccess = True
TileLayers = <> TileLayers = <
item
URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
Opacity = 1.000000000000000000
end>
ElementContainers = <> ElementContainers = <>
HeadLinks = <> HeadLinks = <>
end end
end end
object httpReqGeoJson: TWebHttpRequest object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText ResponseType = rtText
URL = 'assets/bpddistricts.geojson' URL = 'assets/bpddistricts-updated.geojson'
OnResponse = httpReqGeoJsonResponse OnResponse = httpReqGeoJsonResponse
Left = 114 Left = 114
Top = 696 Top = 696
end end
object xdwcMap: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 232
Top = 696
end
object tmrRefresh: TWebTimer
Interval = 30000
Left = 358
Top = 696
end
end end
...@@ -47,3 +47,6 @@ ...@@ -47,3 +47,6 @@
</div> </div>
...@@ -5,10 +5,10 @@ interface ...@@ -5,10 +5,10 @@ interface
uses uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, WEBLib.Dialogs, System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.Controls, WEBLib.Grids, Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.Controls, WEBLib.Grids,
WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
VCL.TMSFNCTypes, VCL.TMSFNCUtils, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser, VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
VCL.TMSFNCMapsCommonTypes; XData.Web.Connection, ConnectionModule, Utils;
type type
TFViewMap = class(TWebForm) TFViewMap = class(TWebForm)
...@@ -21,15 +21,21 @@ type ...@@ -21,15 +21,21 @@ type
pnlMap: TWebPanel; pnlMap: TWebPanel;
lfMap: TTMSFNCLeaflet; lfMap: TTMSFNCLeaflet;
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
xdwcMap: TXDataWebClient;
tmrRefresh: TWebTimer;
procedure lfMapMapInitialized(Sender: TObject); procedure lfMapMapInitialized(Sender: TObject);
procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
procedure httpReqGeoJsonError(Sender: TObject; AError: string);
procedure lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement); procedure lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
procedure lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement); procedure lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
procedure lfMapCustomizeMarker(Sender: TObject;
var ACustomizeMarker: string);
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
private private
procedure StyleDistrictsAndFit; FUnitsLoaded: Boolean;
procedure AssignDistrictNamesFromGeoJSON(const AJson: string); FComplaintsLoaded: Boolean;
[async] procedure LoadPointsAsync;
function CarIconForDistrict(const DistrictCode: string): string;
public public
end; end;
...@@ -45,175 +51,235 @@ uses ...@@ -45,175 +51,235 @@ uses
procedure TFViewMap.lfMapMapInitialized(Sender: TObject); procedure TFViewMap.lfMapMapInitialized(Sender: TObject);
begin begin
httpReqGeoJson.Execute; // GET assets/bpddistricts.geojson as text ShowSpinner('spinner');
FUnitsLoaded := False;
FComplaintsLoaded := False;
httpReqGeoJson.Execute;
end; end;
procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
begin
lfMap.LoadGeoJSONFromText(AResponse, True, False);
AssignDistrictNamesFromGeoJSON(AResponse);
StyleDistrictsAndFit;
end;
procedure TFViewMap.httpReqGeoJsonError(Sender: TObject; AError: string);
begin
Console.Log('Failed to load bpddistricts.geojson: ' + AError);
end;
procedure TFViewMap.AssignDistrictNamesFromGeoJSON(const AJson: string); [async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var var
Root, Feature, Props, Geom: JS.TJSObject; i: Integer;
Features, Coords: JS.TJSArray; P: TTMSFNCMapsPolygon;
f, count, i, polyIndex: Integer; nm: string;
name, gtype: string;
begin begin
Root := JS.TJSObject(JS.TJSJSON.parse(AJson)); lfMap.BeginUpdate;
Features := JS.TJSArray(Root['features']); try
if Features = nil then Exit; lfMap.Polygons.Clear;
polyIndex := 0;
for f := 0 to Features.Length - 1 do Console.Log('GeoJSON len=' + AResponse.Length.ToString);
begin lfMap.LoadGeoJSONFromText(AResponse, True, False);
Feature := JS.TJSObject(Features[f]); Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString);
Props := JS.TJSObject(Feature['properties']);
Geom := JS.TJSObject(Feature['geometry']);
if (Props <> nil) and Props.hasOwnProperty('NAME') then
name := string(Props['NAME'])
else
name := '';
if Geom <> nil then
gtype := string(Geom['type'])
else
gtype := 'Polygon';
if SameText(gtype, 'Polygon') then for i := 0 to lfMap.Polygons.Count - 1 do
count := 1
else if SameText(gtype, 'MultiPolygon') then
begin begin
Coords := JS.TJSArray(Geom['coordinates']); // array of polygons P := lfMap.Polygons[i];
if Coords <> nil then
count := Coords.Length
else
count := 1;
end
else
count := 1;
// tag each created polygon with the district name case i of
for i := 0 to count - 1 do 0: begin
begin P.DisplayName := 'District A';
if polyIndex < lfMap.Polygons.Count then P.FillColor := HTMLToColor('#d3ffbe'); // light green
begin P.StrokeColor := HTMLToColor('#6ea85c'); // darker green
lfMap.Polygons[polyIndex].DisplayName := name;
Inc(polyIndex);
end; end;
1: begin
P.DisplayName := 'District B';
P.FillColor := HTMLToColor('#ffbebe'); // light red
P.StrokeColor := HTMLToColor('#b34a4a'); // darker red
end; end;
2: begin
P.DisplayName := 'District C';
P.FillColor := HTMLToColor('#ffd37f'); // light orange
P.StrokeColor := HTMLToColor('#b36e00'); // darker orange
end; end;
end; 3: begin
P.DisplayName := 'District D';
procedure TFViewMap.StyleDistrictsAndFit; P.FillColor := HTMLToColor('#bed2ff'); // light blue
P.StrokeColor := HTMLToColor('#3c5ca8'); // darker blue
function DistrictLetter(const S: string): Char;
var U: string; k: Integer;
begin
U := UpperCase(Trim(S));
// find last A..Z; works even if theres extra text/spacing
Result := #0;
for k := Length(U) downto 1 do
if (U[k] >= 'A') and (U[k] <= 'Z') then
begin
Result := U[k];
Break;
end; end;
end; 4: begin
P.DisplayName := 'District E';
procedure ApplyHardCodedColors(const P: TTMSFNCMapsPolygon); P.FillColor := HTMLToColor('#ffffbe'); // light yellow
var L: Char; P.StrokeColor := HTMLToColor('#a8a85c'); // darker yellow/olive
begin
L := DistrictLetter(P.DisplayName);
case L of
'A': begin P.FillColor := $FF60A5FA; P.StrokeColor := $FF1D4ED8; end; // blue
'B': begin P.FillColor := $FF34D399; P.StrokeColor := $FF047857; end; // emerald
'C': begin P.FillColor := $FFF59E0B; P.StrokeColor := $FFB45309; end; // amber
'D': begin P.FillColor := $FFF87171; P.StrokeColor := $FFB91C1C; end; // red
'E': begin P.FillColor := $FFA78BFA; P.StrokeColor := $FF6D28D9; end; // purple (north)
else
// fallback cycle if a name didnt come through
case (P.Index mod 5) of
0: begin P.FillColor := $FF60A5FA; P.StrokeColor := $FF1D4ED8; end;
1: begin P.FillColor := $FF34D399; P.StrokeColor := $FF047857; end;
2: begin P.FillColor := $FFF59E0B; P.StrokeColor := $FFB45309; end;
3: begin P.FillColor := $FFF87171; P.StrokeColor := $FFB91C1C; end;
else begin P.FillColor := $FFA78BFA; P.StrokeColor := $FF6D28D9; end;
end; end;
end; end;
P.FillOpacity := 0.42; // bolder than before P.FillOpacity := 0.60;
P.StrokeOpacity := 1.0; P.StrokeOpacity := 1.0;
P.StrokeWidth := 2; P.StrokeWidth := 2;
end;
if lfMap.Polygons.Count > 0 then
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
finally
lfMap.EndUpdate;
end; end;
await(LoadPointsAsync);
end;
function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
var var
I, J: Integer; U: string;
P: TTMSFNCMapsPolygon; L: Char;
C: TTMSFNCMapsCoordinates;
minLat, minLng, maxLat, maxLng, lat, lng: Double;
hasAny: Boolean;
B: TTMSFNCMapsBoundsRec;
begin begin
if lfMap.Polygons.Count = 0 then Exit; U := UpperCase(Trim(DistrictCode));
if U = '' then
Exit('assets/markers/car_X.png');
// Apply hard-coded district colors L := U[1];
for I := 0 to lfMap.Polygons.Count - 1 do case L of
begin 'A','B','C','D','E','X':
P := lfMap.Polygons[I]; Result := 'assets/markers/car_' + L + '.png';
ApplyHardCodedColors(P); else
Result := 'assets/markers/default.png';
end; end;
end;
// Build bounds and zoom to fit
hasAny := False;
minLat := 90; minLng := 180;
maxLat := -90; maxLng := -180;
for I := 0 to lfMap.Polygons.Count - 1 do [async] procedure TFViewMap.LoadPointsAsync;
var
resp: TXDataClientResponse;
root, item: TJSObject;
data: TJSArray;
i: Integer;
m: TTMSFNCMapsMarker;
lat, lng: Double;
uname, dist: string;
complaintId, codeDesc, dispatchDist: string;
pngName, iconUrl: string;
begin
ShowSpinner('spinner');
FUnitsLoaded := False;
FComplaintsLoaded := False;
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
data := TJSArray(root['data']);
if data <> nil then
begin begin
P := lfMap.Polygons[I]; lfMap.BeginUpdate;
C := P.Coordinates; try
if (C <> nil) and (C.Count > 0) then for i := 0 to data.Length - 1 do
for J := 0 to C.Count - 1 do
begin begin
lat := C.Items[J].Latitude; item := TJSObject(data[i]);
lng := C.Items[J].Longitude; lat := Double(item['Lat']);
lng := Double(item['Lng']);
uname := string(item['UnitName']);
dist := string(item['District']);
m := lfMap.Markers.Add;
m.Latitude := lat;
m.Longitude := lng;
m.Title := uname + IfThen(dist <> '', ' / ' + dist, '');
m.DataString := 'unit';
m.IconURL := CarIconForDistrict(dist);
if not hasAny then
begin
minLat := lat; maxLat := lat;
minLng := lng; maxLng := lng;
hasAny := True;
end
else
begin
if lat < minLat then minLat := lat;
if lat > maxLat then maxLat := lat;
if lng < minLng then minLng := lng;
if lng > maxLng then maxLng := lng;
end; end;
finally
lfMap.EndUpdate;
end; end;
end; end;
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
if hasAny then try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result);
data := TJSArray(root['data']);
if data <> nil then
begin
lfMap.BeginUpdate;
try
for i := 0 to data.Length - 1 do
begin begin
B.SouthWest.Latitude := minLat; item := TJSObject(data[i]);
B.SouthWest.Longitude := minLng; complaintId := string(item['ComplaintId']);
B.NorthEast.Latitude := maxLat; codeDesc := string(item['DispatchCodeDesc']);
B.NorthEast.Longitude := maxLng; dispatchDist := string(item['DispatchDistrict']);
lfMap.ZoomToBounds(B); lat := Double(item['Lat']);
lng := Double(item['Lng']);
if ((lat = 0) and (lng = 0)) or (Abs(lat) > 90) or (Abs(lng) > 180) then
Continue;
pngName := string(item['pngName']);
if Trim(pngName) <> '' then
iconUrl := 'assets/markers/' + pngName
else
iconUrl := 'assets/markers/default.png';
m := lfMap.Markers.Add;
m.Latitude := lat;
m.Longitude := lng;
m.Title := Format('%s %s (%s)', [complaintId, codeDesc, dispatchDist]);
m.DataString := 'complaint|' + complaintId;
m.IconURL := iconUrl;
end; end;
finally
lfMap.EndUpdate;
end;
end;
FComplaintsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
// if FUnitsLoaded and FComplaintsLoaded then
// FitBoundsPolysAndMarkers;
HideSpinner('spinner');
end; end;
procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
begin
ACustomizeMarker :=
'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
'if (o.icon && o.icon.options && o.icon.options.iconUrl) {' + #13#10 +
' var u = o.icon.options.iconUrl;' + #13#10 +
' m.setIcon(L.icon({' + #13#10 +
' iconUrl: u,' + #13#10 +
' iconSize: [32,32],' + #13#10 +
' iconAnchor: [16,32],' + #13#10 +
' popupAnchor: [0,-20]' + #13#10 +
' }));' + #13#10 +
'}' + #13#10 +
'var t = (o && o.title) ? o.title : "";' + #13#10 +
'try { m.unbindTooltip(); } catch(e) {}' + #13#10 +
'm.bindTooltip(t, {' + #13#10 +
' className: "emi-tip",' + #13#10 +
' direction: "top",' + #13#10 +
' offset: [0,-28],' + #13#10 +
' sticky: true' + #13#10 +
'});';
end;
procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin
ACustomizeCSS :=
'.leaflet-tooltip.emi-tip {' + #13#10 +
' background: #111;' + #13#10 +
' color: #fff;' + #13#10 +
' border-radius: 6px;' + #13#10 +
' padding: 6px 8px;' + #13#10 +
' border: 1px solid rgba(255,255,255,.15);' + #13#10 +
' box-shadow: 0 4px 14px rgba(0,0,0,.35);' + #13#10 +
' font: 500 12px/1.2 system-ui, Segoe UI, Roboto, sans-serif;' + #13#10 +
'}' + #13#10 +
'.leaflet-tooltip-top.emi-tip:before { border-top-color: #111; }' + #13#10 +
'.leaflet-tooltip-bottom.emi-tip:before { border-bottom-color: #111; }' + #13#10 +
'.leaflet-tooltip-left.emi-tip:before { border-left-color: #111; }' + #13#10 +
'.leaflet-tooltip-right.emi-tip:before { border-right-color: #111; }';
end;
procedure TFViewMap.lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement); procedure TFViewMap.lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
begin begin
if AElement is TTMSFNCMapsPolygon then if AElement is TTMSFNCMapsPolygon then
...@@ -223,6 +289,7 @@ begin ...@@ -223,6 +289,7 @@ begin
end; end;
end; end;
procedure TFViewMap.lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement); procedure TFViewMap.lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
begin begin
if AElement is TTMSFNCMapsPolygon then if AElement is TTMSFNCMapsPolygon then
......
object FViewUnits: TFViewUnits object FViewUnits: TFViewUnits
Width = 359 Width = 359
Height = 480 Height = 480
ElementFont = efCSS
Font.Charset = DEFAULT_CHARSET Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText Font.Color = clWindowText
Font.Height = -11 Font.Height = -11
...@@ -38,15 +39,23 @@ object FViewUnits: TFViewUnits ...@@ -38,15 +39,23 @@ 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"> <div class="card-body py-2 px-' + 'iv class="card border shadow-sm position-relative"> <div class=' +
'3"> <div class="d-flex justify-content-between align-items-ba' + '"card-body py-2 px-3"> <!-- Unit + Status --> <div class="' +
'seline"> <div class="fw-bold fs-6">(%UnitName%)</div> ' + 'fw-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%S' +
'<div class="small text-end text-body-secondary ms-3 text-truncat' + 'tatus%) </div> <!-- Location --> <div class="small text' +
'e">(%Location%)</div> </div> <div class="small">(%Status%)' + '-body-secondary mb-1"> (%Location%) </div> <!-- Call ' +
'</div> <div class="small">(%Officer1%)</div> <div class="s' + 'type --> <div class="small">(%CallType%)</div> <!-- Divide' +
'mall">(%Officer2%)</div> </div></div>' '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 ListSource = wdsUnits
end end
object btnRefresh: TWebButton object btnRefresh: TWebButton
...@@ -123,10 +132,19 @@ object FViewUnits: TFViewUnits ...@@ -123,10 +132,19 @@ object FViewUnits: TFViewUnits
object xdwdsUnitsOfficer2: TStringField object xdwdsUnitsOfficer2: TStringField
FieldName = 'Officer2' FieldName = 'Officer2'
end end
object xdwdsUnitsCallType: TStringField
FieldName = 'CallType'
end
end end
object xdwcUnits: TXDataWebClient object xdwcUnits: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 58 Left = 58
Top = 410 Top = 410
end end
object tmrRefresh: TWebTimer
Interval = 30000
OnTimer = tmrRefreshTimer
Left = 172
Top = 22
end
end end
...@@ -27,9 +27,13 @@ type ...@@ -27,9 +27,13 @@ type
xdwdsUnitsStatus: TStringField; xdwdsUnitsStatus: TStringField;
xdwdsUnitsOfficer1: TStringField; xdwdsUnitsOfficer1: TStringField;
xdwdsUnitsOfficer2: TStringField; xdwdsUnitsOfficer2: TStringField;
xdwdsUnitsCallType: TStringField;
tmrRefresh: TWebTimer;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject); procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject);
private private
FLoading: Boolean;
[async] procedure GetUnits; [async] procedure GetUnits;
public public
...@@ -43,14 +47,36 @@ implementation ...@@ -43,14 +47,36 @@ implementation
{$R *.dfm} {$R *.dfm}
procedure TFViewUnits.WebFormCreate(Sender: TObject); procedure TFViewUnits.WebFormCreate(Sender: TObject);
begin begin
console.log('Units.WebFormCreate: Starting setup...'); console.log('Units.WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True; DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected); console.log('API connection active:', DMConnection.ApiConnection.Connected);
tmrRefresh.Enabled := False;
GetUnits; 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; end;
procedure TFViewUnits.btnRefreshClick(Sender: TObject); procedure TFViewUnits.btnRefreshClick(Sender: TObject);
...@@ -64,6 +90,9 @@ var ...@@ -64,6 +90,9 @@ var
respObj: TJSObject; respObj: TJSObject;
unitCount: Integer; unitCount: Integer;
begin begin
if FLoading then Exit;
FLoading := True;
console.log('GetUnits: Invoking API...'); console.log('GetUnits: Invoking API...');
Utils.ShowSpinner('spinner'); Utils.ShowSpinner('spinner');
try try
...@@ -98,5 +127,10 @@ begin ...@@ -98,5 +127,10 @@ begin
end; end;
end; end;
procedure TFViewUnits.tmrRefreshTimer(Sender: TObject);
begin
GetUnits;
end;
end. end.
...@@ -158,3 +158,4 @@ span.card { ...@@ -158,3 +158,4 @@ span.card {
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<noscript>Your browser does not support JavaScript!</noscript> <noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/> <link href="data:;base64,=" rel="icon"/>
<title>emiMobile</title> <title>emiMobile</title>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
<!-- jQuery --> <!-- jQuery -->
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="https://code.jquery.com/jquery-3.5.1.min.js" type="text/javascript"></script> <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 @@ ...@@ -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> <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"/> <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"/>
<!-- Leaflet --> <!-- App -->
<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 -->
<script src="$(ProjectName).js" type="text/javascript"></script> <script src="$(ProjectName).js" type="text/javascript"></script>
<!-- App styles --> <!-- App styles -->
<style></style> <style></style>
<link href="css/app.css" rel="stylesheet"/> <link href="css/app.css" rel="stylesheet"/>
<link href="css/spinner.css" rel="stylesheet" type="text/css"/>
</head> </head>
<body> <body>
<script type="text/javascript">rtl.run();</script> <script type="text/javascript">rtl.run();</script>
......
...@@ -97,6 +97,7 @@ ...@@ -97,6 +97,7 @@
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSUseJSDebugger>2</TMSUseJSDebugger> <TMSUseJSDebugger>2</TMSUseJSDebugger>
<VerInfo_Release>3</VerInfo_Release> <VerInfo_Release>3</VerInfo_Release>
<TMSWebBrowser>3</TMSWebBrowser>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols> <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