Commit 376416dd by emsys

merge conflicts

parents 8bb49d94 d1a82903
...@@ -21,3 +21,5 @@ emiMobileServer/logs/* ...@@ -21,3 +21,5 @@ emiMobileServer/logs/*
*.tvsconfig *.tvsconfig
*.dxsettings *.dxsettings
*.zip
...@@ -2,12 +2,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -2,12 +2,8 @@ object ApiDatabaseModule: TApiDatabaseModule
Height = 491 Height = 491
Width = 640 Width = 640
object OracleUniProvider1: TOracleUniProvider object OracleUniProvider1: TOracleUniProvider
Left = 182 Left = 164
Top = 98 Top = 38
end
object uqBooking: TUniQuery
Left = 350
Top = 98
end end
object uqMapUnits: TUniQuery object uqMapUnits: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
...@@ -18,11 +14,38 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -18,11 +14,38 @@ object ApiDatabaseModule: TApiDatabaseModule
' COALESCE(uc.UNITNAME, uc.CAR_NUMBER) AS UNITNAME,' ' COALESCE(uc.UNITNAME, uc.CAR_NUMBER) AS UNITNAME,'
' uc.UNIT_DISTRICT,' ' uc.UNIT_DISTRICT,'
' uc.GPS_LATITUDE,' ' uc.GPS_LATITUDE,'
' uc.GPS_LONGITUDE' ' uc.GPS_LONGITUDE,'
'FROM UNITS_CURRENT@AVL_LINK uc') ' 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 ReadOnly = True
Left = 470 Left = 464
Top = 414 Top = 390
object uqMapUnitsENTRYID: TFloatField object uqMapUnitsENTRYID: TFloatField
FieldName = 'ENTRYID' FieldName = 'ENTRYID'
end end
...@@ -43,6 +66,57 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -43,6 +66,57 @@ object ApiDatabaseModule: TApiDatabaseModule
object uqMapUnitsGPS_LONGITUDE: TFloatField object uqMapUnitsGPS_LONGITUDE: TFloatField
FieldName = 'GPS_LONGITUDE' FieldName = 'GPS_LONGITUDE'
end 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 end
object uqUnitList: TUniQuery object uqUnitList: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
...@@ -105,8 +179,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -105,8 +179,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'' ''
'') '')
ReadOnly = True ReadOnly = True
Left = 278 Left = 76
Top = 322 Top = 188
object uqUnitListUNITID: TFloatField object uqUnitListUNITID: TFloatField
FieldName = 'UNITID' FieldName = 'UNITID'
end end
...@@ -221,8 +295,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -221,8 +295,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'WHERE ca.COMPLAINTID = :COMPLAINTID' 'WHERE ca.COMPLAINTID = :COMPLAINTID'
'ORDER BY ca.DATEDISPATCHED') 'ORDER BY ca.DATEDISPATCHED')
ReadOnly = True ReadOnly = True
Left = 470 Left = 466
Top = 356 Top = 326
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
...@@ -268,15 +342,15 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -268,15 +342,15 @@ object ApiDatabaseModule: TApiDatabaseModule
' cm.REMARKS' ' cm.REMARKS'
'FROM CFS_MEMOS cm' 'FROM CFS_MEMOS cm'
'WHERE cm.CFSID = :CFSID' 'WHERE cm.CFSID = :CFSID'
'ORDER BY cm.TIMESTAMP ASC') 'ORDER BY cm.TIMESTAMP DESC')
ReadOnly = True ReadOnly = True
Left = 278 Left = 196
Top = 382 Top = 248
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
Name = 'CFSID' Name = 'CFSID'
Value = Null Value = nil
end> end>
object uqCFSMemosMEMO_ID: TFloatField object uqCFSMemosMEMO_ID: TFloatField
FieldName = 'MEMO_ID' FieldName = 'MEMO_ID'
...@@ -317,7 +391,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -317,7 +391,7 @@ object ApiDatabaseModule: TApiDatabaseModule
' ca.APARTMENT,' ' ca.APARTMENT,'
' ca.CITY,' ' ca.CITY,'
' ca.BUSINESS,' ' ca.BUSINESS,'
' ca.DISPATCHDISTRICT,' ' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.DISPATCHSECTOR,' ' ca.DISPATCHSECTOR,'
' ca.ADDRESSDISTRICT,' ' ca.ADDRESSDISTRICT,'
' ca.ADDRESSSECTOR,' ' ca.ADDRESSSECTOR,'
...@@ -343,20 +417,17 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -343,20 +417,17 @@ object ApiDatabaseModule: TApiDatabaseModule
'LEFT JOIN CD_CALLPRIORITIES cp ON cp.CODE = ca.PRIORITY AND cp.A' + 'LEFT JOIN CD_CALLPRIORITIES cp ON cp.CODE = ca.PRIORITY AND cp.A' +
'GENCY = ca.AGENCY' 'GENCY = ca.AGENCY'
'LEFT JOIN CD_DISTRICT cd ON cd.AGENCYCODE = ca.DISPATCHDI' + 'JOIN CD_DISTRICT cd ON cd.AGENCYCODE = ca.DISPATCHDI' +
'STRICT' 'STRICT'
'LEFT JOIN CD_SECTOR cs ON cs.AGENCYCODE = ca.DISPATCHSE' + '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' 'WHERE ca.COMPLAINT IS NOT NULL'
'ORDER BY cd.CODE_DESC, ct.DATEREPORTED DESC, ca.PRIORITY DESC;')
'ORDER BY ca.DISPATCHDISTRICT, ct.DATEREPORTED DESC, ca.PRIORITY ' +
'DESC'
'')
ReadOnly = True ReadOnly = True
OnCalcFields = uqComplaintListCalcFields OnCalcFields = uqComplaintListCalcFields
Left = 94 Left = 78
Top = 320 Top = 244
object uqComplaintListCOMPLAINTID: TFloatField object uqComplaintListCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID' FieldName = 'COMPLAINTID'
Required = True Required = True
...@@ -494,22 +565,45 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -494,22 +565,45 @@ object ApiDatabaseModule: TApiDatabaseModule
' ca.PRIORITY,' ' ca.PRIORITY,'
' ca.DISPATCHCODE,' ' ca.DISPATCHCODE,'
' cdc.CODE_DESC AS DISPATCH_CODE_DESC,' ' cdc.CODE_DESC AS DISPATCH_CODE_DESC,'
' ca.DISPATCHDISTRICT,' ' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.ADDRESS,' ' ca.ADDRESS,'
' ca.BUSINESS,'
' ct.DATEREPORTED,' ' ct.DATEREPORTED,'
' ct.DATERECEIVED,' ' ct.DATERECEIVED,'
' ct.DATEDISPATCHED,' ' ct.DATEDISPATCHED,'
' ct.DATERESPONDED,' ' ct.DATERESPONDED,'
' ct.DATEARRIVED,' ' 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' 'FROM COMPLAINT_ACTIVE ca'
'JOIN COMPLAINT_TIMES ct ON ca.COMPLAINTID = ct.COMPLAINTID' 'JOIN COMPLAINT_TIMES ct'
'LEFT JOIN CD_DISPATCHCODES cdc ON ca.DISPATCHCODE = cdc.CODE' ' ON ca.COMPLAINTID = ct.COMPLAINTID'
'WHERE ca.COMPLAINTID = :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 ReadOnly = True
Left = 92 Left = 80
Top = 376 Top = 302
ParamData = < ParamData = <
item item
DataType = ftUnknown DataType = ftUnknown
...@@ -542,7 +636,7 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -542,7 +636,7 @@ object ApiDatabaseModule: TApiDatabaseModule
end end
object uqComplaintDetailsDISPATCHDISTRICT: TStringField object uqComplaintDetailsDISPATCHDISTRICT: TStringField
FieldName = 'DISPATCHDISTRICT' FieldName = 'DISPATCHDISTRICT'
Size = 6 Size = 120
end end
object uqComplaintDetailsADDRESS: TStringField object uqComplaintDetailsADDRESS: TStringField
FieldName = 'ADDRESS' FieldName = 'ADDRESS'
...@@ -572,6 +666,53 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -572,6 +666,53 @@ object ApiDatabaseModule: TApiDatabaseModule
FieldName = 'DATECLEARED' FieldName = 'DATECLEARED'
ReadOnly = True ReadOnly = True
end 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 end
object ucENTCAD: TUniConnection object ucENTCAD: TUniConnection
ProviderName = 'Oracle' ProviderName = 'Oracle'
...@@ -580,8 +721,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -580,8 +721,8 @@ object ApiDatabaseModule: TApiDatabaseModule
Server = 'BUFENTCAD' Server = 'BUFENTCAD'
Connected = True Connected = True
LoginPrompt = False LoginPrompt = False
Left = 38 Left = 50
Top = 98 Top = 36
EncryptedPassword = 'BAFFB1FFABFFBCFFBEFFBBFF' EncryptedPassword = 'BAFFB1FFABFFBCFFBEFFBBFF'
end end
object uqMapComplaints: TUniQuery object uqMapComplaints: TUniQuery
...@@ -589,29 +730,24 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -589,29 +730,24 @@ object ApiDatabaseModule: TApiDatabaseModule
SQL.Strings = ( SQL.Strings = (
'SELECT' 'SELECT'
' ca.COMPLAINTID,' ' ca.COMPLAINTID,'
' ca.DISPATCHDISTRICT,' ' cd.CODE_DESC AS DISPATCHDISTRICT,'
' ca.PRIORITY AS PRIORITY,' ' ca.PRIORITY AS PRIORITY,'
' ca.BUSINESS,'
' cdc.MOBILE_MAP_CATEGORY AS DISPATCHCODECATEGORY,' ' cdc.MOBILE_MAP_CATEGORY AS DISPATCHCODECATEGORY,'
'' ''
' CASE ' ' SDO_CS.TRANSFORM('
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YC' + ' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YCOORD' +
'OORD, NULL), NULL, NULL),' ', NULL), NULL, NULL),'
' 4326' ' 4326'
' ).sdo_point.x' ' ).sdo_point.x AS LNG,'
' END AS LNG,'
'' ''
' CASE ' ' SDO_CS.TRANSFORM('
' WHEN ca.XCOORD IS NOT NULL AND ca.YCOORD IS NOT NULL THEN'
' SDO_CS.TRANSFORM('
' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YC' + ' SDO_GEOMETRY(2001, 2262, SDO_POINT_TYPE(ca.XCOORD, ca.YCOORD' +
'OORD, NULL), NULL, NULL),' ', NULL), NULL, NULL),'
' 4326' ' 4326'
' ).sdo_point.y' ' ).sdo_point.y AS LAT,'
' END AS LAT,'
'' ''
' cdc.CODE_DESC AS DISPATCH_CODE_DESC,' ' cdc.CODE_DESC AS DISPATCH_CODE_DESC,'
' ca.ADDRESS AS ADDRESS' ' ca.ADDRESS AS ADDRESS'
...@@ -620,22 +756,26 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -620,22 +756,26 @@ object ApiDatabaseModule: TApiDatabaseModule
' ON ct.COMPLAINTID = ca.COMPLAINTID' ' ON ct.COMPLAINTID = ca.COMPLAINTID'
'LEFT JOIN CD_DISPATCHCODES cdc' 'LEFT JOIN CD_DISPATCHCODES cdc'
' ON cdc.CODE = ca.DISPATCHCODE' ' ON cdc.CODE = ca.DISPATCHCODE'
'WHERE ca.COMPLAINT IS NOT NULL ' 'JOIN CD_DISTRICT cd'
' AND ca.XCOORD IS NOT NULL ' ' ON cd.AGENCYCODE = ca.DISPATCHDISTRICT'
'WHERE ca.COMPLAINT IS NOT NULL'
' AND ca.XCOORD IS NOT NULL'
' AND ca.YCOORD IS NOT NULL;' ' AND ca.YCOORD IS NOT NULL;'
'' ''
''
''
'') '')
ReadOnly = True ReadOnly = True
OnCalcFields = uqMapComplaintsCalcFields OnCalcFields = uqMapComplaintsCalcFields
Left = 470 Left = 468
Top = 296 Top = 264
object uqMapComplaintsCOMPLAINTID: TFloatField object uqMapComplaintsCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID' FieldName = 'COMPLAINTID'
Required = True Required = True
end end
object uqMapComplaintsDISPATCHDISTRICT: TStringField object uqMapComplaintsDISPATCHDISTRICT: TStringField
FieldName = 'DISPATCHDISTRICT' FieldName = 'DISPATCHDISTRICT'
Size = 6 Size = 120
end end
object uqMapComplaintsLNG: TFloatField object uqMapComplaintsLNG: TFloatField
FieldName = 'LNG' FieldName = 'LNG'
...@@ -671,6 +811,10 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -671,6 +811,10 @@ object ApiDatabaseModule: TApiDatabaseModule
FieldName = 'pngName' FieldName = 'pngName'
Calculated = True Calculated = True
end end
object uqMapComplaintsBUSINESS: TStringField
FieldName = 'BUSINESS'
Size = 35
end
end end
object uqBadgeCounts: TUniQuery object uqBadgeCounts: TUniQuery
Connection = ucENTCAD Connection = ucENTCAD
...@@ -686,8 +830,8 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -686,8 +830,8 @@ object ApiDatabaseModule: TApiDatabaseModule
'from dual;' 'from dual;'
'') '')
ReadOnly = True ReadOnly = True
Left = 194 Left = 198
Top = 270 Top = 190
object uqBadgeCountsCOMPLAINTS: TFloatField object uqBadgeCountsCOMPLAINTS: TFloatField
FieldName = 'COMPLAINTS' FieldName = 'COMPLAINTS'
ReadOnly = True ReadOnly = True
...@@ -717,9 +861,9 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -717,9 +861,9 @@ object ApiDatabaseModule: TApiDatabaseModule
' AND c.YCOORD IS NOT NULL' ' AND c.YCOORD IS NOT NULL'
')' ')'
'ORDER BY ca.COMPLAINTID, ca.DATEDISPATCHED;') 'ORDER BY ca.COMPLAINTID, ca.DATEDISPATCHED;')
Active = True ReadOnly = True
Left = 470 Left = 464
Top = 242 Top = 202
object uqMapComplaintUnitsListCOMPLAINTID: TFloatField object uqMapComplaintUnitsListCOMPLAINTID: TFloatField
FieldName = 'COMPLAINTID' FieldName = 'COMPLAINTID'
end end
...@@ -747,4 +891,282 @@ object ApiDatabaseModule: TApiDatabaseModule ...@@ -747,4 +891,282 @@ object ApiDatabaseModule: TApiDatabaseModule
Size = 30 Size = 30
end end
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 end
...@@ -10,7 +10,6 @@ uses ...@@ -10,7 +10,6 @@ uses
type type
TApiDatabaseModule = class(TDataModule) TApiDatabaseModule = class(TDataModule)
OracleUniProvider1: TOracleUniProvider; OracleUniProvider1: TOracleUniProvider;
uqBooking: TUniQuery;
uqMapUnits: TUniQuery; uqMapUnits: TUniQuery;
uqUnitList: TUniQuery; uqUnitList: TUniQuery;
uqComplaintUnits: TUniQuery; uqComplaintUnits: TUniQuery;
...@@ -60,12 +59,6 @@ type ...@@ -60,12 +59,6 @@ type
uqCFSMemosTIMESTAMP: TDateTimeField; uqCFSMemosTIMESTAMP: TDateTimeField;
uqCFSMemosBADGE_NUMBER: TStringField; uqCFSMemosBADGE_NUMBER: TStringField;
uqCFSMemosREMARKS: TStringField; uqCFSMemosREMARKS: TStringField;
uqMapUnitsENTRYID: TFloatField;
uqMapUnitsUNITID: TFloatField;
uqMapUnitsUNITNAME: TStringField;
uqMapUnitsUNIT_DISTRICT: TStringField;
uqMapUnitsGPS_LATITUDE: TFloatField;
uqMapUnitsGPS_LONGITUDE: TFloatField;
uqComplaintListcomplaintNumber: TStringField; uqComplaintListcomplaintNumber: TStringField;
uqComplaintListPRIORITY_COLOR: TFloatField; uqComplaintListPRIORITY_COLOR: TFloatField;
uqComplaintListDISTRICT_DESC: TStringField; uqComplaintListDISTRICT_DESC: TStringField;
...@@ -128,6 +121,57 @@ type ...@@ -128,6 +121,57 @@ type
uqMapComplaintUnitsListDATECLEARED: TDateTimeField; uqMapComplaintUnitsListDATECLEARED: TDateTimeField;
uqMapComplaintUnitsListLOCATION: TStringField; uqMapComplaintUnitsListLOCATION: TStringField;
uqMapComplaintsADDRESS: 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 uqComplaintListCalcFields(DataSet: TDataSet);
procedure uqMapComplaintsCalcFields(DataSet: TDataSet); procedure uqMapComplaintsCalcFields(DataSet: TDataSet);
private private
......
...@@ -22,6 +22,14 @@ type ...@@ -22,6 +22,14 @@ type
[HttpGet] function GetComplaintMap: TJSONObject; [HttpGet] function GetComplaintMap: TJSONObject;
[HttpGet] function GetUnitMap: TJSONObject; [HttpGet] function GetUnitMap: TJSONObject;
[HttpGet] function GetComplaintDetails(const ComplaintId: string): 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; end;
implementation implementation
......
...@@ -16,6 +16,7 @@ type ...@@ -16,6 +16,7 @@ type
private private
procedure AfterConstruction; override; procedure AfterConstruction; override;
procedure BeforeDestruction; override; procedure BeforeDestruction; override;
function GetComplaintMemos(const CfsId: string): TJSONObject;
public public
function GetBadgeCounts: TJSONObject; function GetBadgeCounts: TJSONObject;
function GetComplaintList: TJSONObject; function GetComplaintList: TJSONObject;
...@@ -23,6 +24,11 @@ type ...@@ -23,6 +24,11 @@ type
function GetComplaintMap: TJSONObject; function GetComplaintMap: TJSONObject;
function GetUnitMap: TJSONObject; function GetUnitMap: TJSONObject;
function GetComplaintDetails(const ComplaintId: string): 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; end;
implementation implementation
...@@ -66,7 +72,7 @@ begin ...@@ -66,7 +72,7 @@ begin
except except
on E: Exception do on E: Exception do
begin 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'); raise EXDataHttpException.Create(500, 'Failed to load badge counts');
end; end;
end; end;
...@@ -146,13 +152,14 @@ begin ...@@ -146,13 +152,14 @@ begin
item:=TJSONObject.Create; item:=TJSONObject.Create;
item.AddPair('ComplaintId',ApiDB.uqMapComplaintsCOMPLAINTID.AsString); 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('DispatchCodeDesc',ApiDB.uqMapComplaintsDISPATCH_CODE_DESC.AsString);
item.AddPair('DispatchCodeCategory',ApiDB.uqMapComplaintsDISPATCHCODECATEGORY.AsString); item.AddPair('DispatchCodeCategory',ApiDB.uqMapComplaintsDISPATCHCODECATEGORY.AsString);
item.AddPair('Priority',ApiDB.uqMapComplaintsPRIORITY.AsString); item.AddPair('Priority',ApiDB.uqMapComplaintsPRIORITY.AsString);
item.AddPair('priorityKey',ApiDB.uqMapComplaintspriorityKey.AsString); item.AddPair('priorityKey',ApiDB.uqMapComplaintspriorityKey.AsString);
item.AddPair('pngName',ApiDB.uqMapComplaintspngName.AsString); item.AddPair('pngName',ApiDB.uqMapComplaintspngName.AsString);
item.AddPair('Address',ApiDB.uqMapComplaintsADDRESS.AsString); item.AddPair('Address',ApiDB.uqMapComplaintsADDRESS.AsString);
item.AddPair('Business',ApiDB.uqMapComplaintsBUSINESS.AsString);
complaintId:=ApiDB.uqMapComplaintsCOMPLAINTID.AsString; complaintId:=ApiDB.uqMapComplaintsCOMPLAINTID.AsString;
if UnitsByComplaintMap.TryGetValue(complaintId,unitArray) then if UnitsByComplaintMap.TryGetValue(complaintId,unitArray) then
...@@ -176,7 +183,7 @@ begin ...@@ -176,7 +183,7 @@ begin
on E: Exception do on E: Exception do
begin begin
FreeAndNil(data); 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'); raise EXDataHttpException.Create(500,'Failed to load complaint map');
end; end;
end; end;
...@@ -186,13 +193,18 @@ begin ...@@ -186,13 +193,18 @@ begin
end; end;
function TApiService.GetUnitMap: TJSONObject; function TApiService.GetUnitMap: TJSONObject;
var var
data: TJSONArray; data: TJSONArray;
item: TJSONObject;
unitStatus: string;
updateTimeText: string;
begin 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; Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
...@@ -202,21 +214,51 @@ begin ...@@ -202,21 +214,51 @@ begin
with ApiDB.uqMapUnits do with ApiDB.uqMapUnits do
begin begin
Open; Open;
First; try
while not Eof do First;
begin while not Eof do
// skip rows without coordinates
if (not FieldByName('GPS_LATITUDE').IsNull) and (not FieldByName('GPS_LONGITUDE').IsNull) then
begin begin
var item := TJSONObject.Create; if (not ApiDB.uqMapUnitsGPS_LATITUDE.IsNull) and (not ApiDB.uqMapUnitsGPS_LONGITUDE.IsNull) then
item.AddPair('UnitId', ApiDB.uqMapUnitsUNITID.AsString); begin
item.AddPair('UnitName', ApiDB.uqMapUnitsUNITNAME.AsString); item := TJSONObject.Create;
item.AddPair('District', ApiDB.uqMapUnitsUNIT_DISTRICT.AsString);
item.AddPair('Lat', TJSONNumber.Create(ApiDB.uqMapUnitsGPS_LATITUDE.AsFloat)); item.AddPair('UnitId', ApiDB.uqMapUnitsUNITID.AsString);
item.AddPair('Lng', TJSONNumber.Create(ApiDB.uqMapUnitsGPS_LONGITUDE.AsFloat)); item.AddPair('UnitName', ApiDB.uqMapUnitsUNITNAME.AsString);
data.AddElement(item); 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; end;
Next; finally
Close;
end; end;
end; end;
...@@ -225,14 +267,16 @@ begin ...@@ -225,14 +267,16 @@ begin
Result.AddPair('data', data); Result.AddPair('data', data);
except except
data.Free; data.Free;
Logger.Log(3, '---TApiService.GetUnitMap End (error)'); Logger.Log(2, '---TApiService.GetUnitMap error');
raise EXDataHttpException.Create(500, 'Failed to load unit map'); raise EXDataHttpException.Create(500, 'Failed to load unit map');
end; end;
Logger.Log(3, '---TApiService.GetUnitMap End'); Logger.Log(4, '---TApiService.GetUnitMap End');
end; end;
function TApiService.GetComplaintList: TJSONObject; function TApiService.GetComplaintList: TJSONObject;
var var
data: TJSONArray; data: TJSONArray;
...@@ -287,6 +331,7 @@ begin ...@@ -287,6 +331,7 @@ begin
item.AddPair('Priority', ApiDB.uqComplaintListPRIORITY.AsString); item.AddPair('Priority', ApiDB.uqComplaintListPRIORITY.AsString);
item.AddPair('DispatchCodeDesc', ApiDB.uqComplaintListDISPATCH_CODE_DESC.AsString); item.AddPair('DispatchCodeDesc', ApiDB.uqComplaintListDISPATCH_CODE_DESC.AsString);
item.AddPair('Address', ApiDB.uqComplaintListADDRESS.AsString); item.AddPair('Address', ApiDB.uqComplaintListADDRESS.AsString);
item.AddPair('Business', ApiDB.uqComplaintListBUSINESS.AsString);
item.AddPair('CFSId', ApiDB.uqComplaintListCFSID.AsString); item.AddPair('CFSId', ApiDB.uqComplaintListCFSID.AsString);
item.AddPair('Status', status); item.AddPair('Status', status);
item.AddPair('DispatchDistrict', curDistrict); item.AddPair('DispatchDistrict', curDistrict);
...@@ -302,7 +347,7 @@ begin ...@@ -302,7 +347,7 @@ begin
Result.AddPair('data', data); Result.AddPair('data', data);
except except
data.Free; 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'); raise EXDataHttpException.Create(500, 'Failed to load complaints list');
end; end;
...@@ -415,35 +460,51 @@ begin ...@@ -415,35 +460,51 @@ begin
if Eof then raise EXDataHttpException.Create(404,'Complaint not found'); if Eof then raise EXDataHttpException.Create(404,'Complaint not found');
obj := TJSONObject.Create; obj := TJSONObject.Create;
obj.AddPair('ComplaintId',FieldByName('COMPLAINTID').AsString); obj.AddPair('ComplaintId', ApiDB.uqComplaintDetailsCOMPLAINTID.AsString);
obj.AddPair('CFSId',FieldByName('CFSID').AsString); obj.AddPair('CFSId', ApiDB.uqComplaintDetailsCFSID.AsString);
obj.AddPair('Complaint',FieldByName('COMPLAINT').AsString); obj.AddPair('Complaint', ApiDB.uqComplaintDetailsCOMPLAINT.AsString);
obj.AddPair('Priority',FieldByName('PRIORITY').AsString); obj.AddPair('Priority', ApiDB.uqComplaintDetailsPRIORITY.AsString);
obj.AddPair('DispatchCode',FieldByName('DISPATCHCODE').AsString); obj.AddPair('DispatchCode', ApiDB.uqComplaintDetailsDISPATCHCODE.AsString);
obj.AddPair('DispatchCodeDesc',FieldByName('DISPATCH_CODE_DESC').AsString); obj.AddPair('DispatchCodeDesc', ApiDB.uqComplaintDetailsDISPATCH_CODE_DESC.AsString);
obj.AddPair('DispatchDistrict',FieldByName('DISPATCHDISTRICT').AsString); obj.AddPair('DispatchDistrict', ApiDB.uqComplaintDetailsDISPATCHDISTRICT.AsString);
obj.AddPair('Address',FieldByName('ADDRESS').AsString); obj.AddPair('Address', ApiDB.uqComplaintDetailsADDRESS.AsString);
obj.AddPair('Business',ApiDB.uqComplaintDetailsBUSINESS.AsString);
if FieldByName('DATEREPORTED').IsNull obj.AddPair('History', ApiDB.uqComplaintDetailsHISTORY.AsString);
then obj.AddPair('DateReported','') obj.AddPair('Contacts', ApiDB.uqComplaintDetailsCONTACTS.AsString);
else obj.AddPair('DateReported',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEREPORTED') as TDateTimeField).AsDateTime)); obj.AddPair('Warnings', ApiDB.uqComplaintDetailsWARNINGS.AsString);
if FieldByName('DATERECEIVED').IsNull
then obj.AddPair('DateReceived','') if ApiDB.uqComplaintDetailsDATEREPORTED.IsNull then
else obj.AddPair('DateReceived',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATERECEIVED') as TDateTimeField).AsDateTime)); obj.AddPair('DateReported', '')
if FieldByName('DATEDISPATCHED').IsNull else
then obj.AddPair('DateDispatched','') obj.AddPair('DateReported', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATEREPORTED.AsDateTime));
else obj.AddPair('DateDispatched',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEDISPATCHED') as TDateTimeField).AsDateTime));
if FieldByName('DATERESPONDED').IsNull if ApiDB.uqComplaintDetailsDATERECEIVED.IsNull then
then obj.AddPair('DateResponded','') obj.AddPair('DateReceived', '')
else obj.AddPair('DateResponded',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATERESPONDED') as TDateTimeField).AsDateTime)); else
if FieldByName('DATEARRIVED').IsNull obj.AddPair('DateReceived', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATERECEIVED.AsDateTime));
then obj.AddPair('DateArrived','')
else obj.AddPair('DateArrived',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATEARRIVED') as TDateTimeField).AsDateTime)); if ApiDB.uqComplaintDetailsDATEDISPATCHED.IsNull then
if FieldByName('DATECLEARED').IsNull obj.AddPair('DateDispatched', '')
then obj.AddPair('DateCleared','') else
else obj.AddPair('DateCleared',FormatDateTime('yyyy-mm-dd hh:nn:ss',(FieldByName('DATECLEARED') as TDateTimeField).AsDateTime)); obj.AddPair('DateDispatched', FormatDateTime('yyyy-mm-dd hh:nn:ss', ApiDB.uqComplaintDetailsDATEDISPATCHED.AsDateTime));
Result.AddPair('data',obj); 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 finally
Close; Close;
end; end;
...@@ -464,6 +525,377 @@ begin ...@@ -464,6 +525,377 @@ begin
end; 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 initialization
......
[Settings] [Settings]
LogFileNum=531 LogFileNum=610
webClientVersion=0.1.0 webClientVersion=0.1.0
<div class="row"> <div class="container-fluid p-3">
<div class="col-lg-12">
<h1 class="page-header" id="view.userprofile.title">Admin User Profile</h1> <h1 id="view.userprofile.title" class="h2 border-bottom pb-2 mb-4 text-primary">
Admin User Profile
<div role="form"> </h1>
<div class="form-group">
<label id="view.userprofile.form.lblUserName">User Name:</label> <div class="card shadow-sm mb-4">
<input id="view.userprofile.form.edtUserName" class="form-control"> <div class="card-body">
</div> <h5 class="card-title text-secondary mb-3">User Details</h5>
<div class="form-group">
<label id="view.userprofile.form.lblFullName">User Fullname:</label> <div class="row g-3">
<input id="view.userprofile.form.edtFullName" class="form-control">
</div> <div class="col-12">
<div class="form-group"> <label id="view.userprofile.form.lblFullName" for="view.userprofile.form.edtFullName" class="form-label fw-bold">User Fullname:</label>
<label id="view.userprofile.form.lblAgency">User Agency:</label> <input id="view.userprofile.form.edtFullName" type="text" class="form-control">
<input id="view.userprofile.form.edtAgency" class="form-control"> </div>
</div>
<div class="form-group"> <div class="col-md-6">
<label id="view.userprofile.form.lblBadgeNum">User Bage #:</label> <label id="view.userprofile.form.lblUserName" for="view.userprofile.form.edtUserName" class="form-label fw-bold">User Name:</label>
<input id="view.userprofile.form.edtBadgeNum" class="form-control"> <input id="view.userprofile.form.edtUserName" type="text" class="form-control">
</div> </div>
<div class="form-group"> <div class="col-md-6">
<label id="view.userprofile.form.lblUserId">User Id:</label> <label id="view.userprofile.form.lblAgency" for="view.userprofile.form.edtAgency" class="form-label fw-bold">User Agency:</label>
<input id="view.userprofile.form.edtUserId" class="form-control"> <input id="view.userprofile.form.edtAgency" type="text" class="form-control">
</div> </div>
<div class="form-group">
<label id="view.userprofile.form.lblPersonnelId">Personnel Id:</label> <div class="col-md-4">
<input id="view.userprofile.form.edtPersonnelId" class="form-control"> <label id="view.userprofile.form.lblBadgeNum" for="view.userprofile.form.edtBadgeNum" class="form-label small text-muted">User Badge #:</label>
</div> <input id="view.userprofile.form.edtBadgeNum" type="text" class="form-control">
<div class="custom-control custom-checkbox"> </div>
<input type="checkbox" class="custom-control-input" id="view.userprofile.form.chkAdminUser"> <div class="col-md-4">
<label class="custom-control-label" for="view.userprofile.form.chkAdminUser">Admin User</label> <label id="view.userprofile.form.lblUserId" for="view.userprofile.form.edtUserId" class="form-label small text-muted">User Id:</label>
</div> <input id="view.userprofile.form.edtUserId" type="text" class="form-control">
<div class="form-input"> </div>
<div><label id="lblinfo" class="py-2" style="font-size: 1.00rem;"></label></div> <div class="col-md-4">
<div><input class="form-control input-sm" id="edtusername" width='50%'/></div> <label id="view.userprofile.form.lblPersonnelId" for="view.userprofile.form.edtPersonnelId" class="form-label small text-muted">Personnel Id:</label>
<div class="py-2"><input class="form-control input-sm" id="edtpassword" width='50%'/></div> <input id="view.userprofile.form.edtPersonnelId" type="text" class="form-control">
<button id="btnadduser"></button> </div>
<div><label id="lblresult" class="py-2" style="font-size: 1.00rem;"></label></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>
<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>
</div>
</div> </div>
object FViewComplaintDetails: TFViewComplaintDetails object FViewComplaintDetails: TFViewComplaintDetails
Width = 640 Width = 800
Height = 480 Height = 672
Caption = 'tbl_logs'
CSSLibrary = cssBootstrap CSSLibrary = cssBootstrap
ElementFont = efCSS ElementFont = efCSS
object WebDBTableControl1: TWebDBTableControl object btnHistory: TWebButton
Left = 164 Left = 506
Top = 198 Top = 134
Width = 300 Width = 96
Height = 200 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 BorderColor = clSilver
ChildOrder = 8
ElementFont = efCSS ElementFont = efCSS
ElementHeaderClassName = 'thead-light' ElementHeaderClassName = 'table-light'
ElementTableClassName = 'table table-striped table-bordered table-hover' ElementTableClassName =
'table table-sm table-striped table-hover table-bordered mb-0 ali' +
'gn-middle'
Footer.ButtonActiveElementClassName = 'btn btn-primary' Footer.ButtonActiveElementClassName = 'btn btn-primary'
Footer.ButtonElementClassName = 'btn btn-light' Footer.ButtonElementClassName = 'btn btn-light'
Footer.DropDownElementClassName = 'form-control' Footer.DropDownElementClassName = 'form-control'
...@@ -30,6 +302,171 @@ object FViewComplaintDetails: TFViewComplaintDetails ...@@ -30,6 +302,171 @@ object FViewComplaintDetails: TFViewComplaintDetails
Header.ListElementClassName = 'pagination' Header.ListElementClassName = 'pagination'
Header.ListItemElementClassName = 'page-item' Header.ListItemElementClassName = 'page-item'
Header.ListLinkElementClassName = 'page-link' 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
end end
<!-- Sticky local navbar (Complaint Details) --> <div class="d-flex flex-column h-100 w-100 overflow-hidden">
<div class="sticky-top"> <div class="flex-grow-1 d-flex flex-column overflow-auto bg-light p-2 p-md-3" style="min-height:0;">
<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="col-5 col-sm-4 fw-semibold">Status</div> <!-- Sticky block: Summary header + (expanded) summary body + buttons -->
<div class="col-7 col-sm-8" id="lbl_status"></div> <div class="sticky-top bg-light" style="z-index:20;">
<div class="col-5 col-sm-4 fw-semibold">Dispatch Code</div> <!-- Summary header -->
<div class="col-7 col-sm-8" id="lbl_dispatch_code"></div> <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> <span class="summary-chevron" aria-hidden="true">
<div class="col-7 col-sm-8" id="lbl_dispatch_district"></div> <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> <!-- Summary body (stays sticky because it's inside the sticky block) -->
<div class="col-7 col-sm-8" id="lbl_address"></div> <div id="cdetails_summary" class="collapse show">
</div> <div class="card border-0 shadow-sm mb-2">
</div> <div class="card-body py-2">
</div>
<!-- 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> <table class="table table-sm mb-0 w-100">
<button type="button" class="btn btn-success btn-sm px-3" id="btn_e911">E-911</button> <tbody>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_rem">REM</button> <tr>
<button type="button" class="btn btn-success btn-sm px-3" id="btn_unt">UNT</button> <th scope="row" class="fw-semibold text-nowrap pe-2 w-auto">Priority:</th>
</div> <td class="w-100" id="lbl_priority"></td>
</div> </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) --> <!-- Buttons -->
<div class="card"> <div class="pb-2">
<div class="card-body p-0"> <div class="d-flex flex-wrap gap-2 justify-content-center">
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;"> <button type="button" class="btn btn-success btn-sm px-3 active" id="btn_remarks" aria-pressed="true">Remarks</button>
<table class="table table-sm table-striped mb-0"> <button type="button" class="btn btn-success btn-sm px-3" id="btn_history">History</button>
<thead class="table-light sticky-top"> <button type="button" class="btn btn-success btn-sm px-3" id="btn_warnings">Warnings</button>
<tr> <button type="button" class="btn btn-success btn-sm px-3" id="btn_contacts">Contacts</button>
<th style="width: 70px;">Type</th> </div>
<th style="width: 150px;">Timestamp</th>
<th>Remarks</th>
</tr>
</thead>
<tbody id="tbl_complaint_details"></tbody>
</table>
</div>
</div>
</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> </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> </div>
<button id="btn_complaint_view_on_map"
type="button"
class="btn btn-primary btn-sm shadow position-fixed"
style="right: 12px; bottom: 72px; z-index: 1040;">
View On Map
</button>
</div> </div>
...@@ -4,14 +4,111 @@ interface ...@@ -4,14 +4,111 @@ interface
uses uses
System.SysUtils, System.Classes, JS, Web, System.SysUtils, System.Classes, JS, Web,
WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, WEBLib.Forms, WEBLib.Dialogs, WEBLib.Graphics,
WEBLib.Grids, WEBLib.DBCtrls; 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 type
TFViewComplaintDetails = class(TWebForm) 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 private
FComplaintId: string; 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 public
class function CreateForm(AElementID, ComplaintId: string): TWebForm; class function CreateForm(AElementID, ComplaintId: string): TWebForm;
procedure InitializeForm; procedure InitializeForm;
...@@ -22,6 +119,10 @@ var ...@@ -22,6 +119,10 @@ var
implementation implementation
uses
View.Main,
View.Complaints;
{$R *.dfm} {$R *.dfm}
class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string): TWebForm; class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string): TWebForm;
...@@ -31,7 +132,7 @@ class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string ...@@ -31,7 +132,7 @@ class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string
with TFViewComplaintDetails(AForm) do with TFViewComplaintDetails(AForm) do
begin begin
FComplaintId := ComplaintId; FComplaintId := ComplaintId;
InitializeForm; // kick off loading / UI binding here InitializeForm;
end; end;
end; end;
...@@ -41,153 +142,578 @@ end; ...@@ -41,153 +142,578 @@ end;
procedure TFViewComplaintDetails.InitializeForm; procedure TFViewComplaintDetails.InitializeForm;
begin begin
// TODO: xdwcComplaintDetails.Connection := DMConnection.ApiConnection;
// - call your XData endpoint with FComplaintId
// - bind fields / populate controls xdwdsRemarks.Connection := DMConnection.ApiConnection;
// - handle spinner / errors as you do elsewhere 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;
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; procedure TFViewComplaintDetails.btnCmpClick(Sender: TObject);
// begin
//interface FShowCmp := not FShowCmp;
// ApplyRemarksFilters;
//uses end;
// System.SysUtils, System.Classes, JS, Web,
// WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs, procedure TFViewComplaintDetails.btnComplaintViewOnMapClick(Sender: TObject);
// WEBLib.StdCtrls, WEBLib.WebCtrls, WEBLib.ExtCtrls, begin
// WEBLib.DB, Data.DB, if Assigned(FViewMain) then
// XData.Web.Client, XData.Web.JsonDataset, XData.Web.Dataset, FViewMain.ShowMapFocusComplaint(FComplaintId);
// View.Main, View.Complaints, Vcl.Controls, WEBLib.Grids, WEBLib.DBCtrls; end;
//
//type procedure TFViewComplaintDetails.btnE911Click(Sender: TObject);
// TFViewComplaintDetails = class(TWebForm) begin
// // Header controls (ElementID -> HTML ids in snake_case) FShowE911 := not FShowE911;
// btnCloseComplaintDetails: TWebButton; // ElementID = 'btn_close_complaint_details' ApplyRemarksFilters;
// lblComplaintNumber: TWebLabel; // 'lbl_complaint_number' end;
// lblPriority: TWebLabel; // 'lbl_priority'
// lblStatus: TWebLabel; // 'lbl_status' procedure TFViewComplaintDetails.btnREMClick(Sender: TObject);
// lblDispatchCode: TWebLabel; // 'lbl_dispatch_code' begin
// lblDispatchDistrict: TWebLabel; // 'lbl_dispatch_district' FShowRem := not FShowRem;
// lblAddress: TWebLabel; // 'lbl_address' ApplyRemarksFilters;
// end;
// // Action buttons (optional events later)
// btnHistory: TWebButton; // 'btn_history' procedure TFViewComplaintDetails.btnUntClick(Sender: TObject);
// btnWarnings: TWebButton; // 'btn_warnings' begin
// btnContacts: TWebButton; // 'btn_contacts' FShowUnt := not FShowUnt;
// btnCMP: TWebButton; // 'btn_cmp' ApplyRemarksFilters;
// btnE911: TWebButton; // 'btn_e911' end;
// btnREM: TWebButton; // 'btn_rem'
// btnUNT: TWebButton; // 'btn_unt' [async] procedure TFViewComplaintDetails.LoadComplaintAsync;
// var
// // Data-aware table resp: TXDataClientResponse;
// tblComplaintDetails: TWebDBListControl; // ElementID = 'tbl_complaint_details' rootObj: TJSObject;
// xdwcComplaintDetails: TXDataWebClient; dataObj: TJSObject;
// xdwdsxComplaintDetails: TXDataWebDataSet; // (your requested name) businessRowEl: TJSHTMLElement;
// wdsComplaintDetails: TWebDataSource;
// complaintText: string;
// private priorityText: string;
// FComplaintId: string; statusText: string;
// dispatchDescText: string;
// [async] procedure LoadHeaderAsync; dispatchDistrictText: string;
// [async] procedure LoadRowsAsync; addressText: string;
// businessText: string;
// public
// class function CreateForm(AElementID, ComplaintId: string): TWebForm; dateDispatchedText: string;
// procedure InitializeForm; dateArrivedText: string;
// end; dateClearedText: string;
//
//implementation historyFlag: string;
// contactsFlag: string;
//{$R *.dfm} warningsFlag: string;
// begin
//class function TFViewComplaintDetails.CreateForm(AElementID, ComplaintId: string): TWebForm; if FLoading then
// Exit;
// procedure AfterCreate(AForm: TObject);
// begin FLoading := True;
// with TFViewComplaintDetails(AForm) do ShowSpinner('spinner');
// begin try
// FComplaintId := ComplaintId; try
// InitializeForm; resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetails', [FComplaintId]));
// end; rootObj := TJSObject(resp.Result);
// end; dataObj := TJSObject(rootObj['data']);
//
//begin complaintText := string(dataObj['Complaint']);
// Application.CreateForm(TFViewComplaintDetails, AElementID, Result, @AfterCreate); priorityText := string(dataObj['Priority']);
//end; dispatchDescText := string(dataObj['DispatchCodeDesc']);
// dispatchDistrictText := string(dataObj['DispatchDistrict']);
//procedure TFViewComplaintDetails.InitializeForm; addressText := string(dataObj['Address']);
//begin businessText := '';
// // Close button navigates back via Main (keeps host container ownership clean) if dataObj['Business'] <> nil then
// btnCloseComplaintDetails.OnClick := businessText := string(dataObj['Business']);
// procedure(Sender: TObject) FCfsId := string(dataObj['CFSId']);
// begin
// if Assigned(FViewMain) then statusText := '';
// begin if dataObj['Status'] <> nil then
//// FViewMain.SetActiveNavButton('view.main.btncomplaints'); statusText := string(dataObj['Status']);
// FViewMain.ShowForm(TFViewComplaints);
// end; dateDispatchedText := '';
// end; dateArrivedText := '';
// dateClearedText := '';
// // Data-aware wiring (design-time also OK; keeping here for clarity)
// wdsComplaintDetails.DataSet := xdwdsxComplaintDetails; if dataObj['DateDispatched'] <> nil then
// tblComplaintDetails.DataSource := wdsComplaintDetails; dateDispatchedText := string(dataObj['DateDispatched']);
// if dataObj['DateArrived'] <> nil then
// // Expecting ItemTemplate at design-time: dateArrivedText := string(dataObj['DateArrived']);
// // <tr><td>(%MemoType%)</td><td>(%Timestamp%)</td><td>(%Remarks%)</td></tr> if dataObj['DateCleared'] <> nil then
// dateClearedText := string(dataObj['DateCleared']);
// // Kick off loads
// LoadHeaderAsync; if Trim(statusText) = '' then
// LoadRowsAsync; statusText := DeriveStatusFromDates(dateDispatchedText, dateArrivedText, dateClearedText);
//end; // Note: reenable if using the complaint in list html that is currently commented out
// // SetTextById('lbl_complaint_number', complaintText);
//[async] procedure TFViewComplaintDetails.LoadHeaderAsync; SetTextById('lbl_summary_title', 'Summary for Complaint ' + complaintText);
//var SetTextById('lbl_priority', priorityText);
// resp: TXDataClientResponse; SetTextById('lbl_status', statusText);
// obj: TJSObject; SetTextById('lbl_dispatch_code', dispatchDescText);
//begin SetTextById('lbl_dispatch_district', dispatchDistrictText);
// try SetTextById('lbl_address', addressText);
// // TODO:Adjust endpoint and args to match your server (array param is common in XData) SetTextById('lbl_business', businessText);
// resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetailsHeader', [FComplaintId])); businessRowEl := TJSHTMLElement(document.getElementById('row_business'));
// obj := TJSObject(resp.Result); if businessRowEl <> nil then
// begin
// // If the server returns { data: {...} }, unwrap: if Trim(businessText) = '' then
// if obj['data'] <> nil then businessRowEl.classList.add('d-none')
// obj := TJSObject(obj['data']); else
// businessRowEl.classList.remove('d-none');
// lblComplaintNumber.Caption := string(obj['ComplaintNumber'] ?? FComplaintId); end;
// lblPriority.Caption := string(obj['Priority'] ?? '');
// lblStatus.Caption := string(obj['Status'] ?? '');
// lblDispatchCode.Caption := string(obj['DispatchCodeDesc'] ?? ''); historyFlag := '';
// lblDispatchDistrict.Caption := string(obj['DispatchDistrict'] ?? ''); contactsFlag := '';
// lblAddress.Caption := string(obj['Address'] ?? ''); warningsFlag := '';
// except
// on E: Exception do if dataObj['History'] <> nil then
// Console.Log('Header load error: ' + E.Message); historyFlag := string(dataObj['History']);
// end; if dataObj['Contacts'] <> nil then
//end; contactsFlag := string(dataObj['Contacts']);
// if dataObj['Warnings'] <> nil then
//[async] procedure TFViewComplaintDetails.LoadRowsAsync; warningsFlag := string(dataObj['Warnings']);
//var
// resp: TXDataClientResponse; FHasHistory := historyFlag <> '-1';
// root: TJSObject; FHasContacts := contactsFlag <> '-1';
//begin FHasWarnings := warningsFlag <> '-1';
// try
// // Adjust endpoint name/params to your API SetButtonEnabledById('btn_history', FHasHistory);
// resp := await(xdwcComplaintDetails.RawInvokeAsync('IApiService.GetComplaintDetailsRows', [FComplaintId])); SetButtonEnabledById('btn_contacts', FHasContacts);
// root := TJSObject(resp.Result); SetButtonEnabledById('btn_warnings', FHasWarnings);
//
// // Expecting { data: [ { MemoType, Timestamp, Remarks }, ... ] } await(LoadMemosAsync);
// xdwdsxComplaintDetails.Close;
// xdwdsxComplaintDetails.SetJsonData(root['data']); SetActiveTab('remarks');
// xdwdsxComplaintDetails.Open; ApplyRemarksFilters;
// except except
// on E: Exception do on E: EXDataClientRequestException do
// Console.Log('Rows load error: ' + E.Message); Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
// end; on E: Exception do
//end; Utils.ShowErrorModal(E.Message);
// end;
//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 ...@@ -24,49 +24,6 @@ object FViewComplaints: TFViewComplaints
Visible = False Visible = False
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
end end
object btnGroup: TWebButton
Left = 180
Top = 110
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 242
Top = 110
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnRefresh: TWebButton
Left = 114
Top = 110
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'complaints_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object dblComplaintsList: TWebDBListControl object dblComplaintsList: TWebDBListControl
Left = 36 Left = 36
Top = 148 Top = 148
...@@ -86,18 +43,21 @@ object FViewComplaints: TFViewComplaints ...@@ -86,18 +43,21 @@ object FViewComplaints: TFViewComplaints
DataSource = wdsComplaints DataSource = wdsComplaints
ItemTemplate = ItemTemplate =
'<div class="list-section-header small fw-semibold bg-secondary t' + '<div class="list-section-header small fw-semibold bg-secondary t' +
'ext-white rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><div cl' + 'ext-white rounded-1 px-2 mb-1">(%DistrictHeader%)</div><div clas' +
'ass="card border shadow-sm" style="--bs-card-bg:(%PriorityCo' + 's="card border shadow-sm" style="--bs-card-bg:(%PriorityColor%);' +
'lor%);--bs-card-color:(%PriorityTextColor%);"> <div class="card' + '--bs-card-color:(%PriorityTextColor%);"><div class="card-body py' +
'-body py-2 px-3"> <div class="fw-bold text-uppercase small"> ' + '-2 px-3 d-flex gap-2"><div class="flex-grow-1"><div class="fw-bo' +
' (%Priority%): (%DispatchCodeDesc%) </div> <div class=' + 'ld text-uppercase small">(%Priority%): (%DispatchCodeDesc%)</div' +
'"small">(%Address%)</div> <div class="small text-opacity-75 d' + '><div class="small">(%Address%)</div><div class="small d-none co' +
'-flex align-items-center"> <span> (%Complaint%): (%S' + 'mplaint-business" data-business="(%Business%)">(%Business%)</div' +
'tatus%)&nbsp;&nbsp;(%DistrictSector%) </span> <button ' + '><div class="small text-opacity-75">(%Complaint%): (%Status%)&nb' +
'type="button" class="btn btn-primary btn-sm ms-auto' + 'sp;&nbsp;(%DistrictSector%)</div><div class="small text-opacity-' +
' complaint-details-btn" data-id="(%ComplaintId%)"> ' + '75">(%DateReported%)</div></div><div class="d-flex flex-column j' +
' Details </button> </div> <div class="small tex' + 'ustify-content-center gap-1"><button type="button" class="btn bt' +
't-opacity-75">(%DateReported%)</div> </div></div>' '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 ListSource = wdsComplaints
end end
object xdwcComplaints: TXDataWebClient object xdwcComplaints: TXDataWebClient
...@@ -127,6 +87,9 @@ object FViewComplaints: TFViewComplaints ...@@ -127,6 +87,9 @@ object FViewComplaints: TFViewComplaints
object xdwdsComplaintsAddress: TStringField object xdwdsComplaintsAddress: TStringField
FieldName = 'Address' FieldName = 'Address'
end end
object xdwdsComplaintsBusiness: TStringField
FieldName = 'Business'
end
object xdwdsComplaintsStatus: TStringField object xdwdsComplaintsStatus: TStringField
FieldName = 'Status' FieldName = 'Status'
end end
......
<div class="d-flex flex-column h-100">
<div class="sticky-top"> <!-- Header / controls (non-scrolling) -->
<!-- Local navbar (Complaints) --> <div class="flex-shrink-0">
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="complaints_title" class="navbar-brand mb-0 h5 text-white">Complaints</span>
</div>
<div class="col">
<button id="complaints_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="complaints_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="complaints_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"> <div class="bg-light border-bottom py-2">
<div class="container-fluid"> <div class="container-fluid">
<div class="input-group"> <div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span> <span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="complaints_search" class="form-control" placeholder="Search..."> <input id="complaints_search" class="form-control" placeholder="Search...">
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <!-- Scrolling list area -->
<div class="flex-grow-1 overflow-auto" style="min-height:0;">
<!-- Complaints list container --> <div class="container-fluid mt-2">
<div class="container-fluid mt-2"> <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-8">
<div class="col-12 col-md-10 col-lg-8"> <!-- This is where the DBListControl will inject cards -->
<!-- This is where the DBListControl will inject cards --> <div id="complaints_dbl_complaint_list" class="d-flex flex-column gap-2">
<div id="complaints_dbl_complaint_list" class="d-flex flex-column gap-2"> <!-- Cards will render here -->
<!-- Cards will render here --> </div>
<label id="complaints_lblentries" class="mt-2 d-block"></label>
</div>
</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>
</div>
</div>
...@@ -24,9 +24,6 @@ type ...@@ -24,9 +24,6 @@ type
xdwdsComplaintsDispatchDistrict: TStringField; xdwdsComplaintsDispatchDistrict: TStringField;
xdwdsComplaintsDateReported: TStringField; xdwdsComplaintsDateReported: TStringField;
lblEntries: TWebLabel; lblEntries: TWebLabel;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsComplaints: TWebDataSource; wdsComplaints: TWebDataSource;
xdwdsComplaintsComplaintId: TStringField; xdwdsComplaintsComplaintId: TStringField;
xdwdsComplaintsDistrictHeader: TStringField; xdwdsComplaintsDistrictHeader: TStringField;
...@@ -35,6 +32,7 @@ type ...@@ -35,6 +32,7 @@ type
xdwdsComplaintsPriorityTextColor: TStringField; xdwdsComplaintsPriorityTextColor: TStringField;
xdwdsComplaintsDistrictSector: TStringField; xdwdsComplaintsDistrictSector: TStringField;
tmrRefresh: TWebTimer; tmrRefresh: TWebTimer;
xdwdsComplaintsBusiness: TStringField;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject); procedure btnRefreshClick(Sender: TObject);
procedure tmrRefreshTimer(Sender: TObject); procedure tmrRefreshTimer(Sender: TObject);
...@@ -44,6 +42,7 @@ type ...@@ -44,6 +42,7 @@ type
FLoading: Boolean; FLoading: Boolean;
[async] procedure GetComplaints; [async] procedure GetComplaints;
procedure HandleListClick(e: TJSMouseEvent); procedure HandleListClick(e: TJSMouseEvent);
procedure ShowHideBusinessRows;
public public
property OnShowDetails: TSelectProc read FSelectProc write FSelectProc; property OnShowDetails: TSelectProc read FSelectProc write FSelectProc;
end; end;
...@@ -62,94 +61,135 @@ begin ...@@ -62,94 +61,135 @@ begin
tmrRefresh.Enabled := False; tmrRefresh.Enabled := False;
GetComplaints; GetComplaints;
tmrRefresh.Enabled := True; 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; end;
procedure TFViewComplaints.HandleListClick(e: TJSMouseEvent); procedure TFViewComplaints.HandleListClick(e: TJSMouseEvent);
var el: TJSElement; id: string; var
el: TJSElement;
btn: TJSElement;
complaintId: string;
begin begin
btn := nil;
el := TJSElement(e.target); el := TJSElement(e.target);
if (el is TJSHtmlElement) and TJSHtmlElement(el).classList.contains('complaint-details-btn') then
asm
btn = (el && el.closest) ? el.closest('.complaint-details-btn') : null;
end;
if (btn <> nil) and (btn is TJSHtmlElement) then
begin begin
id := string(TJSHtmlElement(el).dataset['id']); // comes from (%ComplaintId%) complaintId := string(TJSHtmlElement(btn).getAttribute('data-id'));
e.preventDefault; e.preventDefault;
e.stopPropagation; e.stopPropagation;
if Assigned(FSelectProc) then asm
FSelectProc(id); 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;
end; end;
procedure TFViewComplaints.WebFormDestroy(Sender: TObject); procedure TFViewComplaints.WebFormDestroy(Sender: TObject);
begin begin
Document.removeEventListener('click', @HandleListClick); Document.removeEventListener('click', @HandleListClick);
end; end;
//HTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property //Note: HTML for individual complaint cards can be found in the twebdblistcontrol HTMLString property
procedure TFViewComplaints.btnRefreshClick(Sender: TObject); procedure TFViewComplaints.btnRefreshClick(Sender: TObject);
begin begin
GetComplaints; GetComplaints;
end; end;
procedure TFViewComplaints.GetComplaints; [async] procedure TFViewComplaints.GetComplaints;
var var
xdcResponse: TXDataClientResponse; xdcResponse: TXDataClientResponse;
respObj: TJSObject; respObj: TJSObject;
complaintsCount: Integer; complaintsCount: Integer;
begin begin
if FLoading then Exit; if FLoading then
FLoading := True; Exit;
console.log('GetComplaints: Invoking API...');
FLoading := True;
ShowSpinner('spinner');
try try
try try
xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', [])); xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', []));
console.log('RawInvoke returned:', xdcResponse.Result);
respObj := TJSObject(xdcResponse.Result); respObj := TJSObject(xdcResponse.Result);
xdwdsComplaints.Close; xdwdsComplaints.Close;
console.log('Dataset closed');
xdwdsComplaints.SetJsonData(respObj['data']); xdwdsComplaints.SetJsonData(respObj['data']);
console.log('JsonData set on dataset:', respObj['data']);
xdwdsComplaints.Open; xdwdsComplaints.Open;
console.log('PriorityColor field name = ' +
xdwdsComplaintsPriorityColor.FieldName + ShowHideBusinessRows;
' sample value = ' +
xdwdsComplaintsPriorityColor.AsString);
if xdwdsComplaints.RecordCount > 0 then
begin
console.log('First record - Complaint:' + xdwdsComplaints.FieldByName('Complaint').AsString);
end;
complaintsCount := Integer(respObj['count']); complaintsCount := Integer(respObj['count']);
lblEntries.Caption := Format('%d active complaints', [complaintsCount]); lblEntries.Caption := Format('%d active complaints', [complaintsCount]);
console.log('Label updated:' + lblEntries.Caption);
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
begin
console.log('XData exception:' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage); Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end; on E: Exception do
Utils.ShowErrorModal(E.Message);
end; end;
finally finally
HideSpinner('spinner');
console.log('GetComplaints complete'); FLoading := False;
end; end;
HideSpinner('spinner');
end; end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject); procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin begin
GetComplaints; GetComplaints;
console.log('tmrRefreshTimer fired');
end; end;
end. end.
......
...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain ...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain
OnClick = lblUsersClick OnClick = lblUsersClick
Caption = 'Users' Caption = 'Users'
end end
object lblMainTitle: TWebLabel
Left = 131
Top = 31
Width = 3
Height = 15
ElementID = 'lbl_main_title'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel object WebPanel1: TWebPanel
Left = 136 Left = 136
Top = 110 Top = 110
......
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column vh-100">
<!-- Top Nav --> <!-- Top Nav -->
<nav class="navbar navbar-light bg-primary border-light text-light fixed-top"> <nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
<div class="container-fluid"> <div class="container-fluid">
<!-- Left: Font button -->
<button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm">
font
</button>
<!-- Center: App title --> <!-- App title + current view title -->
<a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a> <div class="d-flex align-items-center ms-3">
<a id="view.main.apptitle" class="navbar-brand fw-bold text-light mb-0 me-1" href="index.html">emiMobile</a>
<span class="navbar-brand text-light mb-0 mx-0">-</span>
<span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span>
</div>
<!-- Right: Connection label --> <!-- Right: Connection label -->
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto">Connected</span> <span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span>
</div> </div>
</nav> </nav>
<!-- Main content: fills space between navbars --> <!-- Main content: fills space between navbars -->
<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 --> <!-- TWebPanel content gets injected here -->
</main> </main>
<!-- Bottom Nav --> <!-- 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="container-fluid">
<div class="d-flex justify-content-center gap-3 w-100"> <div class="d-flex justify-content-center gap-3 w-100">
<button id="view.main.btnmap" type="button" class="btn btn-primary"> <button id="view.main.btnmap" type="button" class="btn btn-primary">
...@@ -46,10 +46,10 @@ ...@@ -46,10 +46,10 @@
<!-- Spinner --> <!-- Spinner -->
<div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none"> <div id="spinner" class="position-absolute top-50 start-50 translate-middle d-none">
<div class="lds-roller"> <div class="lds-roller">
<div></div><div></div><div></div><div></div> <div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div> <div></div><div></div><div></div><div></div>
</div> </div>
</div> </div>
<!-- Error modal --> <!-- Error modal -->
......
...@@ -24,6 +24,7 @@ type ...@@ -24,6 +24,7 @@ type
btnComplaints: TWebButton; btnComplaints: TWebButton;
btnUnits: TWebButton; btnUnits: TWebButton;
tmrBadgeCounts: TWebTimer; tmrBadgeCounts: TWebTimer;
lblMainTitle: TWebLabel;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject); procedure mnuLogoutClick(Sender: TObject);
procedure wllblUserProfileClick(Sender: TObject); procedure wllblUserProfileClick(Sender: TObject);
...@@ -44,8 +45,9 @@ type ...@@ -44,8 +45,9 @@ type
procedure ShowCrudForm( AFormClass: TWebFormClass ); procedure ShowCrudForm( AFormClass: TWebFormClass );
//procedure EditUser( AParam, BParam, CParam, DParam, EParam: string); //procedure EditUser( AParam, BParam, CParam, DParam, EParam: string);
function GetUserInfo: string; function GetUserInfo: string;
procedure SetActiveNavButton(const BtnId: string);
[async] procedure RefreshBadgesAsync; [async] procedure RefreshBadgesAsync;
procedure ShowUnitDetails(UnitId: string);
procedure SetHeaderTitle(const title: string);
public public
{ Public declarations } { Public declarations }
class procedure Display(LogoutProc: TLogoutProc); class procedure Display(LogoutProc: TLogoutProc);
...@@ -53,6 +55,9 @@ type ...@@ -53,6 +55,9 @@ type
procedure EditUser( Mode, FullName, Username, Phone, Email: string; admin, active: boolean); procedure EditUser( Mode, FullName, Username, Phone, Email: string; admin, active: boolean);
procedure ShowUserForm(Info: string); procedure ShowUserForm(Info: string);
procedure ShowComplaintDetails(ComplaintId: string); procedure ShowComplaintDetails(ComplaintId: string);
procedure SetActiveNavButton(const BtnId: string);
procedure ShowMapFocusUnit(const unitId: string);
procedure ShowMapFocusComplaint(const complaintId: string);
end; end;
var var
...@@ -71,6 +76,7 @@ uses ...@@ -71,6 +76,7 @@ uses
View.Admin, View.Admin,
View.Users, View.Users,
View.EditUser, View.EditUser,
View.UnitDetails,
Utils; Utils;
{$R *.dfm} {$R *.dfm}
...@@ -87,9 +93,20 @@ begin ...@@ -87,9 +93,20 @@ begin
lblUsers.Visible := false; lblUsers.Visible := false;
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
ShowForm(TFViewMap); ShowForm(TFViewMap);
SetHeaderTitle('Map');
RefreshBadgesAsync; RefreshBadgesAsync;
end; end;
procedure TFViewMain.SetHeaderTitle(const title: string);
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); procedure TFViewMain.lblUsersClick(Sender: TObject);
begin begin
...@@ -136,6 +153,7 @@ end; ...@@ -136,6 +153,7 @@ end;
procedure TFViewMain.btnComplaintsClick(Sender: TObject); procedure TFViewMain.btnComplaintsClick(Sender: TObject);
begin begin
SetHeaderTitle('Complaints');
SetActiveNavButton('view.main.btncomplaints'); SetActiveNavButton('view.main.btncomplaints');
ShowForm(TFViewComplaints); ShowForm(TFViewComplaints);
...@@ -149,12 +167,14 @@ end; ...@@ -149,12 +167,14 @@ end;
procedure TFViewMain.btnMapClick(Sender: TObject); procedure TFViewMain.btnMapClick(Sender: TObject);
begin begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap'); SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap); ShowForm(TFViewMap);
end; end;
procedure TFViewMain.btnUnitsClick(Sender: TObject); procedure TFViewMain.btnUnitsClick(Sender: TObject);
begin begin
SetHeaderTitle('Units');
SetActiveNavButton('view.main.btnunits'); SetActiveNavButton('view.main.btnunits');
ShowForm(TFViewUnits); ShowForm(TFViewUnits);
end; end;
...@@ -197,11 +217,19 @@ end; ...@@ -197,11 +217,19 @@ end;
procedure TFViewMain.ShowComplaintDetails(ComplaintId: string); procedure TFViewMain.ShowComplaintDetails(ComplaintId: string);
begin begin
SetHeaderTitle('Complaint Details');
if Assigned(FChildForm) then if Assigned(FChildForm) then
FChildForm.Free; FChildForm.Free;
FChildForm := TFViewComplaintDetails.CreateForm(WebPanel1.ElementID, ComplaintId); FChildForm := TFViewComplaintDetails.CreateForm(WebPanel1.ElementID, ComplaintId);
end; 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); procedure TFViewMain.SetActiveNavButton(const btnId: string);
...@@ -264,4 +292,41 @@ begin ...@@ -264,4 +292,41 @@ begin
end; end;
procedure TFViewMain.ShowMapFocusUnit(const unitId: string);
var
pendingUnitId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingUnitId := unitId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusUnit(pendingUnitId);
end, 50);
end;
procedure TFViewMain.ShowMapFocusComplaint(const complaintId: string);
var
pendingComplaintId: string;
begin
SetHeaderTitle('Map');
SetActiveNavButton('view.main.btnmap');
ShowForm(TFViewMap);
pendingComplaintId := complaintId;
window.setTimeout(
procedure
begin
if (FChildForm <> nil) and (FChildForm is TFViewMap) then
TFViewMap(FChildForm).FocusComplaint(pendingComplaintId);
end, 50);
end;
end. end.
...@@ -2,81 +2,15 @@ object FViewMap: TFViewMap ...@@ -2,81 +2,15 @@ object FViewMap: TFViewMap
Width = 475 Width = 475
Height = 802 Height = 802
ElementFont = efCSS ElementFont = efCSS
object btnMenu: TWebButton
Left = 62
Top = 66
Width = 41
Height = 25
Caption = 'Menu'
ChildOrder = 1
ElementID = 'map.btnmenu'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnAlerts: TWebButton
Left = 148
Top = 66
Width = 35
Height = 25
Caption = 'Alerts'
ChildOrder = 1
ElementID = 'map.btnalerts'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnGroups: TWebButton
Left = 194
Top = 66
Width = 41
Height = 25
Caption = 'Groups'
ChildOrder = 1
ElementID = 'map.btngroups'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnLocate: TWebButton
Left = 246
Top = 66
Width = 39
Height = 25
Caption = 'Locate'
ChildOrder = 1
ElementID = 'map.btnlocate'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilters: TWebButton
Left = 297
Top = 66
Width = 35
Height = 25
Caption = 'Filters'
ChildOrder = 1
ElementID = 'map.btnfilters'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnDisplay: TWebButton
Left = 351
Top = 66
Width = 40
Height = 25
Caption = 'Display'
ChildOrder = 1
ElementID = 'map.btndisplay'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object pnlMap: TWebPanel object pnlMap: TWebPanel
Left = 62 Left = 62
Top = 120 Top = 120
Width = 335 Width = 335
Height = 555 Height = 555
ElementID = 'map_pnlmap' ElementID = 'map_pnlmap'
Caption = 'pnlMap'
ChildOrder = 7 ChildOrder = 7
TabOrder = 6 ElementPosition = epIgnore
TabOrder = 0
object lfMap: TTMSFNCLeaflet object lfMap: TTMSFNCLeaflet
Left = 0 Left = 0
Top = 0 Top = 0
...@@ -114,6 +48,18 @@ object FViewMap: TFViewMap ...@@ -114,6 +48,18 @@ object FViewMap: TFViewMap
HeadLinks = <> HeadLinks = <>
end end
end end
object btnFindLocation: TWebButton
Left = 90
Top = 74
Width = 61
Height = 25
Caption = 'Location'
ChildOrder = 1
ElementID = 'btn_find_location'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnFindLocationClick
end
object httpReqGeoJson: TWebHttpRequest object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText ResponseType = rtText
URL = 'assets/bpddistricts-updated.geojson' URL = 'assets/bpddistricts-updated.geojson'
...@@ -128,7 +74,15 @@ object FViewMap: TFViewMap ...@@ -128,7 +74,15 @@ object FViewMap: TFViewMap
end end
object tmrRefresh: TWebTimer object tmrRefresh: TWebTimer
Interval = 30000 Interval = 30000
OnTimer = tmrRefreshTimer
Left = 358 Left = 358
Top = 696 Top = 696
end end
object tmrLocate: TWebTimer
Enabled = False
Interval = 100
OnTimer = tmrLocateTimer
Left = 174
Top = 74
end
end end
<!-- Root wrapper inside main.webpanel --> <div id="map.root" class="d-flex flex-column h-100 w-100">
<div id="map.root" class="d-flex flex-column" style="height:100%;">
<!-- Local navbar --> <!-- New: offcavnvas is a bootstrap class that adds an easy slide in modal -->
<nav class="navbar navbar-dark bg-primary py-2"> <div class="offcanvas offcanvas-end"
<div class="container-fluid"> tabindex="-1"
<div class="row w-100 g-2 align-items-stretch"> id="map_filters_offcanvas"
<div class="col"> aria-labelledby="map_filters_offcanvas_label"
<button id="map.btnmenu" type="button" class="btn btn-primary w-100 h-100"> style="--bs-offcanvas-width: 280px;">
<i class="fa fa-bars me-1"></i><span class="d-none d-sm-inline">Menu</span> <div class="offcanvas-header">
</button> <h5 class="offcanvas-title" id="map_filters_offcanvas_label">Map Filters</h5>
</div> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
<div class="col"> </div>
<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> <div class="offcanvas-body">
</button> <div class="mb-3">
</div> <div class="form-check">
<div class="col"> <input class="form-check-input" type="checkbox" id="map_filter_units" checked>
<button id="map.btngroups" type="button" class="btn btn-primary w-100 h-100"> <label class="form-check-label" for="map_filter_units">Show Units</label>
<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>
</div> </div>
<div class="col"> <div class="form-check">
<button id="map.btnfilters" type="button" class="btn btn-primary w-100 h-100"> <input class="form-check-input" type="checkbox" id="map_filter_complaints" checked>
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span> <label class="form-check-label" for="map_filter_complaints">Show Complaints</label>
</button>
</div> </div>
<div class="col"> <div class="form-check">
<button id="map.btndisplay" type="button" class="btn btn-primary w-100 h-100"> <input class="form-check-input" type="checkbox" id="map_filter_location">
<i class="fa fa-sun me-1"></i><span class="d-none d-sm-inline">Display</span> <label class="form-check-label" for="map_filter_location">Show Location</label>
</button>
</div> </div>
</div> </div>
</div> <div class="d-grid gap-2 mt-4">
</nav> <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="mt-3 small text-muted">
<div class="flex-grow-1" style="min-height:400px;"> <span class="fw-semibold">Active:</span>
<!-- TWebPanel (pnlMap) will render itself here --> <span id="map_filters_summary"></span>
<div id="map_pnlmap" class="w-100 h-100"></div> </div>
</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 ...@@ -8,33 +8,48 @@ uses
WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils, WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client, VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
XData.Web.Connection, ConnectionModule, Utils; XData.Web.Connection, ConnectionModule, Utils, uMapFilters;
type type
TFViewMap = class(TWebForm) TFViewMap = class(TWebForm)
btnMenu: TWebButton;
btnAlerts: TWebButton;
btnGroups: TWebButton;
btnLocate: TWebButton;
btnFilters: TWebButton;
btnDisplay: TWebButton;
pnlMap: TWebPanel; pnlMap: TWebPanel;
lfMap: TTMSFNCLeaflet; lfMap: TTMSFNCLeaflet;
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
xdwcMap: TXDataWebClient; xdwcMap: TXDataWebClient;
tmrRefresh: TWebTimer; tmrRefresh: TWebTimer;
btnFindLocation: TWebButton;
tmrLocate: TWebTimer;
procedure lfMapMapInitialized(Sender: TObject); procedure lfMapMapInitialized(Sender: TObject);
[async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
procedure lfMapCustomizeMarker(Sender: TObject; procedure lfMapCustomizeMarker(Sender: TObject;
var ACustomizeMarker: string); var ACustomizeMarker: string);
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrRefreshTimer(Sender: TObject);
procedure btnFindLocationClick(Sender: TObject);
procedure tmrLocateTimer(Sender: TObject);
private private
userLocationMarker: TTMSFNCMapsMarker;
geoWatchId: Integer;
FUnitsLoaded: Boolean; FUnitsLoaded: Boolean;
FComplaintsLoaded: 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; function CarIconForDistrict(const DistrictCode: string): string;
procedure UpdateDeviceLocation(lat, lng: Double);
procedure StartDeviceLocation;
procedure ApplyPendingUnitFocus;
procedure ApplyPendingComplaintFocus;
public public
procedure FocusUnit(const unitId: string);
procedure FocusComplaint(const complaintId: string);
end; end;
var var
...@@ -53,19 +68,81 @@ begin ...@@ -53,19 +68,81 @@ begin
FUnitsLoaded := False; FUnitsLoaded := False;
FComplaintsLoaded := False; FComplaintsLoaded := False;
httpReqGeoJson.Execute; httpReqGeoJson.Execute;
{$IFNDEF WIN32}
asm asm
window.showComplaintDetails = function (id) { window.showComplaintDetails = function (id) {
console.log('JS bridge showComplaintDetails called, id=', id); console.log('JS bridge showComplaintDetails called, id=', id);
try { try {
pas['View.Main'].FViewMain.ShowComplaintDetails(id); pas['View.Main'].FViewMain.ShowComplaintDetails(id);
console.log('TFViewMain.ShowComplaintDetails finished OK');
} catch (e) { } catch (e) {
console.log('Error in TFViewMain.ShowComplaintDetails', 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; end;
{$ENDIF}
end; end;
...@@ -73,14 +150,13 @@ end; ...@@ -73,14 +150,13 @@ end;
var var
i: Integer; i: Integer;
P: TTMSFNCMapsPolygon; P: TTMSFNCMapsPolygon;
nm: string;
begin begin
lfMap.BeginUpdate; lfMap.BeginUpdate;
try try
lfMap.Polygons.Clear; lfMap.Polygons.Clear;
Console.Log('GeoJSON len=' + AResponse.Length.ToString); Console.Log('GeoJSON len=' + AResponse.Length.ToString);
lfMap.LoadGeoJSONFromText(AResponse, True, False); lfMap.LoadGeoJSONFromText(AResponse, True, Trim(FPendingUnitId) = '');
Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString); Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString);
for i := 0 to lfMap.Polygons.Count - 1 do for i := 0 to lfMap.Polygons.Count - 1 do
...@@ -120,14 +196,17 @@ begin ...@@ -120,14 +196,17 @@ begin
P.StrokeWidth := 2; P.StrokeWidth := 2;
end; end;
if lfMap.Polygons.Count > 0 then
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
finally finally
lfMap.EndUpdate; lfMap.EndUpdate;
end; end;
await(LoadPointsAsync); await(LoadPointsAsync(True));
if mapFilters = nil then
begin
mapFilters := TMapFilters.Create(lfMap);
mapFilters.Init;
end;
end; end;
function TFViewMap.CarIconForDistrict(const DistrictCode: string): string; function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
...@@ -150,77 +229,198 @@ begin ...@@ -150,77 +229,198 @@ begin
end; end;
[async] procedure TFViewMap.LoadPointsAsync; [async] procedure TFViewMap.LoadPointsAsync(showBusy: Boolean);
var var
resp: TXDataClientResponse; resp: TXDataClientResponse;
root, item, uo: TJSObject; root, item, uo: TJSObject;
data, units: TJSArray; units: TJSArray;
i, ui: Integer; i, ui: Integer;
m: TTMSFNCMapsMarker; m: TTMSFNCMapsMarker;
lat, lng: Double; lat, lng: Double;
uname, dist: string; uName, dist: string;
complaintId, codeDesc, dispatchDist, priority: string; unitId, callType, priorityText, statusText: string;
complaintId, codeDesc, dispatchDist, priority, business: string;
pngName, iconUrl, rowsHtml: 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 begin
ShowSpinner('spinner'); if FLoadingPoints then
FUnitsLoaded := False; Exit;
FComplaintsLoaded := False;
FLoadingPoints := True;
// --- Units --------------------------------------------------------------- if showBusy then
ShowSpinner('spinner');
try try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', [])); FUnitsLoaded := False;
root := TJSObject(resp.Result); FComplaintsLoaded := False;
data := TJSArray(root['data']);
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 // --- Fetch Complaints ----------------------------------------------------
begin try
lfMap.BeginUpdate; resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
try root := TJSObject(resp.Result);
for i := 0 to data.Length - 1 do 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 begin
item := TJSObject(data[i]); item := TJSObject(unitsData[i]);
lat := Double(item['Lat']); lat := Double(item['Lat']);
lng := Double(item['Lng']); lng := Double(item['Lng']);
uname := string(item['UnitName']); uName := string(item['UnitName']);
dist := string(item['District']); 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 := lfMap.Markers.Add;
m.Latitude := lat; m.Latitude := lat;
m.Longitude := lng; m.Longitude := lng;
m.Title := uname + IfThen(dist <> '', ' / ' + dist, '');
m.DataString := 'unit';
m.IconURL := CarIconForDistrict(dist);
end;
finally
lfMap.EndUpdate;
end;
end;
FUnitsLoaded := True; m.Title :=
except '<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
on E: EXDataClientRequestException do '<div class="fw-semibold small">' +
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage); '<span class="fw-bold">Unit:</span> ' + uName +
end; '</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 ---------------------------------------------------------- m.DataString := 'unit|' + unitId;
try console.log('Unit marker ds=' + m.DataString);
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', [])); m.IconURL := CarIconForDistrict(dist);
root := TJSObject(resp.Result); end;
data := TJSArray(root['data']); end;
if data <> nil then // Add complaint markers
begin if complaintsData <> nil then
lfMap.BeginUpdate; begin
try for i := 0 to complaintsData.Length - 1 do
for i := 0 to data.Length - 1 do
begin begin
item := TJSObject(data[i]); item := TJSObject(complaintsData[i]);
complaintId := string(item['ComplaintId']); complaintId := string(item['ComplaintId']);
codeDesc := string(item['DispatchCodeDesc']); codeDesc := string(item['DispatchCodeDesc']);
dispatchDist := string(item['DispatchDistrict']); dispatchDist := string(item['DispatchDistrict']);
priority := string(item['Priority']); priority := string(item['Priority']);
business := string(item['Business']);
lat := Double(item['Lat']); lat := Double(item['Lat']);
lng := Double(item['Lng']); lng := Double(item['Lng']);
...@@ -245,8 +445,8 @@ begin ...@@ -245,8 +445,8 @@ begin
rowsHtml := rowsHtml + rowsHtml := rowsHtml +
'<tr>' + '<tr>' +
'<td>' + string(uo['Unit']) + '</td>' + '<td>' + string(uo['Unit']) + '</td>' +
'<td>' + string(uo['Status']) + '</td>' + '<td>' + string(uo['Status']) + '</td>' +
'<td>' + string(uo['Updated']) + '</td>' + '<td>' + string(uo['Updated']) + '</td>' +
'</tr>'; '</tr>';
end; end;
...@@ -254,11 +454,13 @@ begin ...@@ -254,11 +454,13 @@ begin
else else
rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>'; rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';
// Complaint Markers
m := lfMap.Markers.Add; m := lfMap.Markers.Add;
m.Latitude := lat; m.Latitude := lat;
m.Longitude := lng; m.Longitude := lng;
m.DataString := 'complaint|' + complaintId;
m.IconURL := iconUrl;
m.Title := m.Title :=
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' + '<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
'<div class="fw-semibold small">' + '<div class="fw-semibold small">' +
...@@ -273,6 +475,12 @@ begin ...@@ -273,6 +475,12 @@ begin
'<div class="small">' + '<div class="small">' +
'<span class="fw-bold">Dispatch District:</span> ' + dispatchDist + '<span class="fw-bold">Dispatch District:</span> ' + dispatchDist +
'</div>' + '</div>' +
IfThen(Trim(business) <> '',
'<div class="small">' +
'<span class="fw-bold">Business:</span> ' + business +
'</div>',
''
) +
'<div class="small mb-1">' + '<div class="small mb-1">' +
'<span class="fw-bold">Address:</span> ' + string(item['Address']) + '<span class="fw-bold">Address:</span> ' + string(item['Address']) +
'</div>' + '</div>' +
...@@ -298,22 +506,22 @@ begin ...@@ -298,22 +506,22 @@ begin
'</button>' + '</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
m.IconURL := iconUrl;
end; end;
finally
lfMap.EndUpdate;
end; end;
finally
lfMap.EndUpdate;
end; end;
FComplaintsLoaded := True; if mapFilters <> nil then
except mapFilters.Apply;
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
HideSpinner('spinner'); ApplyPendingUnitFocus;
ApplyPendingComplaintFocus;
finally
if showBusy then
HideSpinner('spinner');
FLoadingPoints := False;
end;
end; end;
...@@ -322,12 +530,17 @@ begin ...@@ -322,12 +530,17 @@ begin
ACustomizeMarker := ACustomizeMarker :=
'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 + 'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
'var rawTitle = (o && o.title) ? o.title : "";' + #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.tooltipHtml = rawTitle;' + #13#10 +
'o.title = "";' + #13#10 + 'o.title = "";' + #13#10 +
'var t = o.tooltipHtml || "";' + #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 + '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.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
'try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 + 'try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
...@@ -382,28 +595,25 @@ end; ...@@ -382,28 +595,25 @@ end;
procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin begin
ACustomizeCSS := ACustomizeCSS :=
// popup container: rounded, shadow // --- Popup container----------------------------------------------------------
'.emi-tip .leaflet-popup-content-wrapper{' + '.emi-tip .leaflet-popup-content-wrapper{' +
'border-radius:8px;' + 'border-radius:8px;' +
'box-shadow:0 4px 14px rgba(0,0,0,.25);' + 'box-shadow:0 4px 14px rgba(0,0,0,.25);' +
'}' + #13#10 + '}' + #13#10 +
// --- Popup content------------------------------------------------------------
// popup content: no fixed width, max 260px on normal screens
'.emi-tip .leaflet-popup-content{' + '.emi-tip .leaflet-popup-content{' +
'margin:0;' + 'margin:0;' +
'padding:0;' + 'padding:0;' +
'width:auto;' + 'width:auto;' +
'max-width:260px;' + 'max-width:260px;' +
'}' + #13#10 + '}' + #13#10 +
// --- Media Query: on very small screens, lets it grow almost full width ------
// on very small screens, let it grow almost full width
'@media (max-width:480px){' + '@media (max-width:480px){' +
'.emi-tip .leaflet-popup-content{' + '.emi-tip .leaflet-popup-content{' +
'max-width:calc(100vw - 32px);' + 'max-width:calc(100vw - 32px);' +
'}' + '}' +
'}' + #13#10 + '}' + #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{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 thead{display:table-header-group;}'+#13#10+
'.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+ '.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+
...@@ -416,13 +626,138 @@ begin ...@@ -416,13 +626,138 @@ begin
'font-size:11px;' + 'font-size:11px;' +
'vertical-align:middle;' + 'vertical-align:middle;' +
'}' + #13#10 + '}' + #13#10 +
// --- Marker Badge ------------------------------------------------------------
// marker badge
'.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+ '.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
'.emi-marker-img{display: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; 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. 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 ...@@ -40,66 +40,22 @@ object FViewUnits: TFViewUnits
DataSource = wdsUnits DataSource = wdsUnits
ItemTemplate = ItemTemplate =
'<div class="list-section-header small fw-semibold bg-body-second' + '<div class="list-section-header small fw-semibold bg-body-second' +
'ary text-dark rounded-1 px-2 mb-1"> (%DistrictHeader%)</div><di' + 'ary text-dark rounded-1 px-2 mb-1">(%DistrictHeader%)</div><div ' +
'v class="card border shadow-sm position-relative"> <div class="' + 'class="card border shadow-sm"> <div class="card-body py-2 px-3 ' +
'card-body py-2 px-3"> <!-- Unit + Status --> <div class="f' + 'd-flex gap-2"> <div class="flex-grow-1"> <div class="fw-' +
'w-bold text-uppercase small"> (%UnitName%)&nbsp;-&nbsp;(%St' + 'bold text-uppercase small">(%UnitName%)&nbsp;-&nbsp;(%Status%)</' +
'atus%) </div> <!-- Location --> <div class="small text-' + 'div> <div class="small text-body-secondary mb-1">(%Location' +
'body-secondary mb-1"> (%Location%) </div> <!-- Call t' + '%)</div> <div class="small">(%CallType%)</div> <hr cla' +
'ype --> <div class="small">(%CallType%)</div> <!-- Divider' + 'ss="unit-divider my-1" style="width: 80px; margin-left: 0" /> ' +
' line --> <hr class="unit-divider my-1" style="width:80px;mar' + ' <div class="small officer1">(%Officer1%)</div> <div clas' +
'gin-left:0;"> <!-- Officers --> <div class="small officer1' + 's="small officer2">(%Officer2%)</div> </div> <div class="d' +
'">(%Officer1%)</div> <div class="small officer2">(%Officer2%)' + '-flex flex-column justify-content-center gap-1"> <button ty' +
'</div> </div> <!-- Vertically centered Details button on the r' + 'pe="button" class="btn btn-primary btn-sm btn-unit-details" data' +
'ight --> <div class="position-absolute top-50 end-0 translate-m' + '-unitid="(%UnitId%)">Details</button> <button type="button"' +
'iddle-y pe-2"> <button type="button" class="btn bt' + ' class="btn btn-primary btn-sm btn-unit-map" data-unitid="(%Unit' +
'n-primary btn-sm btn-unit-details" data-unitid="(%Uni' + 'Id%)">View On Map</button> </div> </div></div>'
'tId%)"> Details </button> </div></div>'
ListSource = wdsUnits ListSource = wdsUnits
end end
object btnRefresh: TWebButton
Left = 110
Top = 82
Width = 53
Height = 25
Caption = 'Refresh'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnrefresh'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnRefreshClick
end
object btnGroup: TWebButton
Left = 186
Top = 82
Width = 43
Height = 25
Caption = 'Group'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btngroup'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilter: TWebButton
Left = 250
Top = 82
Width = 37
Height = 25
Caption = 'Filter'
ChildOrder = 1
ElementClassName = 'btn btn-light'
ElementID = 'units_btnfilter'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object wdsUnits: TWebDataSource object wdsUnits: TWebDataSource
AutoEdit = False AutoEdit = False
DataSet = xdwdsUnits DataSet = xdwdsUnits
......
<div class="d-flex flex-column h-100">
<div class="sticky-top"> <!-- Header / controls (non-scrolling) -->
<!-- Local navbar (Units) --> <div class="flex-shrink-0">
<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>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"><!-- removed sticky-top --> <div class="bg-light border-bottom py-2">
<div class="container-fluid"> <div class="container-fluid">
<div class="input-group"> <div class="input-group">
<span class="input-group-text bg-white"><i class="fa fa-search"></i></span> <span class="input-group-text bg-white"><i class="fa fa-search"></i></span>
<input id="units_search" class="form-control" placeholder="Search..."> <input id="units_search" class="form-control" placeholder="Search...">
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <!-- /sticky-top wrapper --> <!-- Scrolling list area -->
<div class="flex-grow-1 overflow-auto" style="min-height:0;">
<!-- Units list container --> <div class="container-fluid mt-2">
<div class="container-fluid mt-2"> <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-8">
<div class="col-12 col-md-10 col-lg-8"> <!-- This is where the DBListControl will inject cards -->
<!-- This is where the DBListControl will inject cards --> <div id="units_dbl_unit_list" class="d-flex flex-column gap-2">
<div id="units_dbl_unit_list" class="d-flex flex-column gap-2"> <!-- Cards will render here -->
<!-- Cards will render here --> </div>
<!-- Entry Count Label -->
<label id="unitss_lblentries" class="mt-2 d-block"></label>
</div>
</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>
</div>
</div>
...@@ -13,9 +13,6 @@ uses ...@@ -13,9 +13,6 @@ uses
type type
TFViewUnits = class(TWebForm) TFViewUnits = class(TWebForm)
dblUnitsList: TWebDBListControl; dblUnitsList: TWebDBListControl;
btnRefresh: TWebButton;
btnGroup: TWebButton;
btnFilter: TWebButton;
wdsUnits: TWebDataSource; wdsUnits: TWebDataSource;
xdwdsUnits: TXDataWebDataSet; xdwdsUnits: TXDataWebDataSet;
xdwcUnits: TXDataWebClient; xdwcUnits: TXDataWebClient;
...@@ -35,6 +32,7 @@ type ...@@ -35,6 +32,7 @@ type
private private
FLoading: Boolean; FLoading: Boolean;
[async] procedure GetUnits; [async] procedure GetUnits;
procedure HandleListClick(e: TJSMouseEvent);
public public
end; end;
...@@ -49,36 +47,76 @@ implementation ...@@ -49,36 +47,76 @@ implementation
procedure TFViewUnits.WebFormCreate(Sender: TObject); procedure TFViewUnits.WebFormCreate(Sender: TObject);
begin begin
console.log('Units.WebFormCreate: Starting setup...');
DMConnection.ApiConnection.Connected := True; DMConnection.ApiConnection.Connected := True;
console.log('API connection active:', DMConnection.ApiConnection.Connected); Document.addEventListener('click', @HandleListClick);
tmrRefresh.Enabled := False; tmrRefresh.Enabled := False;
GetUnits; GetUnits;
tmrRefresh.Enabled := True; tmrRefresh.Enabled := True;
// {$IFNDEF WIN32} asm
// asm if (!window.showUnitDetails) {
// var root = pas.TFViewUnits(Self).dblUnitsList.ElementHandle; window.showUnitDetails = function (id) {
// if (root && !root.__emiDelegated) { console.log('JS bridge showUnitDetails called, id=', id);
// root.__emiDelegated = true; try {
// pas['View.Main'].FViewMain.ShowUnitDetails(id);
// root.addEventListener('click', function (e) { } catch (e) {
// // Look for a click on, or inside, the details button console.log('Error in TFViewMain.ShowUnitDetails', e);
// var btn = e.target && e.target.closest('.btn-unit-details'); }
// if (!btn || !root.contains(btn)) return; };
// }
// e.preventDefault(); end;
// e.stopPropagation();
//
// var unitId = btn.getAttribute('data-unitid') || '';
// pas.TFViewUnits(Self).OpenUnitDetails(unitId);
// }, { passive: true });
// }
// end;
// {$ENDIF}
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); procedure TFViewUnits.btnRefreshClick(Sender: TObject);
begin begin
GetUnits; GetUnits;
...@@ -122,6 +160,7 @@ begin ...@@ -122,6 +160,7 @@ begin
end; end;
end; end;
finally finally
FLoading := False;
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
console.log('GetUnits complete'); console.log('GetUnits complete');
end; end;
......
/* --- TMS WEB Core Specific Fixes --- */
/* Removes the default border from the main Form wrapper */
span.card {
border: none;
}
/* --- Login Screen Styling --- */
.login-card { .login-card {
display: inline-block; display: inline-block; /* Or use d-flex on the parent to center it */
width: 300px; /* Adjust width as needed */ width: 100%;
max-width: 350px; /* Better than fixed 300px */
padding: 0; padding: 0;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #fff; background-color: #fff;
} }
.mr-2 { /* Optional: Custom navbar look for the login screen */
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
}
.login-navbar { .login-navbar {
max-width: 1200px; /* Set the max-width to match a medium screen */ max-width: 1200px;
margin: auto; margin: auto;
border-bottom-left-radius: 10px; /* Round the bottom left corner */ border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px; /* Round the bottom right corner */ border-bottom-right-radius: 10px;
border: 1px solid #d3d3d3; border: 1px solid #d3d3d3;
} }
.navbar-toggler { /* --- Table Customization --- */
display: none; /* Bootstrap has .table-hover, but this sets your specific blue color */
} .table tbody tr:hover {
background-color: #d1e7fd;
.dropdown-menu a { cursor: pointer;
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 */
} }
/* Style for the selected number */ /* --- Pagination Theme Overrides --- */
.selected-number .page-link { /* These override Bootstrap's blue to your specific darker blue (#204d74) */
background-color: #204d74;
color: #fff !important;
}
/* Style for the unselected numbers and text (previous/next) */ .pagination .page-link {
.pagination .page-item a, color: #204d74; /* Text color for standard links */
.pagination .page-item span {
color: #204d74;
} }
.pagination .page-item.active .page-link, /* Active State (Selected Page) */
.pagination .page-item.active .page-link:hover, .pagination .page-item.active .page-link {
.pagination .page-item.active .page-link:focus {
background-color: #204d74; background-color: #204d74;
border-color: #204d74; border-color: #204d74;
color: #fff !important; color: #fff !important;
} }
/* This is needed to get rid of the line that was appearing. */ /* Hover State */
span.card { .pagination .page-item:not(.active) .page-link:hover {
border: none; background-color: #e9ecef; /* Standard Bootstrap light grey hover */
} color: #16344a; /* Darker text on hover */
.modal-backdrop {
z-index: 1040 !important;
} }
.modal { /* --- Utilities --- */
z-index: 1055 !important; .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 ...@@ -21,7 +21,9 @@ uses
View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html}, View.EditUser in 'View.EditUser.pas' {FViewEditUser: TWebForm} {*.html},
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html}, View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html}; View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas';
{$R *.res} {$R *.res}
......
...@@ -182,11 +182,18 @@ ...@@ -182,11 +182,18 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </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="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
<None Include="assets\bpddistricts-updated.geojson"/> <None Include="assets\bpddistricts-updated.geojson"/>
<None Include="assets\bpddistricts.geojson"/> <None Include="assets\bpddistricts.geojson"/>
<None Include="config\config.json"/>
<RcItem Include="assets\markers\alarm_1.png"> <RcItem Include="assets\markers\alarm_1.png">
<ResourceType>RCDATA</ResourceType> <ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_1</ResourceId> <ResourceId>PngImage_1</ResourceId>
...@@ -479,7 +486,6 @@ ...@@ -479,7 +486,6 @@
<ResourceType>RCDATA</ResourceType> <ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_73</ResourceId> <ResourceId>PngImage_73</ResourceId>
</RcItem> </RcItem>
<None Include="config\config.json"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </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