Commit 445d8738 by Mac Stephens

Improve shared device management, add mobile cards, and bring BFD live-update…

Improve shared device management, add mobile cards, and bring BFD live-update handling in line with WC
parent 28a1f426
......@@ -37,6 +37,8 @@ type
FSelectProc: TSelectProc;
FLoading: Boolean;
FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetComplaints;
procedure HandleListClick(e: TJSMouseEvent);
procedure ShowHideBusinessRows;
......@@ -57,6 +59,8 @@ procedure TFViewComplaints.WebFormCreate(Sender: TObject);
begin
Document.addEventListener('click', @HandleListClick);
FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetComplaints;
asm
if (!window.showComplaintDetails) {
......@@ -176,12 +180,31 @@ begin
HideSpinner('spinner');
FFirstLoad := False;
end;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetComplaints;
end;
end;
end;
procedure TFViewComplaints.RefreshData;
begin
Console.Log('Complaints.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetComplaints;
end;
......@@ -190,7 +213,10 @@ var
complaintsCount: Integer;
begin
if FLoading then
begin
FPendingWsData := aRespObj;
Exit;
end;
xdwdsComplaints.Close;
xdwdsComplaints.SetJsonData(aRespObj['data']);
......
......@@ -31,6 +31,9 @@ type
FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean;
FLoadingPoints: Boolean;
FRefreshPending: Boolean;
FPendingWsUnitMapData: TJSArray;
FPendingWsComplaintMapData: TJSArray;
mapFilters: TMapFilters;
FPendingUnitId: string;
FPendingComplaintId: string;
......@@ -243,33 +246,54 @@ begin
unitsData := nil;
complaintsData := nil;
// --- Fetch Units ---------------------------------------------------------
// Section: Fetch units.
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
unitsData := TJSArray(root['data']);
FUnitsLoaded := True;
FUnitsLoaded := Assigned(unitsData);
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Units error: ' + E.Message);
end;
// --- Fetch Complaints ----------------------------------------------------
// Section: Fetch incidents.
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result);
complaintsData := TJSArray(root['data']);
FComplaintsLoaded := True;
FComplaintsLoaded := Assigned(complaintsData);
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Complaints error: ' + E.Message);
end;
// Note: Apply the latest buffered WebSocket snapshot after the HTTP load.
if Assigned(FPendingWsUnitMapData) then
begin
unitsData := FPendingWsUnitMapData;
FPendingWsUnitMapData := nil;
FUnitsLoaded := True;
end;
// --- Place markers (BeginUpdate wraps both so the map redraws once) ------
if Assigned(FPendingWsComplaintMapData) then
begin
complaintsData := FPendingWsComplaintMapData;
FPendingWsComplaintMapData := nil;
FComplaintsLoaded := True;
end;
// Section: Render both marker sets in one update.
lfMap.BeginUpdate;
try
PlaceUnitMarkers(unitsData);
PlaceComplaintMarkers(complaintsData);
if FUnitsLoaded then
PlaceUnitMarkers(unitsData);
if FComplaintsLoaded then
PlaceComplaintMarkers(complaintsData);
finally
lfMap.EndUpdate;
end;
......@@ -283,6 +307,12 @@ begin
if showBusy then
HideSpinner('spinner');
FLoadingPoints := False;
if FRefreshPending then
begin
FRefreshPending := False;
LoadPointsAsync(False);
end;
end;
end;
......@@ -477,8 +507,13 @@ end;
procedure TFViewMap.ApplyWsUnitMapData(aData: TJSArray);
begin
// Skip if the map is still initialising or an HTTP load is in flight.
if (not Assigned(mapFilters)) or FLoadingPoints then
if FLoadingPoints then
begin
FPendingWsUnitMapData := aData;
Exit;
end;
if not Assigned(mapFilters) then
Exit;
lfMap.BeginUpdate;
......@@ -494,7 +529,13 @@ end;
procedure TFViewMap.ApplyWsComplaintMapData(aData: TJSArray);
begin
if (not Assigned(mapFilters)) or FLoadingPoints then
if FLoadingPoints then
begin
FPendingWsComplaintMapData := aData;
Exit;
end;
if not Assigned(mapFilters) then
Exit;
lfMap.BeginUpdate;
......@@ -692,6 +733,13 @@ end;
procedure TFViewMap.RefreshData;
begin
Console.Log('Map.RefreshData');
if FLoadingPoints then
begin
FRefreshPending := True;
Exit;
end;
LoadPointsAsync(False);
end;
......
......@@ -45,6 +45,8 @@ type
private
FLoading: Boolean;
FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetUnits;
procedure HandleListClick(e: TJSMouseEvent);
procedure PrepareUnitData(aRespObj: TJSObject);
......@@ -66,6 +68,8 @@ begin
DMConnection.ApiConnection.Connected := True;
Document.addEventListener('click', @HandleListClick);
FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetUnits;
asm
......@@ -232,12 +236,31 @@ begin
FFirstLoad := False;
FLoading := False;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetUnits;
end;
end;
end;
procedure TFViewUnits.RefreshData;
begin
Console.Log('Units.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetUnits;
end;
......@@ -246,7 +269,10 @@ var
unitCount: Integer;
begin
if FLoading then
begin
FPendingWsData := aRespObj;
Exit;
end;
PrepareUnitData(aRespObj);
......
unit Auth.Service;
unit Auth.Service;
interface
......@@ -7,10 +7,10 @@ uses
XData.Web.Client;
const
TOKEN_NAME = 'WEBEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WEBEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WEBEMIMOBILE_KEY_TYPE';
DEVICE_MANAGER_TOKEN_NAME = 'WEBEMIMOBILE_DEVICE_MANAGER_TOKEN';
TOKEN_NAME = 'WCEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WCEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WCEMIMOBILE_KEY_TYPE';
DEVICE_MANAGER_TOKEN_NAME = 'WCEMIMOBILE_DEVICE_MANAGER_TOKEN';
type
TOnLoginSuccess = reference to procedure;
......@@ -112,7 +112,35 @@ end;
{ TAuthService }
constructor TAuthService.Create;
const
StorageKeys: array[0..2] of string = (TOKEN_NAME, CREDENTIAL_NAME, KEY_TYPE_NAME);
var
i: Integer;
keyName, oldKeyName, storedValue: string;
begin
// Note: Preserve existing WC registrations when renaming the browser storage keys.
for i := Low(StorageKeys) to High(StorageKeys) do
begin
keyName := StorageKeys[i];
oldKeyName := StringReplace(keyName, 'WCEMIMOBILE_', 'WEBEMIMOBILE_', []);
storedValue := window.localStorage.getItem(oldKeyName);
if not isNull(storedValue) then
begin
if isNull(window.localStorage.getItem(keyName)) then
window.localStorage.setItem(keyName, storedValue);
window.localStorage.removeItem(oldKeyName);
end;
end;
oldKeyName := 'WEBEMIMOBILE_DEVICE_MANAGER_TOKEN';
storedValue := window.sessionStorage.getItem(oldKeyName);
if not isNull(storedValue) then
begin
if isNull(window.sessionStorage.getItem(DEVICE_MANAGER_TOKEN_NAME)) then
window.sessionStorage.setItem(DEVICE_MANAGER_TOKEN_NAME, storedValue);
window.sessionStorage.removeItem(oldKeyName);
end;
FClient := TXDataWebClient.Create(nil);
FClient.Connection := DMConnection.AuthConnection;
end;
......
......@@ -31,10 +31,10 @@ type
function DeviceList: TDataSet; virtual; abstract;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; virtual; abstract;
function UnrevokeDevice(CredentialId: string): Int64; virtual; abstract;
function DeviceNameExists(DeviceName: string): Boolean; virtual; abstract;
function PendingPhone(PhoneNumber: string): TDataSet; virtual; abstract;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; virtual; abstract;
function DeletePendingDevice(PhoneNumber: string): Int64; virtual; abstract;
function DeleteDevice(CredentialId: string): Int64; virtual; abstract;
function DeleteDevice(DeviceId: Integer): Int64; virtual; abstract;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; virtual; abstract;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; virtual; abstract;
function ActiveDevice(CredentialId: string): TDataSet; virtual; abstract;
......
......@@ -377,6 +377,21 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamType = ptInput
end>
end
object uqDeviceName: TUniQuery
Connection = ucDevices
SQL.Strings = (
'SELECT ID FROM DEVICE_REGISTRATIONS'
'WHERE UPPER(TRIM(DEVICE_NAME)) = UPPER(:NAME)')
ReadOnly = True
Left = 760
Top = 440
ParamData = <
item
DataType = ftString
Name = 'NAME'
ParamType = ptInput
end>
end
object uqPendingPhone: TUniQuery
Connection = ucDevices
SQL.Strings = (
......@@ -420,36 +435,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamType = ptInput
end>
end
object uqDeletePendingDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE'
'FROM DEVICE_REGISTRATIONS'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
Active = False
Left = 60
Top = 550
ParamData = <
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
end>
end
object uqDeleteDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE'
'FROM DEVICE_REGISTRATIONS'
'WHERE CREDENTIAL_ID = :CID')
'WHERE ID = :ID')
Active = False
Left = 250
Top = 550
ParamData = <
item
DataType = ftString
Name = 'CID'
DataType = ftInteger
Name = 'ID'
ParamType = ptInput
end>
end
......
......@@ -28,8 +28,8 @@ type
uqRevokeDevice: TUniQuery;
uqUnrevokeDevice: TUniQuery;
uqPendingPhone: TUniQuery;
uqDeviceName: TUniQuery;
uqAddPendingDevice: TUniQuery;
uqDeletePendingDevice: TUniQuery;
uqDeleteDevice: TUniQuery;
uqReserveRedeemCode: TUniQuery;
uqUpdateDeviceUsername: TUniQuery;
......@@ -57,10 +57,10 @@ type
function DeviceList: TDataSet; override;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; override;
function UnrevokeDevice(CredentialId: string): Int64; override;
function DeviceNameExists(DeviceName: string): Boolean; override;
function PendingPhone(PhoneNumber: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeletePendingDevice(PhoneNumber: string): Int64; override;
function DeleteDevice(CredentialId: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; override;
function ActiveDevice(CredentialId: string): TDataSet; override;
......@@ -242,6 +242,18 @@ begin
Result := uqUnrevokeDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeviceNameExists(DeviceName: string): Boolean;
begin
uqDeviceName.Close;
uqDeviceName.ParamByName('NAME').AsString := Trim(DeviceName);
uqDeviceName.Open;
try
Result := not uqDeviceName.IsEmpty;
finally
uqDeviceName.Close;
end;
end;
function TOracleDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
begin
uqPendingPhone.Close;
......@@ -260,18 +272,10 @@ begin
Result := uqAddPendingDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeletePendingDevice(PhoneNumber: string): Int64;
begin
uqDeletePendingDevice.Close;
uqDeletePendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqDeletePendingDevice.ExecSQL;
Result := uqDeletePendingDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeleteDevice(CredentialId: string): Int64;
function TOracleDeviceDatabase.DeleteDevice(DeviceId: Integer): Int64;
begin
uqDeleteDevice.Close;
uqDeleteDevice.ParamByName('CID').AsString := CredentialId;
uqDeleteDevice.ParamByName('ID').AsInteger := DeviceId;
uqDeleteDevice.ExecSQL;
Result := uqDeleteDevice.RowsAffected;
end;
......
......@@ -379,6 +379,21 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
Value = nil
end>
end
object uqDeviceName: TUniQuery
Connection = ucDevices
SQL.Strings = (
'SELECT ID FROM lems.device_registrations'
'WHERE UPPER(TRIM(DEVICE_NAME)) = UPPER(:NAME)')
ReadOnly = True
Left = 760
Top = 440
ParamData = <
item
DataType = ftString
Name = 'NAME'
ParamType = ptInput
end>
end
object uqPendingPhone: TUniQuery
Connection = ucDevices
SQL.Strings = (
......@@ -423,32 +438,16 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
Value = nil
end>
end
object uqDeletePendingDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE FROM lems.device_registrations WHERE phone_number = :PHON' +
'E AND status = '#39'pending'#39)
Left = 60
Top = 566
ParamData = <
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
Value = nil
end>
end
object uqDeleteDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE FROM lems.device_registrations WHERE credential_id = :CID')
'DELETE FROM lems.device_registrations WHERE id = :ID')
Left = 188
Top = 530
ParamData = <
item
DataType = ftString
Name = 'CID'
DataType = ftInteger
Name = 'ID'
ParamType = ptInput
Value = nil
end>
......
......@@ -28,8 +28,8 @@ type
uqRevokeDevice: TUniQuery;
uqUnrevokeDevice: TUniQuery;
uqPendingPhone: TUniQuery;
uqDeviceName: TUniQuery;
uqAddPendingDevice: TUniQuery;
uqDeletePendingDevice: TUniQuery;
uqDeleteDevice: TUniQuery;
uqReserveRedeemCode: TUniQuery;
uqMarkRedeemCodeUsed: TUniQuery;
......@@ -58,10 +58,10 @@ type
function DeviceList: TDataSet; override;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; override;
function UnrevokeDevice(CredentialId: string): Int64; override;
function DeviceNameExists(DeviceName: string): Boolean; override;
function PendingPhone(PhoneNumber: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeletePendingDevice(PhoneNumber: string): Int64; override;
function DeleteDevice(CredentialId: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; override;
function ActiveDevice(CredentialId: string): TDataSet; override;
......@@ -243,6 +243,18 @@ begin
Result := uqUnrevokeDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeviceNameExists(DeviceName: string): Boolean;
begin
uqDeviceName.Close;
uqDeviceName.ParamByName('NAME').AsString := Trim(DeviceName);
uqDeviceName.Open;
try
Result := not uqDeviceName.IsEmpty;
finally
uqDeviceName.Close;
end;
end;
function TPostgreSQLDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
begin
uqPendingPhone.Close;
......@@ -261,18 +273,10 @@ begin
Result := uqAddPendingDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeletePendingDevice(PhoneNumber: string): Int64;
begin
uqDeletePendingDevice.Close;
uqDeletePendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqDeletePendingDevice.ExecSQL;
Result := uqDeletePendingDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeleteDevice(CredentialId: string): Int64;
function TPostgreSQLDeviceDatabase.DeleteDevice(DeviceId: Integer): Int64;
begin
uqDeleteDevice.Close;
uqDeleteDevice.ParamByName('CID').AsString := CredentialId;
uqDeleteDevice.ParamByName('ID').AsInteger := DeviceId;
uqDeleteDevice.ExecSQL;
Result := uqDeleteDevice.RowsAffected;
end;
......
......@@ -39,8 +39,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function DeleteDevice(DeviceId: Integer): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end;
......
......@@ -43,8 +43,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function DeleteDevice(DeviceId: Integer): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end;
......@@ -527,6 +526,13 @@ begin
store := CreateDeviceStore;
try
if store.DeviceNameExists(DeviceName) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'A device with that name already exists. Enter a different device name.');
Exit;
end;
// Reject if a pending entry with this phone already exists
q := store.PendingPhone(normalizedPhone);
if not q.IsEmpty then
......@@ -549,50 +555,7 @@ begin
end;
end;
function TApiService.DeletePendingDevice(const PhoneNumber: string): TJSONObject;
var
affectedRows: Int64;
store: TDeviceStore;
normalizedPhone: string;
begin
RequireAdmin;
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
try
normalizedPhone := NormalizePhoneE164Api(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
store := CreateDeviceStore;
try
affectedRows := store.DeletePendingDevice(normalizedPhone);
if affectedRows > 0 then
begin
Logger.Log(2, 'TApiService.DeletePendingDevice - deleted phone: ' + normalizedPhone);
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Pending device removed.');
end
else
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Pending device not found.');
end;
finally
store.Free;
end;
end;
function TApiService.DeleteDevice(CredentialId: string): TJSONObject;
function TApiService.DeleteDevice(DeviceId: Integer): TJSONObject;
var
affectedRows: Int64;
store: TDeviceStore;
......@@ -602,21 +565,20 @@ begin
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
if Trim(CredentialId) = '' then
if DeviceId <= 0 then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'CredentialId is required.');
Result.AddPair('message', 'DeviceId is required.');
Exit;
end;
store := CreateDeviceStore;
try
affectedRows := store.DeleteDevice(Trim(CredentialId));
affectedRows := store.DeleteDevice(DeviceId);
if affectedRows > 0 then
begin
Logger.Log(2, 'TApiService.DeleteDevice - deleted credential: ' +
Copy(CredentialId, 1, 20));
Logger.Log(2, 'TApiService.DeleteDevice - deleted id: ' + IntToStr(DeviceId));
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Device deleted.');
end
......
......@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager
WidthPercent = 100.000000000000000000
OnClick = btnLogoutClick
end
object btnRefresh: TWebButton
Left = 620
Top = 50
Width = 75
Height = 25
Caption = 'Refresh'
ElementID = 'view.devmgr.btnrefresh'
OnClick = btnRefreshClick
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 800
......
<div class="container-fluid p-3 h-100 d-flex flex-column">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
<h5 class="mb-0 me-auto">Device Management</h5>
<button id="view.devmgr.btnlogout" class="btn btn-outline-secondary btn-sm me-2" type="button">
Log Out
</button>
<button id="view.devmgr.btnrefresh"
class="btn btn-outline-secondary btn-sm"
onclick="document.dispatchEvent(new CustomEvent('devmgr-refresh'))">
type="button">
Refresh
</button>
</div>
......@@ -26,9 +26,9 @@
<!-- Add pending device -->
<div class="card mb-3">
<div class="card-body py-2">
<div class="d-flex align-items-start gap-2 flex-wrap">
<span class="fw-semibold text-nowrap small mt-2">Add Device:</span>
<div style="width:180px;">
<div class="row g-2 align-items-start">
<div class="col-12 col-md-auto"><span class="fw-semibold text-nowrap small">Add Device:</span></div>
<div class="col-6 col-md-auto">
<input type="text"
id="view.devmgr.newname"
class="form-control form-control-sm"
......@@ -36,7 +36,7 @@
required>
<div class="invalid-feedback small">Device name is required.</div>
</div>
<div style="width:160px;">
<div class="col-6 col-md-auto">
<input type="tel"
id="view.devmgr.newphone"
class="form-control form-control-sm"
......@@ -44,7 +44,7 @@
required>
<div class="invalid-feedback small">Phone number is required.</div>
</div>
<div style="width:140px;">
<div class="col-6 col-md-auto">
<select id="view.devmgr.newtype"
class="form-select form-select-sm"
required>
......@@ -56,20 +56,24 @@
</select>
<div class="invalid-feedback small">Device type is required.</div>
</div>
<button id="view.devmgr.btnadd"
class="btn btn-primary btn-sm">Add</button>
<div class="col-6 col-md-auto d-grid">
<button id="view.devmgr.btnadd" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</div>
</div>
<!-- Device table -->
<div class="table-responsive flex-grow-1">
<div class="flex-grow-1 overflow-auto">
<div id="view.devmgr.cards" class="d-md-none"></div>
<div class="table-responsive d-none d-md-block">
<table class="table table-sm table-hover align-middle" id="view.devmgr.table">
<thead class="table-light sticky-top">
<tr>
<th style="min-width:130px;">Device Name</th>
<th style="min-width:100px;">Device Type</th>
<th style="min-width:120px;">Phone</th>
<th style="min-width:145px;">Verification Code</th>
<th style="min-width:110px;">Verification Code</th>
<th style="min-width:140px;">Username</th>
<th>Browser / User Agent</th>
<th style="min-width:135px;">Registered</th>
......@@ -82,6 +86,7 @@
<tbody id="view.devmgr.tbody">
</tbody>
</table>
</div>
<p id="view.devmgr.empty" class="text-muted d-none text-center py-4">
No devices found.
</p>
......
......@@ -2,7 +2,7 @@
Client=WC
[Settings]
LogFileNum=180
LogFileNum=184
webClientVersion=0.9.4.9
MemoLogLevel=3
FileLogLevel=4
......
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