Commit 376416dd by emsys

merge conflicts

parents 8bb49d94 d1a82903
......@@ -21,3 +21,5 @@ emiMobileServer/logs/*
*.tvsconfig
*.dxsettings
*.zip
......@@ -2,12 +2,8 @@ object ApiDatabaseModule: TApiDatabaseModule
Height = 491
Width = 640
object OracleUniProvider1: TOracleUniProvider
Left = 182
Top = 98
end
object uqBooking: TUniQuery
Left = 350
Top = 98
Left = 164
Top = 38
end
object uqMapUnits: TUniQuery
Connection = ucENTCAD
......@@ -18,11 +14,38 @@ object ApiDatabaseModule: TApiDatabaseModule
' COALESCE(uc.UNITNAME, uc.CAR_NUMBER) AS UNITNAME,'
' uc.UNIT_DISTRICT,'
' uc.GPS_LATITUDE,'
' uc.GPS_LONGITUDE'
'FROM UNITS_CURRENT@AVL_LINK uc')
' uc.GPS_LONGITUDE,'
' cdc.CODE_DESC AS CALL_TYPE,'
' cap.PRIORITY AS PRIORITY,'
' cus.CODE_DESC AS UNIT_STATUS_DESC,'
' uc.UPDATETIME AS UPDATE_TIME,'
''
' dua.UNITID AS DIS_UNITID,'
''
' p1.PF_LNAME AS OFFICER1_LNAME,'
' p1.PF_FNAME AS OFFICER1_FNAME,'
' p1.PF_EMPNUM AS OFFICER1_EMPNUM,'
''
' p2.PF_LNAME AS OFFICER2_LNAME,'
' p2.PF_FNAME AS OFFICER2_FNAME,'
' p2.PF_EMPNUM AS OFFICER2_EMPNUM'
''
'FROM UNITS_CURRENT@AVL_LINK uc'
'LEFT JOIN CFS_ACTIVE cfs ON uc.UNITID = cfs.UNITID'
'LEFT JOIN COMPLAINT_ACTIVE cap ON cap.COMPLAINTID = cfs.COMPLAIN' +
'TID'
'LEFT JOIN CD_DISPATCHCODES cdc ON cdc.CODE = cap.DISPATCHCODE'
'LEFT JOIN CD_UNITSTATUS cus ON cus.CODE = cfs.UNITSTATUS'
'LEFT JOIN DIS_UNIT_ACTIVE dua ON dua.UNITID = uc.UNITID'
'LEFT JOIN PERSONNEL p1 ON p1.PF_NAMEID = dua.OFFICER1ID'
'LEFT JOIN PERSONNEL p2 ON p2.PF_NAMEID = dua.OFFICER2ID'
''
''
'')
ReadOnly = True
Left = 470
Top = 414
Left = 464
Top = 390
object uqMapUnitsENTRYID: TFloatField
FieldName = 'ENTRYID'
end
......@@ -43,6 +66,57 @@ object ApiDatabaseModule: TApiDatabaseModule
object uqMapUnitsGPS_LONGITUDE: TFloatField
FieldName = 'GPS_LONGITUDE'
end
object uqMapUnitsCALL_TYPE: TStringField
FieldName = 'CALL_TYPE'
ReadOnly = True
Size = 60
end
object uqMapUnitsPRIORITY: TStringField
FieldName = 'PRIORITY'
ReadOnly = True
Size = 6
end
object uqMapUnitsUNIT_STATUS_DESC: TStringField
FieldName = 'UNIT_STATUS_DESC'
ReadOnly = True
end
object uqMapUnitsUPDATE_TIME: TDateTimeField
FieldName = 'UPDATE_TIME'
end
object uqMapUnitsOFFICER1_LNAME: TStringField
FieldName = 'OFFICER1_LNAME'
ReadOnly = True
Size = 45
end
object uqMapUnitsOFFICER1_FNAME: TStringField
FieldName = 'OFFICER1_FNAME'
ReadOnly = True
Size = 30
end
object uqMapUnitsOFFICER1_EMPNUM: TStringField
FieldName = 'OFFICER1_EMPNUM'
ReadOnly = True
Size = 10
end
object uqMapUnitsOFFICER2_LNAME: TStringField
FieldName = 'OFFICER2_LNAME'
ReadOnly = True
Size = 45
end
object uqMapUnitsOFFICER2_FNAME: TStringField
FieldName = 'OFFICER2_FNAME'
ReadOnly = True
Size = 30
end
object uqMapUnitsOFFICER2_EMPNUM: TStringField
FieldName = 'OFFICER2_EMPNUM'
ReadOnly = True
Size = 10
end
object uqMapUnitsDIS_UNITID: TFloatField
FieldName = 'DIS_UNITID'
ReadOnly = True
end
end
object uqUnitList: TUniQuery
Connection = ucENTCAD
......@@ -105,8 +179,8 @@ object ApiDatabaseModule: TApiDatabaseModule
''
'')
ReadOnly = True
Left = 278
Top = 322
Left = 76
Top = 188
object uqUnitListUNITID: TFloatField
FieldName = 'UNITID'
end
......@@ -221,8 +295,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'WHERE ca.COMPLAINTID = :COMPLAINTID'
'ORDER BY ca.DATEDISPATCHED')
ReadOnly = True
Left = 470
Top = 356
Left = 466
Top = 326
ParamData = <
item
DataType = ftUnknown
......@@ -268,15 +342,15 @@ object ApiDatabaseModule: TApiDatabaseModule
' cm.REMARKS'
'FROM CFS_MEMOS cm'
'WHERE cm.CFSID = :CFSID'
'ORDER BY cm.TIMESTAMP ASC')
'ORDER BY cm.TIMESTAMP DESC')
ReadOnly = True
Left = 278
Top = 382
Left = 196
Top = 248
ParamData = <
item
DataType = ftUnknown
Name = 'CFSID'
Value = Null
Value = nil
end>
object uqCFSMemosMEMO_ID: TFloatField
FieldName = 'MEMO_ID'
......@@ -317,7 +391,7 @@ object ApiDatabaseModule: TApiDatabaseModule
' ca.APARTMENT,'
' ca.CITY,'
' ca.BUSINESS,'
' ca.DISPATCHDISTRICT,'
' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.DISPATCHSECTOR,'
' ca.ADDRESSDISTRICT,'
' ca.ADDRESSSECTOR,'
......@@ -343,20 +417,17 @@ object ApiDatabaseModule: TApiDatabaseModule
'LEFT JOIN CD_CALLPRIORITIES cp ON cp.CODE = ca.PRIORITY AND cp.A' +
'GENCY = ca.AGENCY'
'LEFT JOIN CD_DISTRICT cd ON cd.AGENCYCODE = ca.DISPATCHDI' +
'JOIN CD_DISTRICT cd ON cd.AGENCYCODE = ca.DISPATCHDI' +
'STRICT'
'LEFT JOIN CD_SECTOR cs ON cs.AGENCYCODE = ca.DISPATCHSE' +
'CTOR AND cs.CODE_TYPE = cd.AGENCYCODE '
'CTOR AND cs.CODE_TYPE = cd.AGENCYCODE'
'WHERE ca.COMPLAINT IS NOT NULL'
'ORDER BY ca.DISPATCHDISTRICT, ct.DATEREPORTED DESC, ca.PRIORITY ' +
'DESC'
'')
'ORDER BY cd.CODE_DESC, ct.DATEREPORTED DESC, ca.PRIORITY DESC;')
ReadOnly = True
OnCalcFields = uqComplaintListCalcFields
Left = 94
Top = 320
Left = 78
Top = 244
object uqComplaintListCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID'
Required = True
......@@ -494,22 +565,45 @@ object ApiDatabaseModule: TApiDatabaseModule
' ca.PRIORITY,'
' ca.DISPATCHCODE,'
' cdc.CODE_DESC AS DISPATCH_CODE_DESC,'
' ca.DISPATCHDISTRICT,'
' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.ADDRESS,'
' ca.BUSINESS,'
' ct.DATEREPORTED,'
' ct.DATERECEIVED,'
' ct.DATEDISPATCHED,'
' ct.DATERESPONDED,'
' ct.DATEARRIVED,'
' ct.DATECLEARED'
' ct.DATECLEARED,'
''
' -- For enabling/disabling buttons (disable when = '#39'-1'#39')'
' ca.HISTORY,'
' ca.CONTACTS,'
' ca.WARNINGS,'
''
' -- For Contacts + Warnings lookups'
' ca.ADDRESSID,'
' ca.AGENCY,'
''
' -- For History matching (address parts)'
' a.STRNUMBER,'
' a.STRHNUMBER,'
' a.STRPREFIX,'
' a.STRNAME,'
' a.STRSUFFIX,'
' a.CITY'
'FROM COMPLAINT_ACTIVE ca'
'JOIN COMPLAINT_TIMES ct ON ca.COMPLAINTID = ct.COMPLAINTID'
'LEFT JOIN CD_DISPATCHCODES cdc ON ca.DISPATCHCODE = cdc.CODE'
'WHERE ca.COMPLAINTID = :COMPLAINTID;'
'')
'JOIN COMPLAINT_TIMES ct'
' ON ca.COMPLAINTID = ct.COMPLAINTID'
'LEFT JOIN CD_DISPATCHCODES cdc'
' ON ca.DISPATCHCODE = cdc.CODE'
'JOIN CD_DISTRICT cd'
' ON cd.AGENCYCODE = ca.DISPATCHDISTRICT'
'LEFT JOIN ADDRESS a'
' ON ca.ADDRESSID = a.ADDRESSID'
'WHERE ca.COMPLAINTID = :COMPLAINTID;')
ReadOnly = True
Left = 92
Top = 376
Left = 80
Top = 302
ParamData = <
item
DataType = ftUnknown
......@@ -542,7 +636,7 @@ object ApiDatabaseModule: TApiDatabaseModule
end
object uqComplaintDetailsDISPATCHDISTRICT: TStringField
FieldName = 'DISPATCHDISTRICT'
Size = 6
Size = 120
end
object uqComplaintDetailsADDRESS: TStringField
FieldName = 'ADDRESS'
......@@ -572,6 +666,53 @@ object ApiDatabaseModule: TApiDatabaseModule
FieldName = 'DATECLEARED'
ReadOnly = True
end
object uqComplaintDetailsHISTORY: TFloatField
FieldName = 'HISTORY'
end
object uqComplaintDetailsCONTACTS: TFloatField
FieldName = 'CONTACTS'
end
object uqComplaintDetailsWARNINGS: TFloatField
FieldName = 'WARNINGS'
end
object uqComplaintDetailsADDRESSID: TFloatField
FieldName = 'ADDRESSID'
end
object uqComplaintDetailsAGENCY: TStringField
FieldName = 'AGENCY'
Size = 6
end
object uqComplaintDetailsSTRNUMBER: TFloatField
FieldName = 'STRNUMBER'
ReadOnly = True
end
object uqComplaintDetailsSTRHNUMBER: TStringField
FieldName = 'STRHNUMBER'
ReadOnly = True
Size = 1
end
object uqComplaintDetailsSTRPREFIX: TStringField
FieldName = 'STRPREFIX'
ReadOnly = True
Size = 1
end
object uqComplaintDetailsSTRNAME: TStringField
FieldName = 'STRNAME'
ReadOnly = True
end
object uqComplaintDetailsSTRSUFFIX: TStringField
FieldName = 'STRSUFFIX'
ReadOnly = True
Size = 2
end
object uqComplaintDetailsCITY: TStringField
FieldName = 'CITY'
ReadOnly = True
end
object uqComplaintDetailsBUSINESS: TStringField
FieldName = 'BUSINESS'
Size = 35
end
end
object ucENTCAD: TUniConnection
ProviderName = 'Oracle'
......@@ -580,8 +721,8 @@ object ApiDatabaseModule: TApiDatabaseModule
Server = 'BUFENTCAD'
Connected = True
LoginPrompt = False
Left = 38
Top = 98
Left = 50
Top = 36
EncryptedPassword = 'BAFFB1FFABFFBCFFBEFFBBFF'
end
object uqMapComplaints: TUniQuery
......@@ -589,29 +730,24 @@ object ApiDatabaseModule: TApiDatabaseModule
SQL.Strings = (
'SELECT'
' ca.COMPLAINTID,'
' ca.DISPATCHDISTRICT,'
' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.PRIORITY AS PRIORITY,'
' ca.BUSINESS,'
' cdc.MOBILE_MAP_CATEGORY AS DISPATCHCODECATEGORY,'
''
' CASE '
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' 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,'
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YCOORD' +
', NULL), NULL, NULL),'
' 4326'
' ).sdo_point.x AS LNG,'
''
' CASE '
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' 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,'
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YCOORD' +
', NULL), NULL, NULL),'
' 4326'
' ).sdo_point.y AS LAT,'
''
' cdc.CODE_DESC AS DISPATCH_CODE_DESC,'
' ca.ADDRESS AS ADDRESS'
......@@ -620,22 +756,26 @@ object ApiDatabaseModule: TApiDatabaseModule
' 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 '
'JOIN CD_DISTRICT cd'
' ON cd.AGENCYCODE = ca.DISPATCHDISTRICT'
'WHERE ca.COMPLAINT IS NOT NULL'
' AND ca.XCOORD IS NOT NULL'
' AND ca.YCOORD IS NOT NULL;'
''
''
''
'')
ReadOnly = True
OnCalcFields = uqMapComplaintsCalcFields
Left = 470
Top = 296
Left = 468
Top = 264
object uqMapComplaintsCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID'
Required = True
end
object uqMapComplaintsDISPATCHDISTRICT: TStringField
FieldName = 'DISPATCHDISTRICT'
Size = 6
Size = 120
end
object uqMapComplaintsLNG: TFloatField
FieldName = 'LNG'
......@@ -671,6 +811,10 @@ object ApiDatabaseModule: TApiDatabaseModule
FieldName = 'pngName'
Calculated = True
end
object uqMapComplaintsBUSINESS: TStringField
FieldName = 'BUSINESS'
Size = 35
end
end
object uqBadgeCounts: TUniQuery
Connection = ucENTCAD
......@@ -686,8 +830,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'from dual;'
'')
ReadOnly = True
Left = 194
Top = 270
Left = 198
Top = 190
object uqBadgeCountsCOMPLAINTS: TFloatField
FieldName = 'COMPLAINTS'
ReadOnly = True
......@@ -717,9 +861,9 @@ object ApiDatabaseModule: TApiDatabaseModule
' AND c.YCOORD IS NOT NULL'
')'
'ORDER BY ca.COMPLAINTID, ca.DATEDISPATCHED;')
Active = True
Left = 470
Top = 242
ReadOnly = True
Left = 464
Top = 202
object uqMapComplaintUnitsListCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID'
end
......@@ -747,4 +891,282 @@ object ApiDatabaseModule: TApiDatabaseModule
Size = 30
end
end
object uqComplaintHistory: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'with ctx as ('
' select'
' ca.agency,'
' a.strnumber,'
' a.strhnumber,'
' a.strprefix,'
' a.strname,'
' a.strsuffix,'
' a.city'
' from complaint_active ca'
' left join address a on ca.addressid = a.addressid'
' where ca.complaintid = :COMPLAINTID'
')'
'select'
' c.complaint,'
' c.apartment,'
' c.datereported,'
' cp.code_desc as dpriority,'
' cdc.code_desc as dcalltype'
'from complaint_archive c'
'join ctx on 1 = 1'
'left join cd_callpriorities cp on c.priority = cp.code'
'left join cd_dispatchcodes cdc on c.dispatchcode = cdc.code'
'where c.agency = ctx.agency'
'and ('
' (ctx.strnumber is null and c.strnumber is null)'
' or (ctx.strnumber is not null and c.strnumber = ctx.strnumber)'
')'
'and (ctx.strhnumber is null or c.strhnumber = ctx.strhnumber)'
'and (ctx.strprefix is null or c.strprefix = ctx.strprefix)'
'and (ctx.strname is null or c.strname = ctx.strname)'
'and (ctx.strsuffix is null or c.strsuffix = ctx.strsuffix)'
'and (ctx.city is null or c.city = ctx.city)'
'order by c.datereported desc')
ReadOnly = True
Left = 328
Top = 72
ParamData = <
item
DataType = ftUnknown
Name = 'COMPLAINTID'
Value = nil
end>
end
object uqComplaintContacts: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'with ctx as ('
' select ca.addressid'
' from complaint_active ca'
' where ca.complaintid = :COMPLAINTID'
')'
'select'
' c.name,'
' c.phone,'
' ct.code_desc as dcontacttype,'
' c.remarks'
'from dis_contact c'
'join ctx on c.addressid = ctx.addressid'
'left join cd_contact_type ct on c.contact_type = ct.code')
ReadOnly = True
Left = 330
Top = 130
ParamData = <
item
DataType = ftUnknown
Name = 'COMPLAINTID'
Value = nil
end>
end
object uqComplaintWarnings: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'with ctx as ('
' select ca.agency, ca.addressid'
' from complaint_active ca'
' where ca.complaintid = :COMPLAINTID'
'),'
'sys as ('
' select s.warningdistance'
' from system s'
' join ctx on s.agency = ctx.agency'
'),'
'adr as ('
' select a.xcoord, a.ycoord'
' from address a'
' join ctx on a.addressid = ctx.addressid'
')'
'select'
' wt.code_desc,'
' trim('
' nvl(a.strnumber, '#39#39') || '#39' '#39' ||'
' nvl(a.strhnumber, '#39#39') || '#39' '#39' ||'
' nvl(a.strprefix, '#39#39') || '#39' '#39' ||'
' nvl(a.strname, '#39#39') || '#39' '#39' ||'
' nvl(a.strsuffix, '#39#39') ||'
' case when a.apartment is not null then '#39' '#39' || a.apartment el' +
'se '#39#39' end ||'
' case when a.city is not null then '#39', '#39' || a.city else '#39#39' end'
' ) as address_text,'
' case'
' when w.addressid <> (select addressid from ctx) then '#39'[SECON' +
'DARY ADDRESS] - '#39' || nvl(w.notes, '#39#39')'
' else nvl(w.notes, '#39#39')'
' end as notes,'
' abs(sqrt(power(a.xcoord - (select xcoord from adr), 2) + power' +
'(a.ycoord - (select ycoord from adr), 2))) as distance'
'from dis_warning w'
'join cd_warningtype wt on w.code = wt.code'
'join address a on w.addressid = a.addressid'
'where w.status = 1'
'and ('
' a.addressid = (select addressid from ctx)'
' or ('
' (select xcoord from adr) > 0'
' and (select ycoord from adr) > 0'
' and nvl((select warningdistance from sys), 0) > 0'
' and a.xcoord between (select xcoord from adr) - (select warn' +
'ingdistance from sys)'
' and (select xcoord from adr) + (select warnin' +
'gdistance from sys)'
' and a.ycoord between (select ycoord from adr) - (select warn' +
'ingdistance from sys)'
' and (select ycoord from adr) + (select warnin' +
'gdistance from sys)'
' )'
')'
'order by'
' distance,'
' decode(w.code, '#39'POL'#39', 1, '#39'FIR'#39', 2, 3),'
' wt.code_desc')
ReadOnly = True
Left = 488
Top = 76
ParamData = <
item
DataType = ftUnknown
Name = 'COMPLAINTID'
Value = nil
end>
end
object uqUnitDetails: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'SELECT'
' dua.UNITID,'
' dua.UNITNAME,'
' cun.CODE_DESC AS CARNUMBER_DESC,'
' cd.CODE_DESC AS DISTRICT_DESC,'
' ca.LOCATION,'
' cus.CODE_DESC AS UNIT_STATUS_DESC,'
' p1.PF_LNAME AS OFFICER1_LNAME,'
' p1.PF_FNAME AS OFFICER1_FNAME,'
' p1.PF_EMPNUM AS OFFICER1_EMPNUM,'
' p2.PF_LNAME AS OFFICER2_LNAME,'
' p2.PF_FNAME AS OFFICER2_FNAME,'
' p2.PF_EMPNUM AS OFFICER2_EMPNUM,'
' uc.UPDATETIME AS UPDATE_TIME'
'FROM DIS_UNITS_ACTIVE dua'
'LEFT JOIN CD_UNIT_NUMBER cun ON cun.AGENCYCODE = dua.CARNUMBER'
'LEFT JOIN CD_DISTRICT cd ON cd.AGENCYCODE = dua.DISTRICT'
'LEFT JOIN CFS_ACTIVE ca ON dua.UNITID = ca.UNITID'
'LEFT JOIN CD_UNITSTATUS cus ON ca.UNITSTATUS = cus.CODE'
'LEFT JOIN PERSONNEL p1 ON dua.OFFICER1ID = p1.PF_NAMEID'
'LEFT JOIN PERSONNEL p2 ON dua.OFFICER2ID = p2.PF_NAMEID'
'LEFT JOIN UNITS_CURRENT@AVL_LINK uc ON dua.UNITID = uc.UNITID'
'WHERE dua.UNITID = :UNITID')
ReadOnly = True
Left = 194
Top = 312
ParamData = <
item
DataType = ftUnknown
Name = 'UNITID'
Value = nil
end>
object uqUnitDetailsUNITID: TFloatField
FieldName = 'UNITID'
end
object uqUnitDetailsUNITNAME: TStringField
FieldName = 'UNITNAME'
Size = 10
end
object uqUnitDetailsCARNUMBER_DESC: TStringField
FieldName = 'CARNUMBER_DESC'
ReadOnly = True
Size = 120
end
object uqUnitDetailsDISTRICT_DESC: TStringField
FieldName = 'DISTRICT_DESC'
ReadOnly = True
Size = 120
end
object uqUnitDetailsLOCATION: TStringField
FieldName = 'LOCATION'
ReadOnly = True
Size = 30
end
object uqUnitDetailsUNIT_STATUS_DESC: TStringField
FieldName = 'UNIT_STATUS_DESC'
ReadOnly = True
end
object uqUnitDetailsOFFICER1_LNAME: TStringField
FieldName = 'OFFICER1_LNAME'
ReadOnly = True
Size = 45
end
object uqUnitDetailsOFFICER1_FNAME: TStringField
FieldName = 'OFFICER1_FNAME'
ReadOnly = True
Size = 30
end
object uqUnitDetailsOFFICER1_EMPNUM: TStringField
FieldName = 'OFFICER1_EMPNUM'
ReadOnly = True
Size = 10
end
object uqUnitDetailsOFFICER2_LNAME: TStringField
FieldName = 'OFFICER2_LNAME'
ReadOnly = True
Size = 45
end
object uqUnitDetailsOFFICER2_FNAME: TStringField
FieldName = 'OFFICER2_FNAME'
ReadOnly = True
Size = 30
end
object uqUnitDetailsOFFICER2_EMPNUM: TStringField
FieldName = 'OFFICER2_EMPNUM'
ReadOnly = True
Size = 10
end
object uqUnitDetailsUPDATE_TIME: TDateTimeField
FieldName = 'UPDATE_TIME'
ReadOnly = True
end
end
object uqUnitLogs: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'select'
' dul."TIMESTAMP" as LOG_TIME,'
' dul.COMPLAINT as COMPLAINT_NUM,'
' dul.LOG as LOG_TEXT'
'from ENTCAD.DIS_UNIT_LOG dul'
'where dul.UNITID = :UNITID'
'order by dul."TIMESTAMP" desc')
ReadOnly = True
Left = 190
Top = 374
ParamData = <
item
DataType = ftUnknown
Name = 'UNITID'
Value = nil
end>
object uqUnitLogsLOG_TIME: TDateTimeField
FieldName = 'LOG_TIME'
end
object uqUnitLogsCOMPLAINT_NUM: TStringField
FieldName = 'COMPLAINT_NUM'
Size = 10
end
object uqUnitLogsLOG_TEXT: TStringField
FieldName = 'LOG_TEXT'
Size = 2000
end
end
end
......@@ -10,7 +10,6 @@ uses
type
TApiDatabaseModule = class(TDataModule)
OracleUniProvider1: TOracleUniProvider;
uqBooking: TUniQuery;
uqMapUnits: TUniQuery;
uqUnitList: TUniQuery;
uqComplaintUnits: TUniQuery;
......@@ -60,12 +59,6 @@ type
uqCFSMemosTIMESTAMP: TDateTimeField;
uqCFSMemosBADGE_NUMBER: TStringField;
uqCFSMemosREMARKS: TStringField;
uqMapUnitsENTRYID: TFloatField;
uqMapUnitsUNITID: TFloatField;
uqMapUnitsUNITNAME: TStringField;
uqMapUnitsUNIT_DISTRICT: TStringField;
uqMapUnitsGPS_LATITUDE: TFloatField;
uqMapUnitsGPS_LONGITUDE: TFloatField;
uqComplaintListcomplaintNumber: TStringField;
uqComplaintListPRIORITY_COLOR: TFloatField;
uqComplaintListDISTRICT_DESC: TStringField;
......@@ -128,6 +121,57 @@ type
uqMapComplaintUnitsListDATECLEARED: TDateTimeField;
uqMapComplaintUnitsListLOCATION: TStringField;
uqMapComplaintsADDRESS: TStringField;
uqMapUnitsENTRYID: TFloatField;
uqMapUnitsUNITID: TFloatField;
uqMapUnitsUNITNAME: TStringField;
uqMapUnitsUNIT_DISTRICT: TStringField;
uqMapUnitsGPS_LATITUDE: TFloatField;
uqMapUnitsGPS_LONGITUDE: TFloatField;
uqMapUnitsCALL_TYPE: TStringField;
uqMapUnitsPRIORITY: TStringField;
uqMapUnitsUNIT_STATUS_DESC: TStringField;
uqComplaintDetailsHISTORY: TFloatField;
uqComplaintDetailsCONTACTS: TFloatField;
uqComplaintDetailsWARNINGS: TFloatField;
uqComplaintDetailsADDRESSID: TFloatField;
uqComplaintDetailsAGENCY: TStringField;
uqComplaintDetailsSTRNUMBER: TFloatField;
uqComplaintDetailsSTRHNUMBER: TStringField;
uqComplaintDetailsSTRPREFIX: TStringField;
uqComplaintDetailsSTRNAME: TStringField;
uqComplaintDetailsSTRSUFFIX: TStringField;
uqComplaintDetailsCITY: TStringField;
uqComplaintHistory: TUniQuery;
uqComplaintContacts: TUniQuery;
uqComplaintWarnings: TUniQuery;
uqMapUnitsUPDATE_TIME: TDateTimeField;
uqMapUnitsOFFICER1_LNAME: TStringField;
uqMapUnitsOFFICER1_FNAME: TStringField;
uqMapUnitsOFFICER1_EMPNUM: TStringField;
uqMapUnitsOFFICER2_LNAME: TStringField;
uqMapUnitsOFFICER2_FNAME: TStringField;
uqMapUnitsOFFICER2_EMPNUM: TStringField;
uqUnitDetails: TUniQuery;
uqUnitDetailsUNITID: TFloatField;
uqUnitDetailsUNITNAME: TStringField;
uqUnitDetailsCARNUMBER_DESC: TStringField;
uqUnitDetailsDISTRICT_DESC: TStringField;
uqUnitDetailsLOCATION: TStringField;
uqUnitDetailsUNIT_STATUS_DESC: TStringField;
uqUnitDetailsOFFICER1_LNAME: TStringField;
uqUnitDetailsOFFICER1_FNAME: TStringField;
uqUnitDetailsOFFICER1_EMPNUM: TStringField;
uqUnitDetailsOFFICER2_LNAME: TStringField;
uqUnitDetailsOFFICER2_FNAME: TStringField;
uqUnitDetailsOFFICER2_EMPNUM: TStringField;
uqUnitDetailsUPDATE_TIME: TDateTimeField;
uqUnitLogs: TUniQuery;
uqUnitLogsLOG_TIME: TDateTimeField;
uqUnitLogsCOMPLAINT_NUM: TStringField;
uqUnitLogsLOG_TEXT: TStringField;
uqMapUnitsDIS_UNITID: TFloatField;
uqMapComplaintsBUSINESS: TStringField;
uqComplaintDetailsBUSINESS: TStringField;
procedure uqComplaintListCalcFields(DataSet: TDataSet);
procedure uqMapComplaintsCalcFields(DataSet: TDataSet);
private
......
......@@ -22,6 +22,14 @@ type
[HttpGet] function GetComplaintMap: TJSONObject;
[HttpGet] function GetUnitMap: TJSONObject;
[HttpGet] function GetComplaintDetails(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintMemos(const CfsId: string): TJSONObject;
[HttpGet] function GetComplaintHistory(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintContacts(const ComplaintId: string): TJSONObject;
[HttpGet] function GetComplaintWarnings(const ComplaintId: string): TJSONObject;
[HttpGet] function GetUnitDetails(const UnitId: string): TJSONObject;
[HttpGet] function GetUnitLogs(const UnitId: string): TJSONObject;
end;
implementation
......
......@@ -16,6 +16,7 @@ type
private
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
function GetComplaintMemos(const CfsId: string): TJSONObject;
public
function GetBadgeCounts: TJSONObject;
function GetComplaintList: TJSONObject;
......@@ -23,6 +24,11 @@ type
function GetComplaintMap: TJSONObject;
function GetUnitMap: TJSONObject;
function GetComplaintDetails(const ComplaintId: string): TJSONObject;
function GetComplaintHistory(const ComplaintId: string): TJSONObject;
function GetComplaintContacts(const ComplaintId: string): TJSONObject;
function GetComplaintWarnings(const ComplaintId: string): TJSONObject;
function GetUnitDetails(const UnitId: string): TJSONObject;
function GetUnitLogs(const UnitId: string): TJSONObject;
end;
implementation
......@@ -66,7 +72,7 @@ begin
except
on E: Exception do
begin
Logger.Log(3, '---TApiService.GetBadgeCounts End (error): ' + E.Message);
Logger.Log(2, '---TApiService.GetBadgeCounts End (error): ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load badge counts');
end;
end;
......@@ -146,13 +152,14 @@ begin
item:=TJSONObject.Create;
item.AddPair('ComplaintId',ApiDB.uqMapComplaintsCOMPLAINTID.AsString);
item.AddPair('DispatchDistrict',ApiDB.uqMapComplaintsDISPATCHDISTRICT.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.uqMapComplaintspriorityKey.AsString);
item.AddPair('pngName',ApiDB.uqMapComplaintspngName.AsString);
item.AddPair('Address',ApiDB.uqMapComplaintsADDRESS.AsString);
item.AddPair('Business',ApiDB.uqMapComplaintsBUSINESS.AsString);
complaintId:=ApiDB.uqMapComplaintsCOMPLAINTID.AsString;
if UnitsByComplaintMap.TryGetValue(complaintId,unitArray) then
......@@ -176,7 +183,7 @@ begin
on E: Exception do
begin
FreeAndNil(data);
Logger.Log(1,'GetComplaintMap error: '+E.Message);
Logger.Log(2,'GetComplaintMap error: '+E.Message);
raise EXDataHttpException.Create(500,'Failed to load complaint map');
end;
end;
......@@ -186,13 +193,18 @@ begin
end;
function TApiService.GetUnitMap: TJSONObject;
var
data: TJSONArray;
item: TJSONObject;
unitStatus: string;
updateTimeText: string;
begin
Logger.Log(3, '---TApiService.GetUnitMap initiated');
Logger.Log(4, '---TApiService.GetUnitMap initiated');
// Note: GetUnitMap is AVL-anchored (shows all AVL units).
// Note: DIS_UNITID is null when the unit is not dispatch-active; client should disable/hide Details in that case.
// Note: To restrict map to dispatch-active only, change uqMapUnits join to DIS_UNIT_ACTIVE from LEFT JOIN to INNER JOIN.
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
......@@ -202,21 +214,51 @@ begin
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
try
First;
while not Eof do
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);
if (not ApiDB.uqMapUnitsGPS_LATITUDE.IsNull) and (not ApiDB.uqMapUnitsGPS_LONGITUDE.IsNull) then
begin
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));
item.AddPair('CallType', ApiDB.uqMapUnitsCALL_TYPE.AsString);
item.AddPair('Priority', ApiDB.uqMapUnitsPRIORITY.AsString);
unitStatus := ApiDB.uqMapUnitsUNIT_STATUS_DESC.AsString;
if Trim(unitStatus) = '' then
unitStatus := 'Available';
item.AddPair('Status', unitStatus);
updateTimeText := '';
if not ApiDB.uqMapUnitsUPDATE_TIME.IsNull then
updateTimeText := FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqMapUnitsUPDATE_TIME.AsDateTime);
item.AddPair('UpdateTime', updateTimeText);
item.AddPair('Officer1Lname', ApiDB.uqMapUnitsOFFICER1_LNAME.AsString);
item.AddPair('Officer1Fname', ApiDB.uqMapUnitsOFFICER1_FNAME.AsString);
item.AddPair('Officer1Empnum', ApiDB.uqMapUnitsOFFICER1_EMPNUM.AsString);
item.AddPair('Officer2Lname', ApiDB.uqMapUnitsOFFICER2_LNAME.AsString);
item.AddPair('Officer2Fname', ApiDB.uqMapUnitsOFFICER2_FNAME.AsString);
item.AddPair('Officer2Empnum', ApiDB.uqMapUnitsOFFICER2_EMPNUM.AsString);
item.AddPair('CanShowDetails', TJSONBool.Create(not ApiDB.uqMapUnitsDIS_UNITID.IsNull));
data.AddElement(item);
end;
Next;
end;
Next;
finally
Close;
end;
end;
......@@ -225,14 +267,16 @@ begin
Result.AddPair('data', data);
except
data.Free;
Logger.Log(3, '---TApiService.GetUnitMap End (error)');
Logger.Log(2, '---TApiService.GetUnitMap error');
raise EXDataHttpException.Create(500, 'Failed to load unit map');
end;
Logger.Log(3, '---TApiService.GetUnitMap End');
Logger.Log(4, '---TApiService.GetUnitMap End');
end;
function TApiService.GetComplaintList: TJSONObject;
var
data: TJSONArray;
......@@ -287,6 +331,7 @@ begin
item.AddPair('Priority', ApiDB.uqComplaintListPRIORITY.AsString);
item.AddPair('DispatchCodeDesc', ApiDB.uqComplaintListDISPATCH_CODE_DESC.AsString);
item.AddPair('Address', ApiDB.uqComplaintListADDRESS.AsString);
item.AddPair('Business', ApiDB.uqComplaintListBUSINESS.AsString);
item.AddPair('CFSId', ApiDB.uqComplaintListCFSID.AsString);
item.AddPair('Status', status);
item.AddPair('DispatchDistrict', curDistrict);
......@@ -302,7 +347,7 @@ begin
Result.AddPair('data', data);
except
data.Free;
Logger.Log(3, '---TApiService.GetComplaintList End (error)');
Logger.Log(2, '---TApiService.GetComplaintList End (error)');
raise EXDataHttpException.Create(500, 'Failed to load complaints list');
end;
......@@ -415,35 +460,51 @@ begin
if Eof then raise EXDataHttpException.Create(404,'Complaint not found');
obj := TJSONObject.Create;
obj.AddPair('ComplaintId',FieldByName('COMPLAINTID').AsString);
obj.AddPair('CFSId',FieldByName('CFSID').AsString);
obj.AddPair('Complaint',FieldByName('COMPLAINT').AsString);
obj.AddPair('Priority',FieldByName('PRIORITY').AsString);
obj.AddPair('DispatchCode',FieldByName('DISPATCHCODE').AsString);
obj.AddPair('DispatchCodeDesc',FieldByName('DISPATCH_CODE_DESC').AsString);
obj.AddPair('DispatchDistrict',FieldByName('DISPATCHDISTRICT').AsString);
obj.AddPair('Address',FieldByName('ADDRESS').AsString);
if FieldByName('DATEREPORTED').IsNull
then obj.AddPair('DateReported','')
else obj.AddPair('DateReported',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEREPORTED') as TDateTimeField).AsDateTime));
if FieldByName('DATERECEIVED').IsNull
then obj.AddPair('DateReceived','')
else obj.AddPair('DateReceived',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATERECEIVED') as TDateTimeField).AsDateTime));
if FieldByName('DATEDISPATCHED').IsNull
then obj.AddPair('DateDispatched','')
else obj.AddPair('DateDispatched',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEDISPATCHED') as TDateTimeField).AsDateTime));
if FieldByName('DATERESPONDED').IsNull
then obj.AddPair('DateResponded','')
else obj.AddPair('DateResponded',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATERESPONDED') as TDateTimeField).AsDateTime));
if FieldByName('DATEARRIVED').IsNull
then obj.AddPair('DateArrived','')
else obj.AddPair('DateArrived',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEARRIVED') as TDateTimeField).AsDateTime));
if FieldByName('DATECLEARED').IsNull
then obj.AddPair('DateCleared','')
else obj.AddPair('DateCleared',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATECLEARED') as TDateTimeField).AsDateTime));
Result.AddPair('data',obj);
obj.AddPair('ComplaintId', ApiDB.uqComplaintDetailsCOMPLAINTID.AsString);
obj.AddPair('CFSId', ApiDB.uqComplaintDetailsCFSID.AsString);
obj.AddPair('Complaint', ApiDB.uqComplaintDetailsCOMPLAINT.AsString);
obj.AddPair('Priority', ApiDB.uqComplaintDetailsPRIORITY.AsString);
obj.AddPair('DispatchCode', ApiDB.uqComplaintDetailsDISPATCHCODE.AsString);
obj.AddPair('DispatchCodeDesc', ApiDB.uqComplaintDetailsDISPATCH_CODE_DESC.AsString);
obj.AddPair('DispatchDistrict', ApiDB.uqComplaintDetailsDISPATCHDISTRICT.AsString);
obj.AddPair('Address', ApiDB.uqComplaintDetailsADDRESS.AsString);
obj.AddPair('Business',ApiDB.uqComplaintDetailsBUSINESS.AsString);
obj.AddPair('History', ApiDB.uqComplaintDetailsHISTORY.AsString);
obj.AddPair('Contacts', ApiDB.uqComplaintDetailsCONTACTS.AsString);
obj.AddPair('Warnings', ApiDB.uqComplaintDetailsWARNINGS.AsString);
if ApiDB.uqComplaintDetailsDATEREPORTED.IsNull then
obj.AddPair('DateReported', '')
else
obj.AddPair('DateReported', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATEREPORTED.AsDateTime));
if ApiDB.uqComplaintDetailsDATERECEIVED.IsNull then
obj.AddPair('DateReceived', '')
else
obj.AddPair('DateReceived', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATERECEIVED.AsDateTime));
if ApiDB.uqComplaintDetailsDATEDISPATCHED.IsNull then
obj.AddPair('DateDispatched', '')
else
obj.AddPair('DateDispatched', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATEDISPATCHED.AsDateTime));
if ApiDB.uqComplaintDetailsDATERESPONDED.IsNull then
obj.AddPair('DateResponded', '')
else
obj.AddPair('DateResponded', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATERESPONDED.AsDateTime));
if ApiDB.uqComplaintDetailsDATEARRIVED.IsNull then
obj.AddPair('DateArrived', '')
else
obj.AddPair('DateArrived', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATEARRIVED.AsDateTime));
if ApiDB.uqComplaintDetailsDATECLEARED.IsNull then
obj.AddPair('DateCleared', '')
else
obj.AddPair('DateCleared', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATECLEARED.AsDateTime));
Result.AddPair('data', obj);
finally
Close;
end;
......@@ -464,6 +525,377 @@ begin
end;
function TApiService.GetComplaintMemos(const CfsId: string): TJSONObject;
var
data: TJSONArray;
item: TJSONObject;
ts: string;
begin
Logger.Log(3, '---TApiService.GetComplaintMemos initiated: ' + CfsId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
data := TJSONArray.Create;
try
with ApiDB.uqCFSMemos do
begin
ParamByName('CFSID').AsString := CfsId;
Open;
try
First;
while not Eof do
begin
item := TJSONObject.Create;
item.AddPair('MemoId', ApiDB.uqCFSMemosMEMO_ID.AsString);
item.AddPair('CFSId', ApiDB.uqCFSMemosCFSID.AsString);
item.AddPair('MemoType', ApiDB.uqCFSMemosMEMO_TYPE.AsString);
if ApiDB.uqCFSMemosTIMESTAMP.IsNull then
ts := ''
else
ts := FormatDateTime('yyyy-mm-dd' + ' ' + 'hh:nn:ss', ApiDB.uqCFSMemosTIMESTAMP.AsDateTime);
item.AddPair('Timestamp', ts);
item.AddPair('BadgeNumber', ApiDB.uqCFSMemosBADGE_NUMBER.AsString);
item.AddPair('Remarks', ApiDB.uqCFSMemosREMARKS.AsString);
data.AddElement(item);
Next;
end;
finally
Close;
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.GetComplaintMemos End (error)');
raise EXDataHttpException.Create(500, 'Failed to load complaint memos');
end;
Logger.Log(3, '---TApiService.GetComplaintMemos End');
end;
function TApiService.GetComplaintHistory(const ComplaintId: string): TJSONObject;
var
dataArr: TJSONArray;
rowObj: TJSONObject;
returnedCount: Integer;
begin
Logger.Log(4, '---TApiService.GetComplaintHistory initiated: ' + ComplaintId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
dataArr := TJSONArray.Create;
Result.AddPair('data', dataArr);
returnedCount := 0;
try
with ApiDB.uqComplaintHistory do
begin
ParamByName('COMPLAINTID').AsString := ComplaintId;
Open;
try
while not Eof do
begin
if returnedCount >= 50 then
Break;
rowObj := TJSONObject.Create;
dataArr.AddElement(rowObj);
rowObj.AddPair('Complaint', FieldByName('COMPLAINT').AsString);
rowObj.AddPair('Apartment', FieldByName('APARTMENT').AsString);
if FieldByName('DATEREPORTED').IsNull then
rowObj.AddPair('DateReported', '')
else
rowObj.AddPair('DateReported', FormatDateTime('yyyy-mm-dd', FieldByName('DATEREPORTED').AsDateTime));
rowObj.AddPair('DPriority', FieldByName('DPRIORITY').AsString);
rowObj.AddPair('DCallType', FieldByName('DCALLTYPE').AsString);
Inc(returnedCount);
Next;
end;
finally
Close;
end;
end;
Result.AddPair('count', TJSONNumber.Create(returnedCount));
Result.AddPair('returned', TJSONNumber.Create(returnedCount));
Logger.Log(3, '---TApiService.GetComplaintHistory End (returned=' + IntToStr(returnedCount) + ')');
except
on E: EXDataHttpException do
begin
Logger.Log(2, '---TApiService.GetComplaintHistory http error: ' + E.Message);
raise;
end;
on E: Exception do
begin
Logger.Log(2, '---TApiService.GetComplaintHistory error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load complaint history');
end;
end;
end;
function TApiService.GetComplaintContacts(const ComplaintId: string): TJSONObject;
var
dataArr: TJSONArray;
rowObj: TJSONObject;
returnedCount: Integer;
begin
Logger.Log(4, '---TApiService.GetComplaintContacts initiated: ' + ComplaintId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
dataArr := TJSONArray.Create;
Result.AddPair('data', dataArr);
returnedCount := 0;
try
with ApiDB.uqComplaintContacts do
begin
ParamByName('COMPLAINTID').AsString := ComplaintId;
Open;
try
while not Eof do
begin
rowObj := TJSONObject.Create;
dataArr.AddElement(rowObj);
rowObj.AddPair('Name', FieldByName('NAME').AsString);
rowObj.AddPair('Phone', FieldByName('PHONE').AsString);
rowObj.AddPair('DContactType', FieldByName('DCONTACTTYPE').AsString);
rowObj.AddPair('Remarks', FieldByName('REMARKS').AsString);
Inc(returnedCount);
Next;
end;
finally
Close;
end;
end;
Result.AddPair('count', TJSONNumber.Create(returnedCount));
Result.AddPair('returned', TJSONNumber.Create(returnedCount));
Logger.Log(3, '---TApiService.GetComplaintContacts End (returned=' + IntToStr(returnedCount) + ')');
except
on E: EXDataHttpException do
begin
Logger.Log(2, '---TApiService.GetComplaintContacts http error: ' + E.Message);
raise;
end;
on E: Exception do
begin
Logger.Log(2, '---TApiService.GetComplaintContacts error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load complaint contacts');
end;
end;
end;
function TApiService.GetComplaintWarnings(const ComplaintId: string): TJSONObject;
var
dataArr: TJSONArray;
rowObj: TJSONObject;
returnedCount: Integer;
begin
Logger.Log(3, '---TApiService.GetComplaintWarnings initiated: ' + ComplaintId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
dataArr := TJSONArray.Create;
Result.AddPair('data', dataArr);
returnedCount := 0;
try
with ApiDB.uqComplaintWarnings do
begin
ParamByName('COMPLAINTID').AsString := ComplaintId;
Open;
try
while not Eof do
begin
rowObj := TJSONObject.Create;
dataArr.AddElement(rowObj);
rowObj.AddPair('CodeDesc', FieldByName('CODE_DESC').AsString);
rowObj.AddPair('Address', FieldByName('ADDRESS_TEXT').AsString);
rowObj.AddPair('Notes', FieldByName('NOTES').AsString);
Inc(returnedCount);
Next;
end;
finally
Close;
end;
end;
Result.AddPair('count', TJSONNumber.Create(returnedCount));
Result.AddPair('returned', TJSONNumber.Create(returnedCount));
Logger.Log(3, '---TApiService.GetComplaintWarnings End (returned=' + IntToStr(returnedCount) + ')');
except
on E: EXDataHttpException do
begin
Logger.Log(2, '---TApiService.GetComplaintWarnings http error: ' + E.Message);
raise;
end;
on E: Exception do
begin
Logger.Log(2, '---TApiService.GetComplaintWarnings error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load complaint warnings');
end;
end;
end;
function TApiService.GetUnitLogs(const UnitId: string): TJSONObject;
var
dataArr: TJSONArray;
rowObj: TJSONObject;
returnedCount: Integer;
ts: string;
complaintText: string;
begin
Logger.Log(4, '---TApiService.GetUnitLogs initiated: ' + UnitId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
dataArr := TJSONArray.Create;
returnedCount := 0;
try
with ApiDB.uqUnitLogs do
begin
ParamByName('UNITID').AsString := UnitId;
Open;
try
while not Eof do
begin
rowObj := TJSONObject.Create;
dataArr.AddElement(rowObj);
ts := '';
if not ApiDB.uqUnitLogsLOG_TIME.IsNull then
ts := FormatDateTime('yyyy-mm-dd' + ' ' + 'HH:nn:ss', ApiDB.uqUnitLogsLOG_TIME.AsDateTime);
complaintText := Trim(ApiDB.uqUnitLogsCOMPLAINT_NUM.AsString);
rowObj.AddPair('LogTime', ts);
rowObj.AddPair('Complaint', complaintText);
rowObj.AddPair('Log', ApiDB.uqUnitLogsLOG_TEXT.AsString);
Inc(returnedCount);
Next;
end;
finally
Close;
end;
end;
Result.AddPair('count', TJSONNumber.Create(returnedCount));
Result.AddPair('returned', TJSONNumber.Create(returnedCount));
Result.AddPair('data', dataArr);
Logger.Log(4, '---TApiService.GetUnitLogs End (returned=' + IntToStr(returnedCount) + ')');
except
on E: EXDataHttpException do
begin
Logger.Log(2, '---TApiService.GetUnitLogs http error: ' + E.Message);
raise;
end;
on E: Exception do
begin
Logger.Log(2, '---TApiService.GetUnitLogs error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load unit logs');
end;
end;
end;
function TApiService.GetUnitDetails(const UnitId: string): TJSONObject;
var
obj: TJSONObject;
updateTimeText: string;
unitStatus: string;
begin
Logger.Log(4, '---TApiService.GetUnitDetails initiated: ' + UnitId);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
try
with ApiDB.uqUnitDetails do
begin
ParamByName('UNITID').AsString := UnitId;
Open;
try
if Eof then
raise EXDataHttpException.Create(404, 'Unit not found');
obj := TJSONObject.Create;
obj.AddPair('UnitId', ApiDB.uqUnitDetailsUNITID.AsString);
obj.AddPair('UnitName', ApiDB.uqUnitDetailsUNITNAME.AsString);
obj.AddPair('CarNumberDesc', ApiDB.uqUnitDetailsCARNUMBER_DESC.AsString);
obj.AddPair('District', ApiDB.uqUnitDetailsDISTRICT_DESC.AsString);
obj.AddPair('Location', ApiDB.uqUnitDetailsLOCATION.AsString);
unitStatus := Trim(ApiDB.uqUnitDetailsUNIT_STATUS_DESC.AsString);
if unitStatus = '' then
unitStatus := 'Available';
obj.AddPair('Status', unitStatus);
obj.AddPair('Officer1Lname', ApiDB.uqUnitDetailsOFFICER1_LNAME.AsString);
obj.AddPair('Officer1Fname', ApiDB.uqUnitDetailsOFFICER1_FNAME.AsString);
obj.AddPair('Officer1Empnum', ApiDB.uqUnitDetailsOFFICER1_EMPNUM.AsString);
obj.AddPair('Officer2Lname', ApiDB.uqUnitDetailsOFFICER2_LNAME.AsString);
obj.AddPair('Officer2Fname', ApiDB.uqUnitDetailsOFFICER2_FNAME.AsString);
obj.AddPair('Officer2Empnum', ApiDB.uqUnitDetailsOFFICER2_EMPNUM.AsString);
updateTimeText := '';
if not ApiDB.uqUnitDetailsUPDATE_TIME.IsNull then
updateTimeText := FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqUnitDetailsUPDATE_TIME.AsDateTime);
obj.AddPair('UpdateTime', updateTimeText);
Result.AddPair('data', obj);
finally
Close;
end;
end;
except
on E: EXDataHttpException do
begin
Logger.Log(2, '---TApiService.GetUnitDetails http error: ' + E.Message);
raise;
end;
on E: Exception do
begin
Logger.Log(2, '---TApiService.GetUnitDetails error: ' + E.Message);
raise EXDataHttpException.Create(500, 'Failed to load unit details');
end;
end;
Logger.Log(4, '---TApiService.GetUnitDetails End');
end;
initialization
......
[Settings]
LogFileNum=531
LogFileNum=610
webClientVersion=0.1.0
<div class="row">
<div class="col-lg-12">
<h1 class="page-header" id="view.userprofile.title">Admin User Profile</h1>
<div role="form">
<div class="form-group">
<label id="view.userprofile.form.lblUserName">User Name:</label>
<input id="view.userprofile.form.edtUserName" class="form-control">
</div>
<div class="form-group">
<label id="view.userprofile.form.lblFullName">User Fullname:</label>
<input id="view.userprofile.form.edtFullName" class="form-control">
</div>
<div class="form-group">
<label id="view.userprofile.form.lblAgency">User Agency:</label>
<input id="view.userprofile.form.edtAgency" class="form-control">
</div>
<div class="form-group">
<label id="view.userprofile.form.lblBadgeNum">User Bage #:</label>
<input id="view.userprofile.form.edtBadgeNum" class="form-control">
</div>
<div class="form-group">
<label id="view.userprofile.form.lblUserId">User Id:</label>
<input id="view.userprofile.form.edtUserId" class="form-control">
</div>
<div class="form-group">
<label id="view.userprofile.form.lblPersonnelId">Personnel Id:</label>
<input id="view.userprofile.form.edtPersonnelId" class="form-control">
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="view.userprofile.form.chkAdminUser">
<label class="custom-control-label" for="view.userprofile.form.chkAdminUser">Admin User</label>
</div>
<div class="form-input">
<div><label id="lblinfo" class="py-2" style="font-size: 1.00rem;"></label></div>
<div><input class="form-control input-sm" id="edtusername" width='50%'/></div>
<div class="py-2"><input class="form-control input-sm" id="edtpassword" width='50%'/></div>
<button id="btnadduser"></button>
<div><label id="lblresult" class="py-2" style="font-size: 1.00rem;"></label></div>
<div class="container-fluid p-3">
<h1 id="view.userprofile.title" class="h2 border-bottom pb-2 mb-4 text-primary">
Admin User Profile
</h1>
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title text-secondary mb-3">User Details</h5>
<div class="row g-3">
<div class="col-12">
<label id="view.userprofile.form.lblFullName" for="view.userprofile.form.edtFullName" class="form-label fw-bold">User Fullname:</label>
<input id="view.userprofile.form.edtFullName" type="text" class="form-control">
</div>
<div class="col-md-6">
<label id="view.userprofile.form.lblUserName" for="view.userprofile.form.edtUserName" class="form-label fw-bold">User Name:</label>
<input id="view.userprofile.form.edtUserName" type="text" class="form-control">
</div>
<div class="col-md-6">
<label id="view.userprofile.form.lblAgency" for="view.userprofile.form.edtAgency" class="form-label fw-bold">User Agency:</label>
<input id="view.userprofile.form.edtAgency" type="text" class="form-control">
</div>
<div class="col-md-4">
<label id="view.userprofile.form.lblBadgeNum" for="view.userprofile.form.edtBadgeNum" class="form-label small text-muted">User Badge #:</label>
<input id="view.userprofile.form.edtBadgeNum" type="text" class="form-control">
</div>
<div class="col-md-4">
<label id="view.userprofile.form.lblUserId" for="view.userprofile.form.edtUserId" class="form-label small text-muted">User Id:</label>
<input id="view.userprofile.form.edtUserId" type="text" class="form-control">
</div>
<div class="col-md-4">
<label id="view.userprofile.form.lblPersonnelId" for="view.userprofile.form.edtPersonnelId" class="form-label small text-muted">Personnel Id:</label>
<input id="view.userprofile.form.edtPersonnelId" type="text" class="form-control">
</div>
<div class="col-12">
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" id="view.userprofile.form.chkAdminUser">
<label class="form-check-label user-select-none" for="view.userprofile.form.chkAdminUser">
Admin User
</label>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 bg-light">
<div class="card-body">
<h6 class="card-subtitle mb-3 text-muted">Quick Actions / Create User</h6>
<div class="mb-2" style="min-height: 1.5rem;">
<label id="lblinfo" class="text-info fw-bold mb-0"></label>
</div>
<div class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Username</label>
<input id="edtusername" type="text" class="form-control form-control-sm">
</div>
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Password</label>
<input id="edtpassword" type="password" class="form-control form-control-sm">
</div>
<div class="col-md-2">
<button id="btnadduser" class="btn btn-primary btn-sm w-100">
<i class="fa fa-plus me-1"></i> Add
</button>
</div>
</div>
<div class="mt-2">
<label id="lblresult" class="text-success small mb-0"></label>
</div>
</div>
</div>
</div>
object FViewComplaintDetails: TFViewComplaintDetails
Width = 640
Height = 480
Width = 800
Height = 672
Caption = 'tbl_logs'
CSSLibrary = cssBootstrap
ElementFont = efCSS
object WebDBTableControl1: TWebDBTableControl
Left = 164
Top = 198
Width = 300
Height = 200
object btnHistory: TWebButton
Left = 506
Top = 134
Width = 96
Height = 25
Caption = 'History'
ChildOrder = 1
ElementID = 'btn_history'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnHistoryClick
end
object btnWarnings: TWebButton
Left = 294
Top = 134
Width = 96
Height = 25
Caption = 'Warnings'
ChildOrder = 1
ElementID = 'btn_warnings'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnWarningsClick
end
object btnContacts: TWebButton
Left = 404
Top = 134
Width = 96
Height = 25
Caption = 'Contacts'
ChildOrder = 1
ElementID = 'btn_contacts'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnContactsClick
end
object btnCmp: TWebButton
Left = 190
Top = 165
Width = 96
Height = 25
Caption = 'CMP'
ChildOrder = 1
ElementID = 'btn_cmp'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnCmpClick
end
object btnE911: TWebButton
Left = 294
Top = 165
Width = 96
Height = 25
Caption = 'E-911'
ChildOrder = 1
ElementID = 'btn_e911'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnE911Click
end
object btnREM: TWebButton
Left = 404
Top = 165
Width = 96
Height = 25
Caption = 'REM'
ChildOrder = 1
ElementID = 'btn_rem'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnREMClick
end
object btnUnt: TWebButton
Left = 506
Top = 165
Width = 96
Height = 25
Caption = 'UNT'
ChildOrder = 1
ElementID = 'btn_unt'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnUntClick
end
object tblRemarks: TWebDBTableControl
Left = 8
Top = 196
Width = 191
Height = 147
ElementId = 'tbl_remarks'
BorderColor = clSilver
ChildOrder = 8
ElementFont = efCSS
ElementHeaderClassName = 'table-light'
ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control'
Footer.InputElementClassName = 'form-control'
Footer.LinkActiveElementClassName = 'link-primary'
Footer.LinkElementClassName = 'link-secondary'
Footer.ListElementClassName = 'pagination'
Footer.ListItemElementClassName = 'page-item'
Footer.ListLinkElementClassName = 'page-link'
Header.ButtonActiveElementClassName = 'btn btn-primary'
Header.ButtonElementClassName = 'btn btn-light'
Header.DropDownElementClassName = 'form-control'
Header.InputElementClassName = 'form-control'
Header.LinkActiveElementClassName = 'link-primary'
Header.LinkElementClassName = 'link-secondary'
Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link'
WordWrap = True
Columns = <
item
ElementClassName = 'd-none'
DataField = 'MemoType'
Title = 'Type'
TitleElementClassName = 'd-none'
end
item
DataField = 'Timestamp'
Title = 'Timestamp'
end
item
DataField = 'Remarks'
Title = 'Remarks'
end>
DataSource = wdsRemarks
end
object tblContacts: TWebDBTableControl
Left = 399
Top = 196
Width = 191
Height = 147
ElementId = 'tbl_contacts'
BorderColor = clSilver
ChildOrder = 8
ElementFont = efCSS
ElementHeaderClassName = 'table-light'
ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control'
Footer.InputElementClassName = 'form-control'
Footer.LinkActiveElementClassName = 'link-primary'
Footer.LinkElementClassName = 'link-secondary'
Footer.ListElementClassName = 'pagination'
Footer.ListItemElementClassName = 'page-item'
Footer.ListLinkElementClassName = 'page-link'
Header.ButtonActiveElementClassName = 'btn btn-primary'
Header.ButtonElementClassName = 'btn btn-light'
Header.DropDownElementClassName = 'form-control'
Header.InputElementClassName = 'form-control'
Header.LinkActiveElementClassName = 'link-primary'
Header.LinkElementClassName = 'link-secondary'
Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link'
WordWrap = True
Columns = <
item
ElementClassName = 'text-nowrap'
DataField = 'Name'
Title = 'Name'
end
item
ElementClassName = 'text-nowrap'
DataField = 'Phone'
Title = 'Phone'
end
item
ElementClassName = 'text-nowrap'
DataField = 'DContactType'
Title = 'Contact Type'
end
item
DataField = 'Remarks'
Title = 'Remarks'
end>
DataSource = wdsContacts
end
object tblHistory: TWebDBTableControl
Left = 202
Top = 196
Width = 191
Height = 147
ElementId = 'tbl_history'
BorderColor = clSilver
ChildOrder = 8
ElementFont = efCSS
ElementHeaderClassName = 'table-light'
ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control'
Footer.InputElementClassName = 'form-control'
Footer.LinkActiveElementClassName = 'link-primary'
Footer.LinkElementClassName = 'link-secondary'
Footer.ListElementClassName = 'pagination'
Footer.ListItemElementClassName = 'page-item'
Footer.ListLinkElementClassName = 'page-link'
Header.ButtonActiveElementClassName = 'btn btn-primary'
Header.ButtonElementClassName = 'btn btn-light'
Header.DropDownElementClassName = 'form-control'
Header.InputElementClassName = 'form-control'
Header.LinkActiveElementClassName = 'link-primary'
Header.LinkElementClassName = 'link-secondary'
Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link'
WordWrap = True
Columns = <
item
ElementClassName = 'text-nowrap'
DataField = 'Complaint'
Title = 'Complaint'
end
item
ElementClassName = 'text-nowrap'
DataField = 'Apartment'
Title = 'Apt'
end
item
ElementClassName = 'text-nowrap'
DataField = 'DateReported'
Title = 'Date'
end
item
ElementClassName = 'text-nowrap'
DataField = 'DPriority'
Title = 'Pri'
end
item
DataField = 'DCallType'
Title = 'Call Type'
end>
DataSource = wdsHistory
end
object tblWarnings: TWebDBTableControl
Left = 596
Top = 196
Width = 191
Height = 147
ElementId = 'tbl_warnings'
BorderColor = clSilver
ChildOrder = 8
ElementFont = efCSS
ElementHeaderClassName = 'thead-light'
ElementTableClassName = 'table table-striped table-bordered table-hover'
ElementHeaderClassName = 'table-light'
ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control'
......@@ -30,6 +302,171 @@ object FViewComplaintDetails: TFViewComplaintDetails
Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link'
Columns = <>
WordWrap = True
Columns = <
item
ElementClassName = 'text-nowrap'
DataField = 'CodeDesc'
Title = 'Warning'
end
item
ElementClassName = 'text-nowrap'
DataField = 'Address'
Title = 'Address'
end
item
DataField = 'Notes'
Title = 'Notes'
end>
DataSource = wdsWarnings
end
object btnRemarks: TWebButton
Left = 190
Top = 134
Width = 96
Height = 25
Caption = 'Remarks'
ChildOrder = 1
ElementID = 'btn_remarks'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
TabStop = False
WidthPercent = 100.000000000000000000
OnClick = btnRemarksClick
end
object lstWarnings: TWebDBListControl
Left = 596
Top = 352
Width = 191
Height = 65
ElementID = 'lst_warnings'
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
ChildOrder = 12
DefaultItemClassName = 'list-group-item'
DefaultItemLinkClassName = 'list-group-link'
ElementFont = efCSS
ElementListClassName = 'list-group'
Items = <>
Style = lsListGroup
DataSource = wdsWarnings
ItemTemplate =
'<div class="card mb-2"> <div class="card-body py-2"> <div cl' +
'ass="d-flex justify-content-between gap-2"> <div class="fw-' +
'semibold">(%CodeDesc%)</div> <div class="text-muted small t' +
'ext-end">(%ADDRESS%)</div> </div> <div class="small mt-1">' +
'(%NOTES%)</div> </div></div>'
ListSource = wdsWarnings
end
object btnComplaintViewOnMap: TWebButton
Left = 510
Top = 430
Width = 96
Height = 25
Caption = 'Map'
ChildOrder = 1
ElementID = 'btn_complaint_view_on_map'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnComplaintViewOnMapClick
end
object xdwcComplaintDetails: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 378
Top = 424
end
object xdwdsRemarks: TXDataWebDataSet
Left = 40
Top = 350
object xdwdsRemarksMemoId: TStringField
FieldName = 'MemoId'
end
object xdwdsRemarksCFSId: TStringField
FieldName = 'CFSId'
end
object xdwdsRemarksMemoType: TStringField
FieldName = 'MemoType'
end
object xdwdsRemarksTimestamp: TStringField
FieldName = 'Timestamp'
end
object xdwdsRemarksBadgeNumber: TStringField
FieldName = 'BadgeNumber'
end
object xdwdsRemarksRemarks: TStringField
FieldName = 'Remarks'
end
end
object wdsRemarks: TWebDataSource
DataSet = xdwdsRemarks
Left = 126
Top = 350
end
object xdwdsHistory: TXDataWebDataSet
Left = 238
Top = 348
object xdwdsHistoryComplaint: TStringField
FieldName = 'Complaint'
end
object xdwdsHistoryApartment: TStringField
FieldName = 'Apartment'
end
object xdwdsHistoryDateReported: TStringField
FieldName = 'DateReported'
end
object xdwdsHistoryDPriority: TStringField
FieldName = 'DPriority'
end
object xdwdsHistoryDCallType: TStringField
FieldName = 'DCallType'
end
end
object wdsHistory: TWebDataSource
DataSet = xdwdsHistory
Left = 328
Top = 350
end
object xdwdsContacts: TXDataWebDataSet
Left = 436
Top = 352
object xdwdsContactsName: TStringField
FieldName = 'Name'
end
object xdwdsContactsPhone: TStringField
FieldName = 'Phone'
end
object xdwdsContactsDContactType: TStringField
FieldName = 'DContactType'
end
object xdwdsContactsRemarks: TStringField
FieldName = 'Remarks'
end
end
object wdsContacts: TWebDataSource
DataSet = xdwdsContacts
Left = 530
Top = 352
end
object xdwdsWarnings: TXDataWebDataSet
Left = 634
Top = 492
object xdwdsWarningsCodeDesc: TStringField
FieldName = 'CodeDesc'
end
object xdwdsWarningsAddress: TStringField
FieldName = 'Address'
end
object xdwdsWarningsNotes: TStringField
FieldName = 'Notes'
end
end
object wdsWarnings: TWebDataSource
DataSet = xdwdsWarnings
Left = 728
Top = 492
end
end
<!-- Sticky local navbar (Complaint Details) -->
<div class="sticky-top">
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<div id="cdetails_title" class="navbar-brand mb-0 h5 text-white">Complaint Details</div>
</div>
<div class="col-auto">
<button id="btn_close_complaint_details" type="button" class="btn btn-outline-light w-100 h-100">
<i class="fa fa-times me-1"></i>
<span class="d-none d-sm-inline">Close</span>
</button>
</div>
</div>
</div>
</nav>
</div>
<!-- Complaint details content -->
<div class="container-fluid mt-2 complaint-details-page">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<!-- Outer card -->
<div class="card border-0 shadow-sm">
<div class="card-body p-3 bg-light">
<!-- Summary panel -->
<div class="card mb-3">
<div class="card-body py-2">
<div class="row gy-1">
<div class="col-5 col-sm-4 fw-semibold">Complaint</div>
<div class="col-7 col-sm-8" id="lbl_complaint_number"></div>
<div class="col-5 col-sm-4 fw-semibold">Priority</div>
<div class="col-7 col-sm-8" id="lbl_priority"></div>
<div class="d-flex flex-column h-100 w-100 overflow-hidden">
<div class="flex-grow-1 d-flex flex-column overflow-auto bg-light p-2 p-md-3" style="min-height:0;">
<div class="col-5 col-sm-4 fw-semibold">Status</div>
<div class="col-7 col-sm-8" id="lbl_status"></div>
<!-- Sticky block: Summary header + (expanded) summary body + buttons -->
<div class="sticky-top bg-light" style="z-index:20;">
<div class="col-5 col-sm-4 fw-semibold">Dispatch Code</div>
<div class="col-7 col-sm-8" id="lbl_dispatch_code"></div>
<!-- Summary header -->
<div class="card border-0 shadow-sm mb-2">
<div class="card-header bg-white py-2">
<button
class="btn btn-link text-decoration-none p-0 w-100 d-flex align-items-center justify-content-between summary-toggle"
type="button"
data-bs-toggle="collapse"
data-bs-target="#cdetails_summary"
aria-expanded="true"
aria-controls="cdetails_summary">
<span class="fw-semibold text-dark" id="lbl_summary_title">Summary</span>
<div class="col-5 col-sm-4 fw-semibold">Dispatch District</div>
<div class="col-7 col-sm-8" id="lbl_dispatch_district"></div>
<span class="summary-chevron" aria-hidden="true">
<svg class="summary-chevron-icon" viewBox="0 0 16 16" focusable="false">
<path fill="currentColor" d="M7.646 5.354a.5.5 0 0 1 .708 0l5 5a.5.5 0 0 1-.708.708L8 6.414 3.354 11.06a.5.5 0 1 1-.708-.708l5-5z"/>
</svg>
</span>
</button>
</div>
</div>
<div class="col-5 col-sm-4 fw-semibold">Address</div>
<div class="col-7 col-sm-8" id="lbl_address"></div>
</div>
</div>
</div>
<!-- Summary body (stays sticky because it's inside the sticky block) -->
<div id="cdetails_summary" class="collapse show">
<div class="card border-0 shadow-sm mb-2">
<div class="card-body py-2">
<!-- Action tabs (green buttons) -->
<div class="mb-3">
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-success btn-sm px-3" id="btn_history">History</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_warnings">Warnings</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_contacts">Contacts</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_cmp">CMP</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_e911">E-911</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_rem">REM</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_unt">UNT</button>
</div>
</div>
<table class="table table-sm mb-0 w-100">
<tbody>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Priority:</th>
<td class="w-100" id="lbl_priority"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Status:</th>
<td class="w-100" id="lbl_status"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Dispatch Code:</th>
<td class="w-100" id="lbl_dispatch_code"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Dispatch District:</th>
<td class="w-100" id="lbl_dispatch_district"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Address:</th>
<td class="w-100" id="lbl_address"></td>
</tr>
<tr id="row_business" class="d-none">
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Business:</th>
<td class="w-100" id="lbl_business"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Details table (thead in HTML, tbody filled by TWebDBListControl) -->
<div class="card">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
<table class="table table-sm table-striped mb-0">
<thead class="table-light sticky-top">
<tr>
<th style="width: 70px;">Type</th>
<th style="width: 150px;">Timestamp</th>
<th>Remarks</th>
</tr>
</thead>
<tbody id="tbl_complaint_details"></tbody>
</table>
</div>
</div>
</div>
<!-- Buttons -->
<div class="pb-2">
<div class="d-flex flex-wrap gap-2 justify-content-center">
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_remarks" aria-pressed="true">Remarks</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_history">History</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_warnings">Warnings</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_contacts">Contacts</button>
</div>
<!-- Remarks toggle filters (only visible on Remarks tab) -->
<div class="d-flex flex-wrap gap-2 justify-content-center mt-2" id="row_remarks_toggles">
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_cmp" aria-pressed="true">CMP</button>
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_e911" aria-pressed="true">E-911</button>
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_rem" aria-pressed="true">REM</button>
<button type="button" class="btn btn-success btn-sm px-3 active" id="btn_unt" aria-pressed="true">UNT</button>
</div>
</div>
</div>
<!-- Tables (scroll with page) -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div id="tbl_remarks"></div>
<div id="tbl_history" class="tab-hidden"></div>
<div id="tbl_warnings" class="d-none d-md-block"></div>
<div id="lst_warnings" class="d-block d-md-none"></div>
<div id="tbl_contacts" class="tab-hidden"></div>
</div>
</div>
</div>
<button id="btn_complaint_view_on_map"
type="button"
class="btn btn-primary btn-sm shadow position-fixed"
style="right: 12px; bottom: 72px; z-index: 1040;">
View On Map
</button>
</div>
......@@ -4,14 +4,111 @@ interface
uses
System.SysUtils, System.Classes, JS, Web,
WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls,
WEBLib.Grids, WEBLib.DBCtrls;
WEBLib.Forms, WEBLib.Dialogs, WEBLib.Graphics,
XData.Web.Client,
ConnectionModule,
Utils, Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls,
Data.DB, WEBLib.DB, XData.Web.JsonDataset, XData.Web.Dataset,
Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.Lists;
type
TFViewComplaintDetails = class(TWebForm)
WebDBTableControl1: TWebDBTableControl;
xdwcComplaintDetails: TXDataWebClient;
xdwdsRemarks: TXDataWebDataSet;
wdsRemarks: TWebDataSource;
xdwdsRemarksMemoType: TStringField;
xdwdsRemarksTimestamp: TStringField;
xdwdsRemarksRemarks: TStringField;
xdwdsRemarksBadgeNumber: TStringField;
xdwdsRemarksMemoId: TStringField;
xdwdsRemarksCFSId: TStringField;
xdwdsHistory: TXDataWebDataSet;
wdsHistory: TWebDataSource;
xdwdsHistoryApartment: TStringField;
xdwdsHistoryComplaint: TStringField;
xdwdsHistoryDateReported: TStringField;
xdwdsHistoryDPriority: TStringField;
xdwdsHistoryDCallType: TStringField;
xdwdsContacts: TXDataWebDataSet;
wdsContacts: TWebDataSource;
xdwdsContactsName: TStringField;
xdwdsContactsPhone: TStringField;
xdwdsContactsDContactType: TStringField;
xdwdsContactsRemarks: TStringField;
xdwdsWarnings: TXDataWebDataSet;
wdsWarnings: TWebDataSource;
xdwdsWarningsCodeDesc: TStringField;
xdwdsWarningsAddress: TStringField;
xdwdsWarningsNotes: TStringField;
tblRemarks: TWebDBTableControl;
tblContacts: TWebDBTableControl;
tblHistory: TWebDBTableControl;
tblWarnings: TWebDBTableControl;
btnRemarks: TWebButton;
btnHistory: TWebButton;
btnWarnings: TWebButton;
btnContacts: TWebButton;
btnCmp: TWebButton;
btnE911: TWebButton;
btnREM: TWebButton;
btnUnt: TWebButton;
lstWarnings: TWebDBListControl;
btnComplaintViewOnMap: TWebButton;
procedure btnRemarksClick(Sender: TObject);
procedure btnHistoryClick(Sender: TObject);
procedure btnContactsClick(Sender: TObject);
procedure btnWarningsClick(Sender: TObject);
procedure btnCmpClick(Sender: TObject);
procedure btnComplaintViewOnMapClick(Sender: TObject);
procedure btnE911Click(Sender: TObject);
procedure btnREMClick(Sender: TObject);
procedure btnUntClick(Sender: TObject);
private
FComplaintId: string;
FCfsId: string;
FLoading: Boolean;
FAllMemos: TJSArray;
FShowCmp: Boolean;
FShowE911: Boolean;
FShowRem: Boolean;
FShowUnt: Boolean;
FHasHistory: Boolean;
FHasContacts: Boolean;
FHasWarnings: Boolean;
FHistoryLoaded: Boolean;
FContactsLoaded: Boolean;
FWarningsLoaded: Boolean;
procedure WireUi;
procedure CloseClick(Event: TJSEvent);
procedure SetTextById(const elementId, value: string);
function MemoTypeInList(const memoType: string; const list: array of string): Boolean;
function MemoTypeLabel(const memoTypeCode: string): string;
function DeriveStatusFromDates(const dateDispatched, dateArrived, dateCleared: string): string;
procedure SetDataSetJsonData(dataSet: TXDataWebDataSet; dataArr: TJSArray);
procedure SetHiddenById(const elementId: string; hidden: Boolean);
procedure SetActiveTab(const tabName: string);
procedure UpdateToggleButtonCss(const buttonId: string; isOn: Boolean);
procedure UpdateTabButtonCss(const activeTabName: string);
procedure SetButtonEnabledById(const buttonId: string; enabled: Boolean);
procedure ApplyRemarksFilters;
[async] procedure ApplyTabAsync(const tabName: string);
[async] procedure LoadComplaintAsync;
[async] procedure LoadMemosAsync;
[async] procedure LoadHistoryAsync;
[async] procedure LoadContactsAsync;
[async] procedure LoadWarningsAsync;
public
class function CreateForm(AElementID, ComplaintId: string): TWebForm;
procedure InitializeForm;
......@@ -22,6 +119,10 @@ var
implementation
uses
View.Main,
View.Complaints;
{$R *.dfm}
class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string): TWebForm;
......@@ -31,7 +132,7 @@ class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string
with TFViewComplaintDetails(AForm) do
begin
FComplaintId := ComplaintId;
InitializeForm; // kick off loading / UI binding here
InitializeForm;
end;
end;
......@@ -41,153 +142,578 @@ end;
procedure TFViewComplaintDetails.InitializeForm;
begin
// TODO:
// - call your XData endpoint with FComplaintId
// - bind fields / populate controls
// - handle spinner / errors as you do elsewhere
xdwcComplaintDetails.Connection := DMConnection.ApiConnection;
xdwdsRemarks.Connection := DMConnection.ApiConnection;
xdwdsHistory.Connection := DMConnection.ApiConnection;
xdwdsContacts.Connection := DMConnection.ApiConnection;
xdwdsWarnings.Connection := DMConnection.ApiConnection;
FShowCmp := True;
FShowE911 := True;
FShowRem := True;
FShowUnt := True;
FHasHistory := True;
FHasContacts := True;
FHasWarnings := True;
FHistoryLoaded := False;
FContactsLoaded := False;
FWarningsLoaded := False;
WireUi;
LoadComplaintAsync;
end;
end.
procedure TFViewComplaintDetails.WireUi;
var
btnClose: TJSElement;
begin
btnClose := Document.getElementById('btn_close_complaint_details');
if btnClose <> nil then
btnClose.addEventListener('click', @CloseClick);
end;
procedure TFViewComplaintDetails.CloseClick(Event: TJSEvent);
begin
Event.preventDefault;
if Assigned(FViewMain) then
begin
FViewMain.SetActiveNavButton('view.main.btncomplaints');
FViewMain.ShowForm(TFViewComplaints);
end;
end;
procedure TFViewComplaintDetails.SetTextById(const elementId, value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if el <> nil then
TJSHtmlElement(el).innerText := value;
end;
function TFViewComplaintDetails.MemoTypeInList(const memoType: string; const list: array of string): Boolean;
var
i: Integer;
begin
Result := False;
for i := Low(list) to High(list) do
begin
if memoType = list[i] then
Exit(True);
end;
end;
function TFViewComplaintDetails.MemoTypeLabel(const memoTypeCode: string): string;
begin
if MemoTypeInList(memoTypeCode, ['30', '31', '32', '33', '34']) then
Exit('UNT');
if MemoTypeInList(memoTypeCode, ['3', '4']) then
Exit('CMP');
if MemoTypeInList(memoTypeCode, ['2']) then
Exit('E-911');
if MemoTypeInList(memoTypeCode, ['1']) then
Exit('REM');
Result := memoTypeCode;
end;
function TFViewComplaintDetails.DeriveStatusFromDates(const dateDispatched, dateArrived, dateCleared: string): string;
begin
if Trim(dateCleared) <> '' then
Exit('Cleared');
if Trim(dateArrived) <> '' then
Exit('On Scene');
if Trim(dateDispatched) <> '' then
Exit('Dispatched');
Result := 'Pending';
end;
procedure TFViewComplaintDetails.SetDataSetJsonData(dataSet: TXDataWebDataSet; dataArr: TJSArray);
begin
dataSet.Close;
dataSet.SetJsonData(dataArr);
dataSet.Open;
end;
procedure TFViewComplaintDetails.SetHiddenById(const elementId: string; hidden: Boolean);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if el = nil then
Exit;
if hidden then
TJSElement(el).classList.add('tab-hidden')
else
TJSElement(el).classList.remove('tab-hidden');
end;
procedure TFViewComplaintDetails.UpdateToggleButtonCss(const buttonId: string; isOn: Boolean);
var
el: TJSElement;
btn: TJSHTMLButtonElement;
begin
el := Document.getElementById(buttonId);
if el = nil then
Exit;
btn := TJSHTMLButtonElement(el);
btn.classList.remove('btn-success');
btn.classList.remove('btn-secondary');
btn.classList.remove('active');
if isOn then
begin
btn.classList.add('btn-success');
btn.classList.add('active');
btn.setAttribute('aria-pressed', 'true');
end
else
begin
btn.classList.add('btn-secondary');
btn.setAttribute('aria-pressed', 'false');
end;
end;
procedure TFViewComplaintDetails.UpdateTabButtonCss(const activeTabName: string);
var
tabRemarks: TJSElement;
tabHistory: TJSElement;
tabContacts: TJSElement;
tabWarnings: TJSElement;
procedure SetTab(el: TJSElement; isActive: Boolean);
var
btn: TJSHTMLButtonElement;
begin
if el = nil then
Exit;
btn := TJSHTMLButtonElement(el);
btn.classList.remove('active');
if isActive then
begin
btn.classList.add('active');
btn.setAttribute('aria-pressed', 'true');
end
else
btn.setAttribute('aria-pressed', 'false');
end;
begin
tabRemarks := Document.getElementById('btn_remarks');
tabHistory := Document.getElementById('btn_history');
tabContacts := Document.getElementById('btn_contacts');
tabWarnings := Document.getElementById('btn_warnings');
SetTab(tabRemarks, activeTabName = 'remarks');
SetTab(tabHistory, activeTabName = 'history');
SetTab(tabContacts, activeTabName = 'contacts');
SetTab(tabWarnings, activeTabName = 'warnings');
end;
procedure TFViewComplaintDetails.SetButtonEnabledById(const buttonId: string; enabled: Boolean);
var
el: TJSElement;
btn: TJSHTMLButtonElement;
begin
el := Document.getElementById(buttonId);
if el = nil then
Exit;
btn := TJSHTMLButtonElement(el);
btn.disabled := not enabled;
btn.classList.remove('disabled');
btn.classList.remove('btn-success');
btn.classList.remove('btn-secondary');
if enabled then
begin
btn.classList.add('btn-success');
end
else
begin
btn.classList.add('btn-secondary');
btn.classList.add('disabled');
btn.classList.remove('active');
btn.setAttribute('aria-pressed', 'false');
end;
end;
procedure TFViewComplaintDetails.SetActiveTab(const tabName: string);
begin
SetHiddenById('tbl_remarks', tabName <> 'remarks');
SetHiddenById('tbl_history', tabName <> 'history');
SetHiddenById('tbl_contacts', tabName <> 'contacts');
SetHiddenById('tbl_warnings', tabName <> 'warnings');
SetHiddenById('lst_warnings', tabName <> 'warnings');
SetHiddenById('row_remarks_toggles', tabName <> 'remarks');
UpdateTabButtonCss(tabName);
end;
procedure TFViewComplaintDetails.ApplyRemarksFilters;
var
filteredArr: TJSArray;
i: Integer;
rowObj: TJSObject;
memoType: string;
showRow: Boolean;
begin
if not Assigned(FAllMemos) then
Exit;
filteredArr := TJSArray.new;
for i := 0 to Integer(FAllMemos.length) - 1 do
begin
rowObj := TJSObject(FAllMemos[i]);
memoType := string(rowObj['MemoTypeCode']);
showRow := False;
if FShowCmp and MemoTypeInList(memoType, ['3', '4']) then
showRow := True;
if FShowE911 and MemoTypeInList(memoType, ['2']) then
showRow := True;
if FShowRem and MemoTypeInList(memoType, ['1']) then
showRow := True;
if FShowUnt and MemoTypeInList(memoType, ['30', '31', '32', '33', '34']) then
showRow := True;
if showRow then
filteredArr.push(rowObj);
end;
SetDataSetJsonData(xdwdsRemarks, filteredArr);
UpdateToggleButtonCss('btn_cmp', FShowCmp);
UpdateToggleButtonCss('btn_e911', FShowE911);
UpdateToggleButtonCss('btn_rem', FShowRem);
UpdateToggleButtonCss('btn_unt', FShowUnt);
end;
[async] procedure TFViewComplaintDetails.ApplyTabAsync(const tabName: string);
begin
if tabName = 'remarks' then
begin
SetActiveTab('remarks');
ApplyRemarksFilters;
Exit;
end;
if tabName = 'history' then
begin
if not FHasHistory then
Exit;
SetActiveTab('history');
if not FHistoryLoaded then
begin
ShowSpinner('spinner');
try
await(LoadHistoryAsync);
finally
HideSpinner('spinner');
end;
end;
Exit;
end;
if tabName = 'contacts' then
begin
if not FHasContacts then
Exit;
SetActiveTab('contacts');
if not FContactsLoaded then
begin
ShowSpinner('spinner');
try
await(LoadContactsAsync);
finally
HideSpinner('spinner');
end;
end;
Exit;
end;
if tabName = 'warnings' then
begin
if not FHasWarnings then
Exit;
SetActiveTab('warnings');
if not FWarningsLoaded then
begin
ShowSpinner('spinner');
try
await(LoadWarningsAsync);
finally
HideSpinner('spinner');
end;
end;
Exit;
end;
end;
procedure TFViewComplaintDetails.btnRemarksClick(Sender: TObject);
begin
ApplyTabAsync('remarks');
end;
procedure TFViewComplaintDetails.btnHistoryClick(Sender: TObject);
begin
ApplyTabAsync('history');
end;
procedure TFViewComplaintDetails.btnContactsClick(Sender: TObject);
begin
ApplyTabAsync('contacts');
end;
procedure TFViewComplaintDetails.btnWarningsClick(Sender: TObject);
begin
ApplyTabAsync('warnings');
end;
//unit View.ComplaintDetails;
//
//interface
//
//uses
// System.SysUtils, System.Classes, JS, Web,
// WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
// WEBLib.StdCtrls, WEBLib.WebCtrls, WEBLib.ExtCtrls,
// WEBLib.DB, Data.DB,
// XData.Web.Client, XData.Web.JsonDataset, XData.Web.Dataset,
// View.Main, View.Complaints, Vcl.Controls, WEBLib.Grids, WEBLib.DBCtrls;
//
//type
// TFViewComplaintDetails = class(TWebForm)
// // Header controls (ElementID -> HTML ids in snake_case)
// btnCloseComplaintDetails: TWebButton; // ElementID = 'btn_close_complaint_details'
// lblComplaintNumber: TWebLabel; // 'lbl_complaint_number'
// lblPriority: TWebLabel; // 'lbl_priority'
// lblStatus: TWebLabel; // 'lbl_status'
// lblDispatchCode: TWebLabel; // 'lbl_dispatch_code'
// lblDispatchDistrict: TWebLabel; // 'lbl_dispatch_district'
// lblAddress: TWebLabel; // 'lbl_address'
//
// // Action buttons (optional events later)
// btnHistory: TWebButton; // 'btn_history'
// btnWarnings: TWebButton; // 'btn_warnings'
// btnContacts: TWebButton; // 'btn_contacts'
// btnCMP: TWebButton; // 'btn_cmp'
// btnE911: TWebButton; // 'btn_e911'
// btnREM: TWebButton; // 'btn_rem'
// btnUNT: TWebButton; // 'btn_unt'
//
// // Data-aware table
// tblComplaintDetails: TWebDBListControl; // ElementID = 'tbl_complaint_details'
// xdwcComplaintDetails: TXDataWebClient;
// xdwdsxComplaintDetails: TXDataWebDataSet; // (your requested name)
// wdsComplaintDetails: TWebDataSource;
//
// private
// FComplaintId: string;
//
// [async] procedure LoadHeaderAsync;
// [async] procedure LoadRowsAsync;
//
// public
// class function CreateForm(AElementID, ComplaintId: string): TWebForm;
// procedure InitializeForm;
// end;
//
//implementation
//
//{$R *.dfm}
//
//class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string): TWebForm;
//
// procedure AfterCreate(AForm: TObject);
// begin
// with TFViewComplaintDetails(AForm) do
// begin
// FComplaintId := ComplaintId;
// InitializeForm;
// end;
// end;
//
//begin
// Application.CreateForm(TFViewComplaintDetails, AElementID, Result, @AfterCreate);
//end;
//
//procedure TFViewComplaintDetails.InitializeForm;
//begin
// // Close button navigates back via Main (keeps host container ownership clean)
// btnCloseComplaintDetails.OnClick :=
// procedure(Sender: TObject)
// begin
// if Assigned(FViewMain) then
// begin
//// FViewMain.SetActiveNavButton('view.main.btncomplaints');
// FViewMain.ShowForm(TFViewComplaints);
// end;
// end;
//
// // Data-aware wiring (design-time also OK; keeping here for clarity)
// wdsComplaintDetails.DataSet := xdwdsxComplaintDetails;
// tblComplaintDetails.DataSource := wdsComplaintDetails;
//
// // Expecting ItemTemplate at design-time:
// // <tr><td>(%MemoType%)</td><td>(%Timestamp%)</td><td>(%Remarks%)</td></tr>
//
// // Kick off loads
// LoadHeaderAsync;
// LoadRowsAsync;
//end;
//
//[async] procedure TFViewComplaintDetails.LoadHeaderAsync;
//var
// resp: TXDataClientResponse;
// obj: TJSObject;
//begin
// try
// // TODO:Adjust endpoint and args to match your server (array param is common in XData)
// resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetailsHeader', [FComplaintId]));
// obj := TJSObject(resp.Result);
//
// // If the server returns { data: {...} }, unwrap:
// if obj['data'] <> nil then
// obj := TJSObject(obj['data']);
//
// lblComplaintNumber.Caption := string(obj['ComplaintNumber'] ?? FComplaintId);
// lblPriority.Caption := string(obj['Priority'] ?? '');
// lblStatus.Caption := string(obj['Status'] ?? '');
// lblDispatchCode.Caption := string(obj['DispatchCodeDesc'] ?? '');
// lblDispatchDistrict.Caption := string(obj['DispatchDistrict'] ?? '');
// lblAddress.Caption := string(obj['Address'] ?? '');
// except
// on E: Exception do
// Console.Log('Header load error: ' + E.Message);
// end;
//end;
//
//[async] procedure TFViewComplaintDetails.LoadRowsAsync;
//var
// resp: TXDataClientResponse;
// root: TJSObject;
//begin
// try
// // Adjust endpoint name/params to your API
// resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetailsRows', [FComplaintId]));
// root := TJSObject(resp.Result);
//
// // Expecting { data: [ { MemoType, Timestamp, Remarks }, ... ] }
// xdwdsxComplaintDetails.Close;
// xdwdsxComplaintDetails.SetJsonData(root['data']);
// xdwdsxComplaintDetails.Open;
// except
// on E: Exception do
// Console.Log('Rows load error: ' + E.Message);
// end;
//end;
//
//end.
procedure TFViewComplaintDetails.btnCmpClick(Sender: TObject);
begin
FShowCmp := not FShowCmp;
ApplyRemarksFilters;
end;
procedure TFViewComplaintDetails.btnComplaintViewOnMapClick(Sender: TObject);
begin
if Assigned(FViewMain) then
FViewMain.ShowMapFocusComplaint(FComplaintId);
end;
procedure TFViewComplaintDetails.btnE911Click(Sender: TObject);
begin
FShowE911 := not FShowE911;
ApplyRemarksFilters;
end;
procedure TFViewComplaintDetails.btnREMClick(Sender: TObject);
begin
FShowRem := not FShowRem;
ApplyRemarksFilters;
end;
procedure TFViewComplaintDetails.btnUntClick(Sender: TObject);
begin
FShowUnt := not FShowUnt;
ApplyRemarksFilters;
end;
[async] procedure TFViewComplaintDetails.LoadComplaintAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataObj: TJSObject;
businessRowEl: TJSHTMLElement;
complaintText: string;
priorityText: string;
statusText: string;
dispatchDescText: string;
dispatchDistrictText: string;
addressText: string;
businessText: string;
dateDispatchedText: string;
dateArrivedText: string;
dateClearedText: string;
historyFlag: string;
contactsFlag: string;
warningsFlag: string;
begin
if FLoading then
Exit;
FLoading := True;
ShowSpinner('spinner');
try
try
resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetails', [FComplaintId]));
rootObj := TJSObject(resp.Result);
dataObj := TJSObject(rootObj['data']);
complaintText := string(dataObj['Complaint']);
priorityText := string(dataObj['Priority']);
dispatchDescText := string(dataObj['DispatchCodeDesc']);
dispatchDistrictText := string(dataObj['DispatchDistrict']);
addressText := string(dataObj['Address']);
businessText := '';
if dataObj['Business'] <> nil then
businessText := string(dataObj['Business']);
FCfsId := string(dataObj['CFSId']);
statusText := '';
if dataObj['Status'] <> nil then
statusText := string(dataObj['Status']);
dateDispatchedText := '';
dateArrivedText := '';
dateClearedText := '';
if dataObj['DateDispatched'] <> nil then
dateDispatchedText := string(dataObj['DateDispatched']);
if dataObj['DateArrived'] <> nil then
dateArrivedText := string(dataObj['DateArrived']);
if dataObj['DateCleared'] <> nil then
dateClearedText := string(dataObj['DateCleared']);
if Trim(statusText) = '' then
statusText := DeriveStatusFromDates(dateDispatchedText, dateArrivedText, dateClearedText);
// Note: reenable if using the complaint in list html that is currently commented out
// SetTextById('lbl_complaint_number', complaintText);
SetTextById('lbl_summary_title', 'Summary for Complaint ' + complaintText);
SetTextById('lbl_priority', priorityText);
SetTextById('lbl_status', statusText);
SetTextById('lbl_dispatch_code', dispatchDescText);
SetTextById('lbl_dispatch_district', dispatchDistrictText);
SetTextById('lbl_address', addressText);
SetTextById('lbl_business', businessText);
businessRowEl := TJSHTMLElement(document.getElementById('row_business'));
if businessRowEl <> nil then
begin
if Trim(businessText) = '' then
businessRowEl.classList.add('d-none')
else
businessRowEl.classList.remove('d-none');
end;
historyFlag := '';
contactsFlag := '';
warningsFlag := '';
if dataObj['History'] <> nil then
historyFlag := string(dataObj['History']);
if dataObj['Contacts'] <> nil then
contactsFlag := string(dataObj['Contacts']);
if dataObj['Warnings'] <> nil then
warningsFlag := string(dataObj['Warnings']);
FHasHistory := historyFlag <> '-1';
FHasContacts := contactsFlag <> '-1';
FHasWarnings := warningsFlag <> '-1';
SetButtonEnabledById('btn_history', FHasHistory);
SetButtonEnabledById('btn_contacts', FHasContacts);
SetButtonEnabledById('btn_warnings', FHasWarnings);
await(LoadMemosAsync);
SetActiveTab('remarks');
ApplyRemarksFilters;
except
on E: EXDataClientRequestException do
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
on E: Exception do
Utils.ShowErrorModal(E.Message);
end;
finally
HideSpinner('spinner');
FLoading := False;
end;
end;
[async] procedure TFViewComplaintDetails.LoadMemosAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataArr: TJSArray;
i: Integer;
rowObj: TJSObject;
memoTypeCode: string;
begin
if Trim(FCfsId) = '' then
Exit;
resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintMemos', [FCfsId]));
rootObj := TJSObject(resp.Result);
dataArr := TJSArray(rootObj['data']);
FAllMemos := dataArr;
for i := 0 to Integer(dataArr.length) - 1 do
begin
rowObj := TJSObject(dataArr[i]);
memoTypeCode := string(rowObj['MemoType']);
rowObj['MemoTypeCode'] := memoTypeCode;
rowObj['MemoType'] := MemoTypeLabel(memoTypeCode);
end;
SetDataSetJsonData(xdwdsRemarks, dataArr);
end;
[async] procedure TFViewComplaintDetails.LoadHistoryAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataArr: TJSArray;
begin
resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintHistory', [FComplaintId]));
rootObj := TJSObject(resp.Result);
dataArr := TJSArray(rootObj['data']);
SetDataSetJsonData(xdwdsHistory, dataArr);
FHistoryLoaded := True;
end;
[async] procedure TFViewComplaintDetails.LoadContactsAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataArr: TJSArray;
begin
resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintContacts', [FComplaintId]));
rootObj := TJSObject(resp.Result);
dataArr := TJSArray(rootObj['data']);
SetDataSetJsonData(xdwdsContacts, dataArr);
FContactsLoaded := True;
end;
[async] procedure TFViewComplaintDetails.LoadWarningsAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataArr: TJSArray;
begin
resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintWarnings', [FComplaintId]));
rootObj := TJSObject(resp.Result);
dataArr := TJSArray(rootObj['data']);
SetDataSetJsonData(xdwdsWarnings, dataArr);
FWarningsLoaded := True;
end;
end.
......@@ -24,49 +24,6 @@ object FViewComplaints: TFViewComplaints
Visible = False
WidthPercent = 100.000000000000000000
end
object btnGroup: TWebButton
Left = 180
Top = 110
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 242
Top = 110
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnRefresh: TWebButton
Left = 114
Top = 110
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object dblComplaintsList: TWebDBListControl
Left = 36
Top = 148
......@@ -86,18 +43,21 @@ object FViewComplaints: TFViewComplaints
DataSource = wdsComplaints
ItemTemplate =
'<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="card' +
'-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 d' +
'-flex align-items-center"> <span> (%Complaint%): (%S' +
'tatus%)&nbsp;&nbsp;(%DistrictSector%) </span> <button ' +
'type="button" class="btn btn-primary btn-sm ms-auto' +
' complaint-details-btn" data-id="(%ComplaintId%)"> ' +
' Details </button> </div> <div class="small tex' +
't-opacity-75">(%DateReported%)</div> </div></div>'
'ext-white rounded-1 px-2 mb-1">(%DistrictHeader%)</div><div clas' +
's="card border shadow-sm" style="--bs-card-bg:(%PriorityColor%);' +
'--bs-card-color:(%PriorityTextColor%);"><div class="card-body py' +
'-2 px-3 d-flex gap-2"><div class="flex-grow-1"><div class="fw-bo' +
'ld text-uppercase small">(%Priority%): (%DispatchCodeDesc%)</div' +
'><div class="small">(%Address%)</div><div class="small d-none co' +
'mplaint-business" data-business="(%Business%)">(%Business%)</div' +
'><div class="small text-opacity-75">(%Complaint%): (%Status%)&nb' +
'sp;&nbsp;(%DistrictSector%)</div><div class="small text-opacity-' +
'75">(%DateReported%)</div></div><div class="d-flex flex-column j' +
'ustify-content-center gap-1"><button type="button" class="btn bt' +
'n-primary btn-sm complaint-details-btn" data-id="(%ComplaintId%)' +
'">Details</button><button type="button" class="btn btn-primary b' +
'tn-sm btn-complaint-map" data-id="(%ComplaintId%)">View On Map</' +
'button></div></div></div>'
ListSource = wdsComplaints
end
object xdwcComplaints: TXDataWebClient
......@@ -127,6 +87,9 @@ object FViewComplaints: TFViewComplaints
object xdwdsComplaintsAddress: TStringField
FieldName = 'Address'
end
object xdwdsComplaintsBusiness: TStringField
FieldName = 'Business'
end
object xdwdsComplaintsStatus: TStringField
FieldName = 'Status'
end
......
<div class="d-flex flex-column h-100">
<div class="sticky-top">
<!-- Local navbar (Complaints) -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="complaints_title" class="navbar-brand mb-0 h5 text-white">Complaints</span>
</div>
<div class="col">
<button id="complaints_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="complaints_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="complaints_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0">
<!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2">
<div class="container-fluid">
<div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="complaints_search" class="form-control" placeholder="Search...">
<!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2">
<div class="container-fluid">
<div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="complaints_search" class="form-control" placeholder="Search...">
</div>
</div>
</div>
</div>
</div>
<!-- Complaints list container -->
<div class="container-fluid mt-2">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<!-- This is where the DBListControl will inject cards -->
<div id="complaints_dbl_complaint_list" class="d-flex flex-column gap-2">
<!-- Cards will render here -->
<!-- Scrolling list area -->
<div class="flex-grow-1 overflow-auto" style="min-height:0;">
<div class="container-fluid mt-2">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<!-- This is where the DBListControl will inject cards -->
<div id="complaints_dbl_complaint_list" class="d-flex flex-column gap-2">
<!-- Cards will render here -->
</div>
<label id="complaints_lblentries" class="mt-2 d-block"></label>
</div>
</div>
<label id="complaints_lblentries" class="mt-2 d-block"></label>
<!-- Pagination -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center" id="pagination">
<!-- Pagination items rendered in Delphi -->
</ul>
</nav>
</div>
</div>
</div>
</div>
......@@ -24,9 +24,6 @@ type
xdwdsComplaintsDispatchDistrict: TStringField;
xdwdsComplaintsDateReported: TStringField;
lblEntries: TWebLabel;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsComplaints: TWebDataSource;
xdwdsComplaintsComplaintId: TStringField;
xdwdsComplaintsDistrictHeader: TStringField;
......@@ -35,6 +32,7 @@ type
xdwdsComplaintsPriorityTextColor: TStringField;
xdwdsComplaintsDistrictSector: TStringField;
tmrRefresh: TWebTimer;
xdwdsComplaintsBusiness: TStringField;
procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject);
......@@ -44,6 +42,7 @@ type
FLoading: Boolean;
[async] procedure GetComplaints;
procedure HandleListClick(e: TJSMouseEvent);
procedure ShowHideBusinessRows;
public
property OnShowDetails: TSelectProc read FSelectProc write FSelectProc;
end;
......@@ -62,94 +61,135 @@ begin
tmrRefresh.Enabled := False;
GetComplaints;
tmrRefresh.Enabled := True;
asm
if (!window.showComplaintDetails) {
window.showComplaintDetails = function (id) {
console.log('JS bridge showComplaintDetails called, id=', id);
try {
pas['View.Main'].FViewMain.ShowComplaintDetails(id);
} catch (e) {
console.log('Error in TFViewMain.ShowComplaintDetails', e);
}
};
}
end;
end;
procedure TFViewComplaints.ShowHideBusinessRows;
var
nodes: TJSNodeList;
i: Integer;
el: TJSHTMLElement;
businessText: string;
begin
nodes := document.querySelectorAll('.complaint-business');
for i := 0 to nodes.length - 1 do
begin
el := TJSHTMLElement(nodes.item(i));
businessText := string(el.getAttribute('data-business'));
if Trim(businessText) <> '' then
el.classList.remove('d-none')
else
el.classList.add('d-none');
end;
end;
procedure TFViewComplaints.HandleListClick(e: TJSMouseEvent);
var el: TJSElement; id: string;
var
el: TJSElement;
btn: TJSElement;
complaintId: string;
begin
btn := nil;
el := TJSElement(e.target);
if (el is TJSHtmlElement) and TJSHtmlElement(el).classList.contains('complaint-details-btn') then
asm
btn = (el && el.closest) ? el.closest('.complaint-details-btn') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
id := string(TJSHtmlElement(el).dataset['id']); // comes from (%ComplaintId%)
complaintId := string(TJSHtmlElement(btn).getAttribute('data-id'));
e.preventDefault;
e.stopPropagation;
if Assigned(FSelectProc) then
FSelectProc(id);
asm
if (window.showComplaintDetails) window.showComplaintDetails(complaintId);
end;
end
else
begin
btn := nil;
asm
btn = (el && el.closest) ? el.closest('.btn-complaint-map') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
complaintId := string(TJSHtmlElement(btn).getAttribute('data-id'));
e.preventDefault;
e.stopPropagation;
asm
pas['View.Main'].FViewMain.ShowMapFocusComplaint(complaintId);
end;
end;
end;
end;
procedure TFViewComplaints.WebFormDestroy(Sender: TObject);
begin
Document.removeEventListener('click', @HandleListClick);
end;
//HTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property
//Note: HTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property
procedure TFViewComplaints.btnRefreshClick(Sender: TObject);
begin
GetComplaints;
end;
procedure TFViewComplaints.GetComplaints;
[async] procedure TFViewComplaints.GetComplaints;
var
xdcResponse: TXDataClientResponse;
respObj: TJSObject;
complaintsCount: Integer;
begin
if FLoading then Exit;
FLoading := True;
console.log('GetComplaints: Invoking API...');
if FLoading then
Exit;
FLoading := True;
ShowSpinner('spinner');
try
try
xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', []));
console.log('RawInvoke returned:', xdcResponse.Result);
respObj := TJSObject(xdcResponse.Result);
xdwdsComplaints.Close;
console.log('Dataset closed');
xdwdsComplaints.SetJsonData(respObj['data']);
console.log('JsonData set on dataset:', respObj['data']);
xdwdsComplaints.Open;
console.log('PriorityColor field name = ' +
xdwdsComplaintsPriorityColor.FieldName +
' sample value = ' +
xdwdsComplaintsPriorityColor.AsString);
if xdwdsComplaints.RecordCount > 0 then
begin
console.log('First record - Complaint:' + xdwdsComplaints.FieldByName('Complaint').AsString);
end;
ShowHideBusinessRows;
complaintsCount := Integer(respObj['count']);
lblEntries.Caption := Format('%d active complaints', [complaintsCount]);
console.log('Label updated:' + lblEntries.Caption);
except
on E: EXDataClientRequestException do
begin
console.log('XData exception:' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
on E: Exception do
Utils.ShowErrorModal(E.Message);
end;
finally
console.log('GetComplaints complete');
HideSpinner('spinner');
FLoading := False;
end;
HideSpinner('spinner');
end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin
GetComplaints;
console.log('tmrRefreshTimer fired');
end;
end.
......
......@@ -77,6 +77,17 @@ object FViewMain: TFViewMain
OnClick = lblUsersClick
Caption = 'Users'
end
object lblMainTitle: TWebLabel
Left = 131
Top = 31
Width = 3
Height = 15
ElementID = 'lbl_main_title'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel
Left = 136
Top = 110
......
<div class="d-flex flex-column vh-100">
<!-- Top Nav -->
<nav class="navbar navbar-light bg-primary border-light text-light fixed-top">
<nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
<div class="container-fluid">
<!-- Left: Font button -->
<button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm">
font
</button>
<!-- Center: App title -->
<a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a>
<!-- App title + current view title -->
<div class="d-flex align-items-center ms-3">
<a id="view.main.apptitle" class="navbar-brand fw-bold text-light mb-0 me-1" href="index.html">emiMobile</a>
<span class="navbar-brand text-light mb-0 mx-0">-</span>
<span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span>
</div>
<!-- Right: Connection label -->
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto">Connected</span>
</div>
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span>
</div>
</nav>
<!-- Main content: fills space between navbars -->
<main id="main.webpanel" class="flex-grow-1 overflow-auto mt-5 mb-5">
<main id="main.webpanel" class="flex-grow-1 position-relative p-0 overflow-hidden" style="min-height:0;">
<!-- TWebPanel content gets injected here -->
</main>
<!-- Bottom Nav -->
<nav class="navbar navbar-dark bg-primary fixed-bottom py-2">
<nav class="navbar navbar-dark bg-primary py-2 flex-shrink-0">
<div class="container-fluid">
<div class="d-flex justify-content-center gap-3 w-100">
<button id="view.main.btnmap" type="button" class="btn btn-primary">
......@@ -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 -->
......
......@@ -24,6 +24,7 @@ type
btnComplaints: TWebButton;
btnUnits: TWebButton;
tmrBadgeCounts: TWebTimer;
lblMainTitle: TWebLabel;
procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject);
procedure wllblUserProfileClick(Sender: TObject);
......@@ -44,8 +45,9 @@ type
procedure ShowCrudForm( AFormClass: TWebFormClass );
//procedure EditUser( AParam, BParam, CParam, DParam, EParam: string);
function GetUserInfo: string;
procedure SetActiveNavButton(const BtnId: string);
[async] procedure RefreshBadgesAsync;
procedure ShowUnitDetails(UnitId: string);
procedure SetHeaderTitle(const title: string);
public
{ Public declarations }
class procedure Display(LogoutProc: TLogoutProc);
......@@ -53,6 +55,9 @@ type
procedure EditUser( Mode, FullName, Username, Phone, Email: string; admin, active: boolean);
procedure ShowUserForm(Info: string);
procedure ShowComplaintDetails(ComplaintId: string);
procedure SetActiveNavButton(const BtnId: string);
procedure ShowMapFocusUnit(const unitId: string);
procedure ShowMapFocusComplaint(const complaintId: string);
end;
var
......@@ -71,6 +76,7 @@ uses
View.Admin,
View.Users,
View.EditUser,
View.UnitDetails,
Utils;
{$R *.dfm}
......@@ -87,9 +93,20 @@ begin
lblUsers.Visible := false;
Utils.HideSpinner('spinner');
ShowForm(TFViewMap);
SetHeaderTitle('Map');
RefreshBadgesAsync;
end;
procedure TFViewMain.SetHeaderTitle(const title: string);
var el: TJSElement;
begin
el := Document.getElementById('lbl_main_title');
if el <> nil then
el.innerHTML := title
else
console.log('SetHeaderTitle: lbl_main_title not found');
end;
procedure TFViewMain.lblUsersClick(Sender: TObject);
begin
......@@ -136,6 +153,7 @@ end;
procedure TFViewMain.btnComplaintsClick(Sender: TObject);
begin
SetHeaderTitle('Complaints');
SetActiveNavButton('view.main.btncomplaints');
ShowForm(TFViewComplaints);
......@@ -149,12 +167,14 @@ end;
procedure TFViewMain.btnMapClick(Sender: TObject);
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
end;
procedure TFViewMain.btnUnitsClick(Sender: TObject);
begin
SetHeaderTitle('Units');
SetActiveNavButton('view.main.btnunits');
ShowForm(TFViewUnits);
end;
......@@ -197,11 +217,19 @@ end;
procedure TFViewMain.ShowComplaintDetails(ComplaintId: string);
begin
SetHeaderTitle('Complaint Details');
if Assigned(FChildForm) then
FChildForm.Free;
FChildForm := TFViewComplaintDetails.CreateForm(WebPanel1.ElementID, ComplaintId);
end;
procedure TFViewMain.ShowUnitDetails(UnitId: string);
begin
SetHeaderTitle('Unit Details');
if Assigned(FChildForm) then
FChildForm.Free;
FChildForm := TFViewUnitDetails.CreateForm(WebPanel1.ElementID, UnitId);
end;
procedure TFViewMain.SetActiveNavButton(const btnId: string);
......@@ -264,4 +292,41 @@ begin
end;
procedure TFViewMain.ShowMapFocusUnit(const unitId: string);
var
pendingUnitId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingUnitId := unitId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusUnit(pendingUnitId);
end, 50);
end;
procedure TFViewMain.ShowMapFocusComplaint(const complaintId: string);
var
pendingComplaintId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingComplaintId := complaintId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusComplaint(pendingComplaintId);
end, 50);
end;
end.
......@@ -2,81 +2,15 @@ object FViewMap: TFViewMap
Width = 475
Height = 802
ElementFont = efCSS
object btnMenu: TWebButton
Left = 62
Top = 66
Width = 41
Height = 25
Caption = 'Menu'
ChildOrder = 1
ElementID = 'map.btnmenu'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnAlerts: TWebButton
Left = 148
Top = 66
Width = 35
Height = 25
Caption = 'Alerts'
ChildOrder = 1
ElementID = 'map.btnalerts'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnGroups: TWebButton
Left = 194
Top = 66
Width = 41
Height = 25
Caption = 'Groups'
ChildOrder = 1
ElementID = 'map.btngroups'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnLocate: TWebButton
Left = 246
Top = 66
Width = 39
Height = 25
Caption = 'Locate'
ChildOrder = 1
ElementID = 'map.btnlocate'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilters: TWebButton
Left = 297
Top = 66
Width = 35
Height = 25
Caption = 'Filters'
ChildOrder = 1
ElementID = 'map.btnfilters'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnDisplay: TWebButton
Left = 351
Top = 66
Width = 40
Height = 25
Caption = 'Display'
ChildOrder = 1
ElementID = 'map.btndisplay'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object pnlMap: TWebPanel
Left = 62
Top = 120
Width = 335
Height = 555
ElementID = 'map_pnlmap'
Caption = 'pnlMap'
ChildOrder = 7
TabOrder = 6
ElementPosition = epIgnore
TabOrder = 0
object lfMap: TTMSFNCLeaflet
Left = 0
Top = 0
......@@ -114,6 +48,18 @@ object FViewMap: TFViewMap
HeadLinks = <>
end
end
object btnFindLocation: TWebButton
Left = 90
Top = 74
Width = 61
Height = 25
Caption = 'Location'
ChildOrder = 1
ElementID = 'btn_find_location'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFindLocationClick
end
object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText
URL = 'assets/bpddistricts-updated.geojson'
......@@ -128,7 +74,15 @@ object FViewMap: TFViewMap
end
object tmrRefresh: TWebTimer
Interval = 30000
OnTimer = tmrRefreshTimer
Left = 358
Top = 696
end
object tmrLocate: TWebTimer
Enabled = False
Interval = 100
OnTimer = tmrLocateTimer
Left = 174
Top = 74
end
end
<!-- Root wrapper inside main.webpanel -->
<div id="map.root" class="d-flex flex-column" style="height:100%;">
<div id="map.root" class="d-flex flex-column h-100 w-100">
<!-- Local navbar -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<button id="map.btnmenu" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-bars me-1"></i><span class="d-none d-sm-inline">Menu</span>
</button>
</div>
<div class="col">
<button id="map.btnalerts" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-exclamation-circle me-1"></i><span class="d-none d-sm-inline">Alerts</span>
</button>
</div>
<div class="col">
<button id="map.btngroups" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-users me-1"></i><span class="d-none d-sm-inline">Groups</span>
</button>
</div>
<div class="col">
<button id="map.btnlocate" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-location-arrow me-1"></i><span class="d-none d-sm-inline">Locate</span>
</button>
<!-- New: offcavnvas is a bootstrap class that adds an easy slide in modal -->
<div class="offcanvas offcanvas-end"
tabindex="-1"
id="map_filters_offcanvas"
aria-labelledby="map_filters_offcanvas_label"
style="--bs-offcanvas-width: 280px;">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="map_filters_offcanvas_label">Map Filters</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_units" checked>
<label class="form-check-label" for="map_filter_units">Show Units</label>
</div>
<div class="col">
<button id="map.btnfilters" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_complaints" checked>
<label class="form-check-label" for="map_filter_complaints">Show Complaints</label>
</div>
<div class="col">
<button id="map.btndisplay" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sun me-1"></i><span class="d-none d-sm-inline">Display</span>
</button>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_location">
<label class="form-check-label" for="map_filter_location">Show Location</label>
</div>
</div>
</div>
</nav>
<div class="d-grid gap-2 mt-4">
<button type="button" class="btn btn-primary" id="map_filters_apply" data-bs-dismiss="offcanvas">
Apply
</button>
<button type="button" class="btn btn-outline-secondary" id="map_filters_reset">
Reset
</button>
</div>
<!-- Map fills remaining space -->
<div class="flex-grow-1" style="min-height:400px;">
<!-- TWebPanel (pnlMap) will render itself here -->
<div id="map_pnlmap" class="w-100 h-100"></div>
<div class="mt-3 small text-muted">
<span class="fw-semibold">Active:</span>
<span id="map_filters_summary"></span>
</div>
</div>
</div>
<div class="flex-grow-1 position-relative" style="min-height:0;">
<div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div>
</div>
<!-- Locate (bottom-right) -->
<button id="btn_find_location"
type="button"
class="btn btn-primary border shadow position-absolute bottom-0 end-0 me-2 mb-4"
style="z-index:1000;">
<i class="fa fa-crosshairs"></i>
<span class="d-none d-sm-inline">Locate</span>
</button>
<!-- Filters (top-right) -->
<button id="btn_map_filters"
type="button"
class="btn btn-primary position-absolute top-0 end-0 m-2 shadow"
style="z-index:1000;"
data-bs-toggle="offcanvas"
data-bs-target="#map_filters_offcanvas"
aria-controls="map_filters_offcanvas">
<i class="fa fa-sliders-h"></i>
<span class="d-none d-sm-inline">Filters</span>
</button>
</div>
</div>
......@@ -8,33 +8,48 @@ uses
WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
XData.Web.Connection, ConnectionModule, Utils;
XData.Web.Connection, ConnectionModule, Utils, uMapFilters;
type
TFViewMap = class(TWebForm)
btnMenu: TWebButton;
btnAlerts: TWebButton;
btnGroups: TWebButton;
btnLocate: TWebButton;
btnFilters: TWebButton;
btnDisplay: TWebButton;
pnlMap: TWebPanel;
lfMap: TTMSFNCLeaflet;
httpReqGeoJson: TWebHttpRequest;
xdwcMap: TXDataWebClient;
tmrRefresh: TWebTimer;
btnFindLocation: TWebButton;
tmrLocate: TWebTimer;
procedure lfMapMapInitialized(Sender: TObject);
[async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
procedure lfMapCustomizeMarker(Sender: TObject;
var ACustomizeMarker: string);
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrRefreshTimer(Sender: TObject);
procedure btnFindLocationClick(Sender: TObject);
procedure tmrLocateTimer(Sender: TObject);
private
userLocationMarker: TTMSFNCMapsMarker;
geoWatchId: Integer;
FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean;
[async] procedure LoadPointsAsync;
FLoadingPoints: Boolean;
mapFilters: TMapFilters;
FPendingUnitId: string;
FPendingComplaintId: string;
FPendingFocusCoord: TTMSFNCMapsCoordinateRec;
FPendingFocusZoom: Integer;
FDoFocusZoom: Boolean;
[async] procedure LoadPointsAsync(showBusy: Boolean);
function CarIconForDistrict(const DistrictCode: string): string;
procedure UpdateDeviceLocation(lat, lng: Double);
procedure StartDeviceLocation;
procedure ApplyPendingUnitFocus;
procedure ApplyPendingComplaintFocus;
public
procedure FocusUnit(const unitId: string);
procedure FocusComplaint(const complaintId: string);
end;
var
......@@ -53,19 +68,81 @@ begin
FUnitsLoaded := False;
FComplaintsLoaded := False;
httpReqGeoJson.Execute;
{$IFNDEF WIN32}
asm
window.showComplaintDetails = function (id) {
console.log('JS bridge showComplaintDetails called, id=', id);
try {
pas['View.Main'].FViewMain.ShowComplaintDetails(id);
console.log('TFViewMain.ShowComplaintDetails finished OK');
} catch (e) {
console.log('Error in TFViewMain.ShowComplaintDetails', e);
}
};
window.showUnitDetails = function (id) {
console.log('JS bridge showUnitDetails called, id=', id);
try {
pas['View.Main'].FViewMain.ShowUnitDetails(id);
} catch (e) {
console.log('Error in TFViewMain.ShowUnitDetails', e);
}
};
end;
StartDeviceLocation;
end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
if userLocationMarker = nil then
begin
userLocationMarker := lfMap.Markers.Add;
userLocationMarker.Title := '<div class="small fw-semibold">You are here</div>';
userLocationMarker.DataString := 'device';
userLocationMarker.IconURL := 'assets/markers/location_dot.png';
userLocationMarker.Visible := False;
end;
userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng;
end;
procedure TFViewMap.StartDeviceLocation;
begin
asm
const self = this;
if (!navigator.geolocation) {
console.log('Geolocation not supported');
return;
}
const onPos = function (pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
try {
self.UpdateDeviceLocation(lat, lng);
} catch (e) {
console.log('UpdateDeviceLocation failed', e);
}
};
const onErr = function (err) {
console.log('Geolocation error:', err && err.message ? err.message : err);
};
if (self.geoWatchId != null && self.geoWatchId !== 0) {
try { navigator.geolocation.clearWatch(self.geoWatchId); } catch(e) {}
self.geoWatchId = 0;
}
self.geoWatchId = navigator.geolocation.watchPosition(onPos, onErr, {
enableHighAccuracy: true,
maximumAge: 5000,
timeout: 15000
});
end;
{$ENDIF}
end;
......@@ -73,14 +150,13 @@ end;
var
i: Integer;
P: TTMSFNCMapsPolygon;
nm: string;
begin
lfMap.BeginUpdate;
try
lfMap.Polygons.Clear;
Console.Log('GeoJSON len=' + AResponse.Length.ToString);
lfMap.LoadGeoJSONFromText(AResponse, True, False);
lfMap.LoadGeoJSONFromText(AResponse, True, Trim(FPendingUnitId) = '');
Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString);
for i := 0 to lfMap.Polygons.Count - 1 do
......@@ -120,14 +196,17 @@ begin
P.StrokeWidth := 2;
end;
if lfMap.Polygons.Count > 0 then
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
finally
lfMap.EndUpdate;
end;
await(LoadPointsAsync);
await(LoadPointsAsync(True));
if mapFilters = nil then
begin
mapFilters := TMapFilters.Create(lfMap);
mapFilters.Init;
end;
end;
function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
......@@ -150,77 +229,198 @@ begin
end;
[async] procedure TFViewMap.LoadPointsAsync;
[async] procedure TFViewMap.LoadPointsAsync(showBusy: Boolean);
var
resp: TXDataClientResponse;
root, item, uo: TJSObject;
data, units: TJSArray;
units: TJSArray;
i, ui: Integer;
m: TTMSFNCMapsMarker;
lat, lng: Double;
uname, dist: string;
complaintId, codeDesc, dispatchDist, priority: string;
uName, dist: string;
unitId, callType, priorityText, statusText: string;
complaintId, codeDesc, dispatchDist, priority, business: string;
pngName, iconUrl, rowsHtml: string;
officer1Lname, officer1Fname, officer1Empnum: string;
officer2Lname, officer2Fname, officer2Empnum: string;
officer1Display, officer2Display: string;
updateTimeText: string;
canShowDetails: Boolean;
canShowDetailsText: string;
detailsBtnHtml: string;
unitsData: TJSArray;
complaintsData: TJSArray;
begin
ShowSpinner('spinner');
FUnitsLoaded := False;
FComplaintsLoaded := False;
if FLoadingPoints then
Exit;
FLoadingPoints := True;
// --- Units ---------------------------------------------------------------
if showBusy then
ShowSpinner('spinner');
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
data := TJSArray(root['data']);
FUnitsLoaded := False;
FComplaintsLoaded := False;
unitsData := nil;
complaintsData := nil;
// --- Fetch Units ---------------------------------------------------------
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
unitsData := TJSArray(root['data']);
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
if data <> nil then
begin
lfMap.BeginUpdate;
try
for i := 0 to data.Length - 1 do
// --- Fetch Complaints ----------------------------------------------------
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result);
complaintsData := TJSArray(root['data']);
FComplaintsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
// --- Swap Markers (no blank map while loading) ---------------------------
lfMap.BeginUpdate;
try
// Delete old unit/complaint markers right before adding new ones
for i := lfMap.Markers.Count - 1 downto 0 do
begin
m := lfMap.Markers[i];
if StartsText('unit|', m.DataString) or StartsText('complaint|', m.DataString) then
lfMap.Markers.Delete(i);
end;
// Add unit markers
if unitsData <> nil then
begin
for i := 0 to unitsData.Length - 1 do
begin
item := TJSObject(data[i]);
item := TJSObject(unitsData[i]);
lat := Double(item['Lat']);
lng := Double(item['Lng']);
uname := string(item['UnitName']);
dist := string(item['District']);
lat := Double(item['Lat']);
lng := Double(item['Lng']);
uName := string(item['UnitName']);
dist := string(item['District']);
unitId := string(item['UnitId']);
callType := string(item['CallType']);
priorityText := string(item['Priority']);
statusText := string(item['Status']);
updateTimeText := string(item['UpdateTime']);
officer1Lname := string(item['Officer1Lname']);
officer1Fname := string(item['Officer1Fname']);
officer1Empnum := string(item['Officer1Empnum']);
officer2Lname := string(item['Officer2Lname']);
officer2Fname := string(item['Officer2Fname']);
officer2Empnum := string(item['Officer2Empnum']);
canShowDetailsText := '';
if item['CanShowDetails'] <> nil then
canShowDetailsText := string(item['CanShowDetails']);
canShowDetails := SameText(Trim(canShowDetailsText), 'true');
if canShowDetails then
begin
detailsBtnHtml :=
'<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
'onclick="window.showUnitDetails(''' + unitId + ''')">' +
'Details' +
'</button>';
end
else
detailsBtnHtml := '';
officer1Display := '';
if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then
begin
officer1Display := Trim(officer1Lname);
if Trim(officer1Fname) <> '' then
officer1Display := officer1Display + ', ' + Trim(officer1Fname);
if Trim(officer1Empnum) <> '' then
officer1Display := officer1Display + ' (' + Trim(officer1Empnum) + ')';
end;
officer2Display := '';
if Trim(officer2Lname + officer2Fname + officer2Empnum) <> '' then
begin
officer2Display := Trim(officer2Lname);
if Trim(officer2Fname) <> '' then
officer2Display := officer2Display + ', ' + Trim(officer2Fname);
if Trim(officer2Empnum) <> '' then
officer2Display := officer2Display + ' (' + Trim(officer2Empnum) + ')';
end;
m := lfMap.Markers.Add;
m.Latitude := lat;
m.Latitude := lat;
m.Longitude := lng;
m.Title := uname + IfThen(dist <> '', ' / ' + dist, '');
m.DataString := 'unit';
m.IconURL := CarIconForDistrict(dist);
end;
finally
lfMap.EndUpdate;
end;
end;
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
m.Title :=
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
'<div class="fw-semibold small">' +
'<span class="fw-bold">Unit:</span> ' + uName +
'</div>' +
IfThen(dist <> '',
'<div class="small"><span class="fw-bold">District:</span> ' + dist + '</div>',
''
) +
IfThen(Trim(callType) <> '',
'<div class="small"><span class="fw-bold">Call Type:</span> ' + callType + '</div>',
''
) +
IfThen(Trim(priorityText) <> '',
'<div class="small"><span class="fw-bold">Priority:</span> ' + priorityText + '</div>',
''
) +
IfThen(Trim(statusText) <> '',
'<div class="small"><span class="fw-bold">Status:</span> ' + statusText + '</div>',
''
) +
IfThen(Trim(officer1Display) <> '',
'<div class="small"><span class="fw-bold">Officer 1:</span> ' + officer1Display + '</div>',
''
) +
IfThen(Trim(officer2Display) <> '',
'<div class="small"><span class="fw-bold">Officer 2:</span> ' + officer2Display + '</div>',
''
) +
IfThen(Trim(updateTimeText) <> '',
'<div class="small mb-1"><span class="fw-bold">Updated:</span> ' + updateTimeText + '</div>',
'<div class="small mb-1"></div>'
) +
IfThen(detailsBtnHtml <> '',
'<div class="d-flex justify-content-end mt-0">' + detailsBtnHtml + '</div>',
''
) +
'</div>';
// --- Complaints ----------------------------------------------------------
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result);
data := TJSArray(root['data']);
m.DataString := 'unit|' + unitId;
console.log('Unit marker ds=' + m.DataString);
m.IconURL := CarIconForDistrict(dist);
end;
end;
if data <> nil then
begin
lfMap.BeginUpdate;
try
for i := 0 to data.Length - 1 do
// Add complaint markers
if complaintsData <> nil then
begin
for i := 0 to complaintsData.Length - 1 do
begin
item := TJSObject(data[i]);
item := TJSObject(complaintsData[i]);
complaintId := string(item['ComplaintId']);
codeDesc := string(item['DispatchCodeDesc']);
complaintId := string(item['ComplaintId']);
codeDesc := string(item['DispatchCodeDesc']);
dispatchDist := string(item['DispatchDistrict']);
priority := string(item['Priority']);
priority := string(item['Priority']);
business := string(item['Business']);
lat := Double(item['Lat']);
lng := Double(item['Lng']);
......@@ -245,8 +445,8 @@ begin
rowsHtml := rowsHtml +
'<tr>' +
'<td>' + string(uo['Unit']) + '</td>' +
'<td>' + string(uo['Status']) + '</td>' +
'<td>' + string(uo['Unit']) + '</td>' +
'<td>' + string(uo['Status']) + '</td>' +
'<td>' + string(uo['Updated']) + '</td>' +
'</tr>';
end;
......@@ -254,11 +454,13 @@ begin
else
rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';
// Complaint Markers
m := lfMap.Markers.Add;
m.Latitude := lat;
m.Latitude := lat;
m.Longitude := lng;
m.DataString := 'complaint|' + complaintId;
m.IconURL := iconUrl;
m.Title :=
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
'<div class="fw-semibold small">' +
......@@ -273,6 +475,12 @@ begin
'<div class="small">' +
'<span class="fw-bold">Dispatch District:</span> ' + dispatchDist +
'</div>' +
IfThen(Trim(business) <> '',
'<div class="small">' +
'<span class="fw-bold">Business:</span> ' + business +
'</div>',
''
) +
'<div class="small mb-1">' +
'<span class="fw-bold">Address:</span> ' + string(item['Address']) +
'</div>' +
......@@ -298,22 +506,22 @@ begin
'</button>' +
'</div>' +
'</div>';
m.IconURL := iconUrl;
end;
finally
lfMap.EndUpdate;
end;
finally
lfMap.EndUpdate;
end;
FComplaintsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
if mapFilters <> nil then
mapFilters.Apply;
HideSpinner('spinner');
ApplyPendingUnitFocus;
ApplyPendingComplaintFocus;
finally
if showBusy then
HideSpinner('spinner');
FLoadingPoints := False;
end;
end;
......@@ -322,12 +530,17 @@ begin
ACustomizeMarker :=
'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 +
'var ds = (o && o.datastring) ? o.datastring : "";' + #13#10 +
'if (ds === "device") {' + #13#10 +
' try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
' try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
' return;' + #13#10 +
'}' + #13#10 +
'o.tooltipHtml = rawTitle;' + #13#10 +
'o.title = "";' + #13#10 +
'var t = o.tooltipHtml || "";' + #13#10 +
'var u = (o.icon && o.icon.options && o.icon.options.iconUrl) ? o.icon.options.iconUrl : null;' + #13#10 +
// clear any old tooltip/popup bindings
'try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
'try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
......@@ -382,28 +595,25 @@ end;
procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin
ACustomizeCSS :=
// popup container: rounded, shadow
// --- Popup container----------------------------------------------------------
'.emi-tip .leaflet-popup-content-wrapper{' +
'border-radius:8px;' +
'box-shadow:0 4px 14px rgba(0,0,0,.25);' +
'}' + #13#10 +
// popup content: no fixed width, max 260px on normal screens
// --- Popup content------------------------------------------------------------
'.emi-tip .leaflet-popup-content{' +
'margin:0;' +
'padding:0;' +
'width:auto;' +
'max-width:260px;' +
'}' + #13#10 +
// on very small screens, let it grow almost full width
// --- Media Query: on very small screens, lets it grow almost full width ------
'@media (max-width:480px){' +
'.emi-tip .leaflet-popup-content{' +
'max-width:calc(100vw - 32px);' +
'}' +
'}' + #13#10 +
// table: compact, wraps text
// --- Table: compact, wraps text ----------------------------------------------
'.emi-tip .emi-tip-table{display:table;border-collapse:collapse;table-layout:auto;width:auto;margin:.25rem 0;}'+#13#10+
'.emi-tip .emi-tip-table thead{display:table-header-group;}'+#13#10+
'.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+
......@@ -416,13 +626,138 @@ begin
'font-size:11px;' +
'vertical-align:middle;' +
'}' + #13#10 +
// marker badge
// --- Marker Badge ------------------------------------------------------------
'.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
'.emi-marker-img{display:block;}'+#13#10+
'.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}';
'.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}' + #13#10;
end;
procedure TFViewMap.tmrLocateTimer(Sender: TObject);
begin
tmrLocate.Enabled := False;
if not FDoFocusZoom then
Exit;
lfMap.SetCenterCoordinate(FPendingFocusCoord);
lfMap.SetZoomLevel(FPendingFocusZoom);
FDoFocusZoom := False;
end;
procedure TFViewMap.tmrRefreshTimer(Sender: TObject);
begin
if FLoadingPoints then
Exit;
LoadPointsAsync(False);
end;
procedure TFViewMap.btnFindLocationClick(Sender: TObject);
var
coord: TTMSFNCMapsCoordinateRec;
begin
if userLocationMarker = nil then
Exit;
coord := CreateCoordinate(userLocationMarker.Latitude, userLocationMarker.Longitude);
lfMap.SetCenterCoordinate(coord);
end;
procedure TFViewMap.FocusUnit(const unitId: string);
begin
FPendingUnitId := Trim(unitId);
if mapFilters <> nil then
LoadPointsAsync(True);
end;
procedure TFViewMap.FocusComplaint(const complaintId: string);
begin
FPendingComplaintId := Trim(complaintId);
if mapFilters <> nil then
LoadPointsAsync(True);
end;
procedure TFViewMap.ApplyPendingUnitFocus;
var
i: Integer;
m: TTMSFNCMapsMarker;
coord: TTMSFNCMapsCoordinateRec;
targetDs: string;
found: Boolean;
begin
if Trim(FPendingUnitId) = '' then
Exit;
targetDs := 'unit|' + FPendingUnitId;
found := False;
for i := 0 to lfMap.Markers.Count - 1 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, targetDs) then
begin
coord := CreateCoordinate(m.Latitude, m.Longitude);
lfmap.SetCenterCoordinate(coord);
FPendingFocusCoord := coord;
FPendingFocusZoom := 17;
FDoFocusZoom := True;
tmrLocate.Interval := 250;
tmrLocate.Enabled := True;
found := True;
Break;
end;
end;
if found then
FPendingUnitId := '';
end;
procedure TFViewMap.ApplyPendingComplaintFocus;
var
i: Integer;
m: TTMSFNCMapsMarker;
coord: TTMSFNCMapsCoordinateRec;
targetDs: string;
found: Boolean;
begin
if Trim(FPendingComplaintId) = '' then
Exit;
targetDs := 'complaint|' + FPendingComplaintId;
found := False;
for i := 0 to lfMap.Markers.Count - 1 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, targetDs) then
begin
coord := CreateCoordinate(m.Latitude, m.Longitude);
lfmap.SetCenterCoordinate(coord);
FPendingFocusCoord := coord;
FPendingFocusZoom := 17;
FDoFocusZoom := True;
tmrLocate.Interval := 250;
tmrLocate.Enabled := True;
found := True;
Break;
end;
end;
if found then
FPendingComplaintId := '';
end;
end.
object FViewUnitDetails: TFViewUnitDetails
Width = 640
Height = 480
CSSLibrary = cssBootstrap
ElementFont = efCSS
object tblLogs: TWebDBTableControl
Left = 166
Top = 116
Width = 300
Height = 200
ElementId = 'tbl_logs'
BorderColor = clSilver
ColHeader = False
ElementFont = efCSS
ElementHeaderClassName = 'table-light'
ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control'
Footer.InputElementClassName = 'form-control'
Footer.LinkActiveElementClassName = 'link-primary'
Footer.LinkElementClassName = 'link-secondary'
Footer.ListElementClassName = 'pagination'
Footer.ListItemElementClassName = 'page-item'
Footer.ListLinkElementClassName = 'page-link'
Header.ButtonActiveElementClassName = 'btn btn-primary'
Header.ButtonElementClassName = 'btn btn-light'
Header.DropDownElementClassName = 'form-control'
Header.InputElementClassName = 'form-control'
Header.LinkActiveElementClassName = 'link-primary'
Header.LinkElementClassName = 'link-secondary'
Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link'
WordWrap = True
Columns = <
item
DataField = 'LogTime'
Title = 'Log Time'
TitleElementClassName = 'w-25'
end
item
ElementClassName = 'text-nowrap'
DataField = 'Complaint'
Title = 'Complaint'
end
item
DataField = 'Log'
Title = 'Log'
end>
DataSource = wdsUnitLogs
end
object btnUnitViewOnMap: TWebButton
Left = 370
Top = 326
Width = 96
Height = 25
Caption = 'Map'
ChildOrder = 1
ElementID = 'btn_unit_view_on_map'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnUnitViewOnMapClick
end
object xdwcUnitDetails: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 150
Top = 394
end
object xdwdsUnitLogs: TXDataWebDataSet
Left = 278
Top = 394
object xdwdsUnitLogsLogTime: TStringField
FieldName = 'LogTime'
end
object xdwdsUnitLogsComplaint: TStringField
FieldName = 'Complaint'
end
object xdwdsUnitLogsLog: TStringField
FieldName = 'Log'
end
end
object wdsUnitLogs: TWebDataSource
DataSet = xdwdsUnitLogs
Left = 208
Top = 362
end
object xdwdsUnitDetails: TXDataWebDataSet
Left = 364
Top = 368
object xdwdsUnitDetailsUnitId: TStringField
FieldName = 'UnitId'
end
object xdwdsUnitDetailsUnitName: TStringField
FieldName = 'UnitName'
end
object xdwdsUnitDetailsCarNumberDesc: TStringField
FieldName = 'CarNumberDesc'
end
object xdwdsUnitDetailsDistrict: TStringField
FieldName = 'District'
end
object xdwdsUnitDetailsLocation: TStringField
FieldName = 'Location'
end
object xdwdsUnitDetailsStatus: TStringField
FieldName = 'Status'
end
object xdwdsUnitDetailsOfficer1LName: TStringField
FieldName = 'Officer1Lname'
end
object xdwdsUnitDetailsOfficer1Fname: TStringField
FieldName = 'Officer1Fname'
end
object xdwdsUnitDetailsOfficer1Empnum: TStringField
FieldName = 'Officer1Empnum'
end
object xdwdsUnitDetailsOfficer2Lname: TStringField
FieldName = 'Officer2Lname'
end
object xdwdsUnitDetailsOfficer2Fname: TStringField
FieldName = 'Officer2Fname'
end
object xdwdsUnitDetailsOfficer2Empnum: TStringField
FieldName = 'Officer2Empnum'
end
object xdwdsUnitDetailsUpdateTime: TStringField
FieldName = 'UpdateTime'
end
end
end
<div class="d-flex flex-column h-100 w-100 overflow-hidden">
<div class="flex-grow-1 d-flex flex-column overflow-auto bg-light p-2 p-md-3" style="min-height:0;">
<!-- Sticky block: Summary header + (expanded) summary body -->
<div class="sticky-top bg-light" style="z-index:20;">
<!-- Summary header -->
<div class="card border-0 shadow-sm mb-2">
<div class="card-header bg-white py-2">
<button
class="btn btn-link text-decoration-none p-0 w-100 d-flex align-items-center justify-content-between summary-toggle"
type="button"
data-bs-toggle="collapse"
data-bs-target="#udetails_summary"
aria-expanded="true"
aria-controls="udetails_summary">
<span class="fw-semibold text-dark" id="lbl_summary_title">Summary</span>
<span class="summary-chevron" aria-hidden="true">
<svg class="summary-chevron-icon" viewBox="0 0 16 16" focusable="false">
<path fill="currentColor" d="M7.646 5.354a.5.5 0 0 1 .708 0l5 5a.5.5 0 0 1-.708.708L8 6.414 3.354 11.06a.5.5 0 1 1-.708-.708l5-5z"/>
</svg>
</span>
</button>
</div>
</div>
<!-- Summary body -->
<div id="udetails_summary" class="collapse show">
<div class="card border-0 shadow-sm mb-2">
<div class="card-body py-2">
<table class="table table-sm mb-0 w-100">
<tbody>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Car:</th>
<td class="w-100" id="lbl_car"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Unit Name:</th>
<td class="w-100" id="lbl_unit_name"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">District:</th>
<td class="w-100" id="lbl_district"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Location:</th>
<td class="w-100" id="lbl_location"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Status:</th>
<td class="w-100" id="lbl_status"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Officer 1:</th>
<td class="w-100" id="lbl_officer1"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Officer 2:</th>
<td class="w-100" id="lbl_officer2"></td>
</tr>
<tr>
<th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Updated:</th>
<td class="w-100" id="lbl_updated"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Logs header row (stays sticky) -->
<div class="pb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="fw-semibold text-muted">Logs</div>
<!-- Spinner shown while loading logs -->
<div class="spinner-border spinner-border-sm text-secondary d-none" role="status" id="spinner_logs">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<!-- Logs table (scroll with page) -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div id="tbl_logs"></div>
</div>
</div>
</div>
<button id="btn_unit_view_on_map"
type="button"
class="btn btn-primary btn-sm shadow position-fixed"
style="right: 12px; bottom: 72px; z-index: 1040;">
Map
</button>
</div>
unit View.UnitDetails;
interface
uses
System.SysUtils, System.Classes, JS, Web,
WEBLib.Forms, WEBLib.Dialogs, WEBLib.Graphics,
XData.Web.Client, XData.Web.JsonDataset, XData.Web.Dataset,
ConnectionModule,
Utils,
Data.DB, WEBLib.DB,
Vcl.Controls, WEBLib.Controls, WEBLib.Grids, WEBLib.DBCtrls, Vcl.StdCtrls,
WEBLib.StdCtrls;
type
TFViewUnitDetails = class(TWebForm)
xdwcUnitDetails: TXDataWebClient;
// Details (single-object) dataset
xdwdsUnitDetails: TXDataWebDataSet;
xdwdsUnitDetailsUnitId: TStringField;
xdwdsUnitDetailsUnitName: TStringField;
xdwdsUnitDetailsCarNumberDesc: TStringField;
xdwdsUnitDetailsDistrict: TStringField;
xdwdsUnitDetailsLocation: TStringField;
xdwdsUnitDetailsStatus: TStringField;
xdwdsUnitDetailsOfficer1Lname: TStringField;
xdwdsUnitDetailsOfficer1Fname: TStringField;
xdwdsUnitDetailsOfficer1Empnum: TStringField;
xdwdsUnitDetailsOfficer2Lname: TStringField;
xdwdsUnitDetailsOfficer2Fname: TStringField;
xdwdsUnitDetailsOfficer2Empnum: TStringField;
xdwdsUnitDetailsUpdateTime: TStringField;
// Logs (array) dataset + table
xdwdsUnitLogs: TXDataWebDataSet;
wdsUnitLogs: TWebDataSource;
tblLogs: TWebDBTableControl;
xdwdsUnitLogsComplaint: TStringField;
xdwdsUnitLogsLog: TStringField;
xdwdsUnitLogsLogTime: TStringField;
btnUnitViewOnMap: TWebButton;
procedure btnUnitViewOnMapClick(Sender: TObject);
private
FUnitId: string;
FLoading: Boolean;
procedure InitializeForm;
procedure SetTextById(const elementId, value: string);
procedure SetHiddenById(const elementId: string; hidden: Boolean);
function FormatOfficer(const lname, fname, empnum: string): string;
[async] procedure LoadUnitAsync;
[async] procedure LoadLogsAsync;
public
class function CreateForm(AElementID, UnitId: string): TWebForm;
end;
var
FViewUnitDetails: TFViewUnitDetails;
implementation
uses
View.Main,
View.Units;
{$R *.dfm}
procedure TFViewUnitDetails.btnUnitViewOnMapClick(Sender: TObject);
begin
if Assigned(FViewMain) then
FViewMain.ShowMapFocusUnit(FUnitId);
console.log('btnViewOnMapClick fired, FUnitId=' + FUnitId);
end;
class function TFViewUnitDetails.CreateForm(AElementID, UnitId: string): TWebForm;
procedure AfterCreate(AForm: TObject);
begin
with TFViewUnitDetails(AForm) do
begin
FUnitId := UnitId;
InitializeForm;
end;
end;
begin
Application.CreateForm(TFViewUnitDetails, AElementID, Result, @AfterCreate);
end;
procedure TFViewUnitDetails.InitializeForm;
begin
xdwcUnitDetails.Connection := DMConnection.ApiConnection;
xdwdsUnitDetails.Connection := DMConnection.ApiConnection;
xdwdsUnitLogs.Connection := DMConnection.ApiConnection;
FLoading := False;
LoadUnitAsync;
end;
procedure TFViewUnitDetails.SetTextById(const elementId, value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if el <> nil then
TJSHtmlElement(el).innerText := value;
end;
procedure TFViewUnitDetails.SetHiddenById(const elementId: string; hidden: Boolean);
var
el: TJSHTMLElement;
begin
el := TJSHTMLElement(Document.getElementById(elementId));
if el = nil then
Exit;
if hidden then
el.classList.add('d-none')
else
el.classList.remove('d-none');
end;
function TFViewUnitDetails.FormatOfficer(const lname, fname, empnum: string): string;
var
nameText: string;
begin
nameText := Trim(lname);
if nameText = '' then
Exit('');
if Trim(fname) <> '' then
nameText := nameText + ', ' + Trim(fname);
if Trim(empnum) <> '' then
nameText := nameText + ' (' + Trim(empnum) + ')';
Result := nameText;
end;
[async] procedure TFViewUnitDetails.LoadUnitAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataObj: TJSObject;
summaryTitle: string;
officer1Text: string;
officer2Text: string;
begin
if FLoading then
Exit;
FLoading := True;
Utils.ShowSpinner('spinner');
try
resp := await(xdwcUnitDetails.RawInvokeAsync('IApiService.GetUnitDetails', [FUnitId]));
rootObj := TJSObject(resp.Result);
dataObj := TJSObject(rootObj['data']);
xdwdsUnitDetails.Close;
xdwdsUnitDetails.SetJsonData(dataObj);
xdwdsUnitDetails.Open;
summaryTitle := xdwdsUnitDetailsUnitName.AsString;
if Trim(summaryTitle) <> '' then
SetTextById('lbl_summary_title', 'Summary for Unit ' + summaryTitle)
else
SetTextById('lbl_summary_title', 'Summary for Unit ' + xdwdsUnitDetailsUnitId.AsString);
SetTextById('lbl_car', xdwdsUnitDetailsCarNumberDesc.AsString);
SetTextById('lbl_unit_name', xdwdsUnitDetailsUnitName.AsString);
SetTextById('lbl_district', xdwdsUnitDetailsDistrict.AsString);
SetTextById('lbl_location', xdwdsUnitDetailsLocation.AsString);
SetTextById('lbl_status', xdwdsUnitDetailsStatus.AsString);
SetTextById('lbl_updated', xdwdsUnitDetailsUpdateTime.AsString);
officer1Text := FormatOfficer(
xdwdsUnitDetailsOfficer1Lname.AsString,
xdwdsUnitDetailsOfficer1Fname.AsString,
xdwdsUnitDetailsOfficer1Empnum.AsString
);
officer2Text := FormatOfficer(
xdwdsUnitDetailsOfficer2Lname.AsString,
xdwdsUnitDetailsOfficer2Fname.AsString,
xdwdsUnitDetailsOfficer2Empnum.AsString
);
SetTextById('lbl_officer1', officer1Text);
SetTextById('lbl_officer2', officer2Text);
await(LoadLogsAsync);
Utils.HideSpinner('spinner');
FLoading := False;
except
on E: EXDataClientRequestException do
begin
Utils.HideSpinner('spinner');
FLoading := False;
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
on E: Exception do
begin
Utils.HideSpinner('spinner');
FLoading := False;
Utils.ShowErrorModal(E.Message);
end;
end;
end;
[async] procedure TFViewUnitDetails.LoadLogsAsync;
var
resp: TXDataClientResponse;
rootObj: TJSObject;
dataArr: TJSArray;
begin
SetHiddenById('spinner_logs', False);
try
resp := await(xdwcUnitDetails.RawInvokeAsync('IApiService.GetUnitLogs', [FUnitId]));
rootObj := TJSObject(resp.Result);
dataArr := TJSArray(rootObj['data']);
console.log('LoadLogsAsync Units rootObj: ' + rootObj.tostring);
xdwdsUnitLogs.Close;
xdwdsUnitLogs.SetJsonData(dataArr);
xdwdsUnitLogs.Open;
SetHiddenById('spinner_logs', True);
except
on E: EXDataClientRequestException do
begin
SetHiddenById('spinner_logs', True);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end;
on E: Exception do
begin
SetHiddenById('spinner_logs', True);
Utils.ShowErrorModal(E.Message);
end;
end;
end;
end.
......@@ -40,66 +40,22 @@ object FViewUnits: TFViewUnits
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 position-relative"> <div class="' +
'card-body py-2 px-3"> <!-- Unit + Status --> <div class="f' +
'w-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%St' +
'atus%) </div> <!-- Location --> <div class="small text-' +
'body-secondary mb-1"> (%Location%) </div> <!-- Call t' +
'ype --> <div class="small">(%CallType%)</div> <!-- Divider' +
' line --> <hr class="unit-divider my-1" style="width:80px;mar' +
'gin-left:0;"> <!-- Officers --> <div class="small officer1' +
'">(%Officer1%)</div> <div class="small officer2">(%Officer2%)' +
'</div> </div> <!-- Vertically centered Details button on the r' +
'ight --> <div class="position-absolute top-50 end-0 translate-m' +
'iddle-y pe-2"> <button type="button" class="btn bt' +
'n-primary btn-sm btn-unit-details" data-unitid="(%Uni' +
'tId%)"> Details </button> </div></div>'
'ary text-dark rounded-1 px-2 mb-1">(%DistrictHeader%)</div><div ' +
'class="card border shadow-sm"> <div class="card-body py-2 px-3 ' +
'd-flex gap-2"> <div class="flex-grow-1"> <div class="fw-' +
'bold text-uppercase small">(%UnitName%)&nbsp;-&nbsp;(%Status%)</' +
'div> <div class="small text-body-secondary mb-1">(%Location' +
'%)</div> <div class="small">(%CallType%)</div> <hr cla' +
'ss="unit-divider my-1" style="width: 80px; margin-left: 0" /> ' +
' <div class="small officer1">(%Officer1%)</div> <div clas' +
's="small officer2">(%Officer2%)</div> </div> <div class="d' +
'-flex flex-column justify-content-center gap-1"> <button ty' +
'pe="button" class="btn btn-primary btn-sm btn-unit-details" data' +
'-unitid="(%UnitId%)">Details</button> <button type="button"' +
' class="btn btn-primary btn-sm btn-unit-map" data-unitid="(%Unit' +
'Id%)">View On Map</button> </div> </div></div>'
ListSource = wdsUnits
end
object btnRefresh: TWebButton
Left = 110
Top = 82
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object btnGroup: TWebButton
Left = 186
Top = 82
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 250
Top = 82
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object wdsUnits: TWebDataSource
AutoEdit = False
DataSet = xdwdsUnits
......
<div class="d-flex flex-column h-100">
<div class="sticky-top">
<!-- Local navbar (Units) -->
<nav class="navbar navbar-dark bg-primary py-2"><!-- removed sticky-top -->
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="units_title" class="navbar-brand mb-0 h5 text-white">Units</span>
</div>
<div class="col">
<button id="units_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="units_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="units_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0">
<!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"><!-- removed sticky-top -->
<div class="container-fluid">
<div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="units_search" class="form-control" placeholder="Search...">
<!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2">
<div class="container-fluid">
<div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="units_search" class="form-control" placeholder="Search...">
</div>
</div>
</div>
</div>
</div> <!-- /sticky-top wrapper -->
<!-- Units list container -->
<div class="container-fluid mt-2">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<!-- This is where the DBListControl will inject cards -->
<div id="units_dbl_unit_list" class="d-flex flex-column gap-2">
<!-- Cards will render here -->
<!-- Scrolling list area -->
<div class="flex-grow-1 overflow-auto" style="min-height:0;">
<div class="container-fluid mt-2">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<!-- This is where the DBListControl will inject cards -->
<div id="units_dbl_unit_list" class="d-flex flex-column gap-2">
<!-- Cards will render here -->
</div>
<!-- Entry Count Label -->
<label id="unitss_lblentries" class="mt-2 d-block"></label>
</div>
</div>
<!-- Entry Count Label -->
<label id="unitss_lblentries" class="mt-2 d-block"></label>
<!-- Pagination -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center" id="pagination">
<!-- Pagination items rendered in Delphi -->
</ul>
</nav>
</div>
</div>
</div>
</div>
......@@ -13,9 +13,6 @@ uses
type
TFViewUnits = class(TWebForm)
dblUnitsList: TWebDBListControl;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsUnits: TWebDataSource;
xdwdsUnits: TXDataWebDataSet;
xdwcUnits: TXDataWebClient;
......@@ -35,6 +32,7 @@ type
private
FLoading: Boolean;
[async] procedure GetUnits;
procedure HandleListClick(e: TJSMouseEvent);
public
end;
......@@ -49,36 +47,76 @@ implementation
procedure TFViewUnits.WebFormCreate(Sender: TObject);
begin
console.log('Units.WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected);
Document.addEventListener('click', @HandleListClick);
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}
asm
if (!window.showUnitDetails) {
window.showUnitDetails = function (id) {
console.log('JS bridge showUnitDetails called, id=', id);
try {
pas['View.Main'].FViewMain.ShowUnitDetails(id);
} catch (e) {
console.log('Error in TFViewMain.ShowUnitDetails', e);
}
};
}
end;
end;
procedure TFViewUnits.HandleListClick(e: TJSMouseEvent);
var
el: TJSElement;
btn: TJSElement;
unitId: string;
begin
btn := nil;
el := TJSElement(e.target);
asm
btn = (el && el.closest) ? el.closest('.btn-unit-details') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
unitId := string(TJSHtmlElement(btn).getAttribute('data-unitid'));
e.preventDefault;
e.stopPropagation;
asm
if (window.showUnitDetails) window.showUnitDetails(unitId);
end;
end
else
begin
btn := nil;
asm
btn = (el && el.closest) ? el.closest('.btn-unit-map') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin
unitId := string(TJSHtmlElement(btn).getAttribute('data-unitid'));
e.preventDefault;
e.stopPropagation;
asm
try {
pas['View.Main'].FViewMain.ShowMapFocusUnit(unitId);
} catch (e) {
console.log('ShowMapFocusUnit failed', e);
}
end;
end;
end;
end;
procedure TFViewUnits.btnRefreshClick(Sender: TObject);
begin
GetUnits;
......@@ -122,6 +160,7 @@ begin
end;
end;
finally
FLoading := False;
Utils.HideSpinner('spinner');
console.log('GetUnits complete');
end;
......
/* --- TMS WEB Core Specific Fixes --- */
/* Removes the default border from the main Form wrapper */
span.card {
border: none;
}
/* --- Login Screen Styling --- */
.login-card {
display: inline-block;
width: 300px; /* Adjust width as needed */
display: inline-block; /* Or use d-flex on the parent to center it */
width: 100%;
max-width: 350px; /* Better than fixed 300px */
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.mr-2 {
margin-right: 0.5rem;
}
.table tbody tr:hover {
background-color: #d1e7fd; /* Light blue color for hover effect */
cursor: pointer;
}
.form-input{
display: table;
}
.form-cells{
display: table-cell
}
/* Optional: Custom navbar look for the login screen */
.login-navbar {
max-width: 1200px; /* Set the max-width to match a medium screen */
max-width: 1200px;
margin: auto;
border-bottom-left-radius: 10px; /* Round the bottom left corner */
border-bottom-right-radius: 10px; /* Round the bottom right corner */
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border: 1px solid #d3d3d3;
}
.navbar-toggler {
display: none;
}
.dropdown-menu a {
display: flex; /* Use flexbox for alignment */
align-items: center; /* Vertically center the content */
width: 100%; /* Ensure they take up the full width */
padding: 0.5rem 1rem; /* Add padding to make them clickable */
color: #000; /* Adjust the text color if necessary */
text-decoration: none; /* Remove underlines */
}
.dropdown-menu a:hover {
background-color: #204d74;
color: #fff;
}
.dropdown-menu a span {
flex-grow: 1; /* Make the span take up the remaining space */
/* --- Table Customization --- */
/* Bootstrap has .table-hover, but this sets your specific blue color */
.table tbody tr:hover {
background-color: #d1e7fd;
cursor: pointer;
}
/* Style for the selected number */
.selected-number .page-link {
background-color: #204d74;
color: #fff !important;
}
/* --- Pagination Theme Overrides --- */
/* These override Bootstrap's blue to your specific darker blue (#204d74) */
/* Style for the unselected numbers and text (previous/next) */
.pagination .page-item a,
.pagination .page-item span {
color: #204d74;
.pagination .page-link {
color: #204d74; /* Text color for standard links */
}
.pagination .page-item.active .page-link,
.pagination .page-item.active .page-link:hover,
.pagination .page-item.active .page-link:focus {
/* Active State (Selected Page) */
.pagination .page-item.active .page-link {
background-color: #204d74;
border-color: #204d74;
color: #fff !important;
}
/* This is needed to get rid of the line that was appearing. */
span.card {
border: none;
}
.modal-backdrop {
z-index: 1040 !important;
/* Hover State */
.pagination .page-item:not(.active) .page-link:hover {
background-color: #e9ecef; /* Standard Bootstrap light grey hover */
color: #16344a; /* Darker text on hover */
}
.modal {
z-index: 1055 !important;
/* --- Utilities --- */
.list-section-header:empty {
display: none;
}
#map_pnlmap > span {
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
position: absolute !important;
}
.list-section-header:empty { display: none; }
html, body {
height: 100%;
overflow: hidden;
}
.tab-hidden { display: none !important; }
.summary-chevron-icon {
width: 1rem;
height: 1rem;
display: block;
transition: transform 0.15s ease-in-out;
}
/* Expanded => show DOWN (rotate base UP chevron) */
.summary-toggle[aria-expanded="true"] .summary-chevron-icon {
transform: rotate(180deg);
}
unit uMapFilters;
interface
uses
System.SysUtils, System.StrUtils,
JS, Web,
VCL.TMSFNCLeaflet, VCL.TMSFNCMaps, VCL.TMSFNCMapsCommonTypes;
type
TMapFilters = class
private
FMap: TTMSFNCLeaflet;
FShowUnits: Boolean;
FShowComplaints: Boolean;
FShowLocation: Boolean;
function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
procedure SetCheckBoxChecked(const elementId: string; checked: Boolean);
procedure SetText(const elementId: string; const value: string);
procedure ReadUi;
procedure ResetUi;
procedure UpdateSummary;
procedure ApplyToMap;
procedure HandleDocClick(e: TJSMouseEvent);
procedure HandleDocChange(e: TJSEvent);
public
constructor Create(AMap: TTMSFNCLeaflet);
procedure Init;
procedure Apply;
procedure Reset;
end;
implementation
{ TMapFilters }
constructor TMapFilters.Create(AMap: TTMSFNCLeaflet);
begin
inherited Create;
FMap := AMap;
FShowUnits := True;
FShowComplaints := True;
FShowLocation := False;
end;
procedure TMapFilters.Init;
begin
Document.addEventListener('click', @HandleDocClick);
Document.addEventListener('change', @HandleDocChange);
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Apply;
begin
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Reset;
begin
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.HandleDocClick(e: TJSMouseEvent);
var
el: TJSElement;
id: string;
begin
el := TJSElement(e.target);
if el = nil then
Exit;
// Note: This supports clicking inner icons/spans by walking up to a parent with an id
asm
while (el && !el.id) { el = el.parentElement; }
end;
if (el <> nil) and (el is TJSHtmlElement) then
begin
id := string(TJSHtmlElement(el).id);
if id = 'map_filters_apply' then
begin
e.preventDefault;
e.stopPropagation;
Apply;
Exit;
end;
if id = 'map_filters_reset' then
begin
e.preventDefault;
e.stopPropagation;
Reset;
Exit;
end;
end;
end;
procedure TMapFilters.HandleDocChange(e: TJSEvent);
begin
//Note: Summary text changes when toggles change
ReadUi;
UpdateSummary;
end;
procedure TMapFilters.ReadUi;
begin
FShowUnits := GetCheckBoxChecked('map_filter_units', True);
FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True);
FShowLocation := GetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.ResetUi;
begin
SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True);
SetCheckBoxChecked('map_filter_location', False);
end;
procedure TMapFilters.UpdateSummary;
var
summaryText: string;
begin
summaryText := '';
if FShowUnits then
summaryText := 'Units';
if FShowComplaints then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Complaints';
end;
if FShowLocation then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Location';
end;
if summaryText = '' then
summaryText := 'None';
SetText('map_filters_summary', summaryText);
end;
procedure TMapFilters.ApplyToMap;
var
i: Integer;
m: TTMSFNCMapsMarker;
ds: string;
showMarker: Boolean;
begin
if FMap = nil then
Exit;
FMap.BeginUpdate;
try
for i := 0 to FMap.Markers.Count - 1 do
begin
m := FMap.Markers[i];
ds := Trim(string(m.DataString));
// Note: Map form sets:
// - units: m.DataString := 'unit'
// - complaints: m.DataString := 'complaint'
showMarker := True;
if SameText(ds, 'unit') then
showMarker := FShowUnits
else if StartsText('complaint', LowerCase(ds)) then
showMarker := FShowComplaints
else if SameText(ds, 'device') then
showMarker := FShowLocation;
// Note: TTMSFNCMapsMarker supports visibility toggling
m.Visible := showMarker;
end;
finally
FMap.EndUpdate;
end;
end;
function TMapFilters.GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
var
el: TJSElement;
begin
Result := defaultValue;
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
Result := TJSHtmlInputElement(el).checked;
end;
procedure TMapFilters.SetCheckBoxChecked(const elementId: string; checked: Boolean);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
TJSHtmlInputElement(el).checked := checked;
end;
procedure TMapFilters.SetText(const elementId: string; const value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlElement) then
TJSHtmlElement(el).innerText := value;
end;
end.
......@@ -21,7 +21,9 @@ uses
View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html},
Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html};
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas';
{$R *.res}
......
......@@ -182,11 +182,18 @@
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.UnitDetails.pas">
<Form>FViewUnitDetails</Form>
<FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="uMapFilters.pas"/>
<None Include="index.html"/>
<None Include="css\app.css"/>
<None Include="css\spinner.css"/>
<None Include="assets\bpddistricts-updated.geojson"/>
<None Include="assets\bpddistricts.geojson"/>
<None Include="config\config.json"/>
<RcItem Include="assets\markers\alarm_1.png">
<ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_1</ResourceId>
......@@ -479,7 +486,6 @@
<ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_73</ResourceId>
</RcItem>
<None Include="config\config.json"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
......
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