Commit 1d985813 by Mac Stephens

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

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