Add Customers/Tenants pages. Improve login page. Implemented profile page, change and request password reset pages.

This commit is contained in:
Igor Kulikov
2021-06-15 16:38:37 +03:00
parent 21e42820fd
commit 17ce15c98d
33 changed files with 1312 additions and 326 deletions

View File

@@ -9,7 +9,7 @@ class AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
AssetDetailsPage(TbContext tbContext, String assetId):
super(tbContext,
entityId: assetId,
defaultTitle: 'Asset');
defaultTitle: 'Asset', subTitle: 'Asset details');
@override
Future<AssetInfo> fetchEntity(String assetId) {
@@ -18,9 +18,25 @@ class AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
@override
Widget buildEntityDetails(BuildContext context, AssetInfo asset) {
return ListTile(
title: Text('${asset.name}'),
subtitle: Text('${asset.type}'),
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('Asset name', style: labelTextStyle),
Text(asset.name, style: valueTextStyle),
SizedBox(height: 16),
Text('Type', style: labelTextStyle),
Text(asset.type, style: valueTextStyle),
SizedBox(height: 16),
Text('Label', style: labelTextStyle),
Text(asset.label ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Assigned to customer', style: labelTextStyle),
Text(asset.customerTitle ?? '', style: valueTextStyle),
]
)
);
}

View File

@@ -27,12 +27,12 @@ mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
@override
Widget buildEntityListCard(BuildContext context, AssetInfo asset) {
return _buildEntityListCard(context, asset, false);
return _buildCard(context, asset);
}
@override
Widget buildEntityListWidgetCard(BuildContext context, AssetInfo asset) {
return _buildEntityListCard(context, asset, true);
return _buildListWidgetCard(context, asset);
}
@override
@@ -40,21 +40,86 @@ mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
return Text(asset.name);
}
Widget _buildEntityListCard(BuildContext context, AssetInfo asset, bool listWidgetCard) {
Widget _buildCard(context, AssetInfo asset) {
return Row(
mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
fit: listWidgetCard ? FlexFit.loose : FlexFit.tight,
fit: FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: listWidgetCard ? 9 : 10, horizontal: 16),
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Row(
mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max,
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(asset.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
),
SizedBox(height: 4),
Text('${asset.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
SizedBox(width: 16),
Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
SizedBox(width: 16)
],
),
)
)
]
);
}
Widget _buildListWidgetCard(BuildContext context, AssetInfo asset) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: FlexFit.loose,
child:
Container(
padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: listWidgetCard ? FlexFit.loose : FlexFit.tight,
fit: FlexFit.loose,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -79,23 +144,10 @@ mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
))
],
)
),
(!listWidgetCard ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(asset.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
]
)
)
)
]
);

View File

@@ -44,7 +44,8 @@ class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage, _Aud
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.auditLog.entityName, style: TextStyle(
if (widget.auditLog.entityName != null)
Text(widget.auditLog.entityName!, style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
height: 20 / 16

View File

@@ -140,7 +140,7 @@ class _AuditLogCardState extends TbContextState<AuditLogCard, _AuditLogCardState
children: [
Flexible(
fit: FlexFit.tight,
child: AutoSizeText(widget.auditLog.entityName,
child: AutoSizeText(widget.auditLog.entityName ?? '',
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
@@ -224,12 +224,7 @@ class _AuditLogCardState extends TbContextState<AuditLogCard, _AuditLogCardState
}
_auditLogDetails(AuditLog auditLog) {
Navigator.of(tbContext.currentState!.context).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new AuditLogDetailsPage(tbContext, auditLog);
},
fullscreenDialog: true
));
tbContext.showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog));
}
}

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class CustomerDetailsPage extends ContactBasedDetailsPage<Customer> {
CustomerDetailsPage(TbContext tbContext, String customerId):
super(tbContext, entityId: customerId, defaultTitle: 'Customer', subTitle: 'Customer details');
@override
Future<Customer> fetchEntity(String customerId) {
return tbClient.getCustomerService().getCustomer(customerId);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'customer_details_page.dart';
import 'customers_page.dart';
class CustomerRoutes extends TbRoutes {
late var customersHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return CustomersPage(tbContext, searchMode: searchMode);
});
late var customerDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return CustomerDetailsPage(tbContext, params["id"][0]);
});
CustomerRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/customers", handler: customersHandler);
router.define("/customer/:id", handler: customerDetailsHandler);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin CustomersBase on EntitiesBase<Customer, PageLink> {
@override
String get title => 'Customers';
@override
String get noItemsFoundText => 'No customers found';
@override
Future<PageData<Customer>> fetchEntities(PageLink pageLink) {
return tbClient.getCustomerService().getCustomers(pageLink);
}
@override
void onEntityTap(Customer customer) {
navigateTo('/customer/${customer.id!.id}');
}
}

View File

@@ -0,0 +1,12 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/core/entity/entities_list.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'customers_base.dart';
class CustomersList extends BaseEntitiesWidget<Customer, PageLink> with CustomersBase, ContactBasedBase, EntitiesListStateBase {
CustomersList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/modules/customer/customers_list.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class CustomersPage extends TbPageWidget<CustomersPage, _CustomersPageState> {
final bool searchMode;
CustomersPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
@override
_CustomersPageState createState() => _CustomersPageState();
}
class _CustomersPageState extends TbPageState<CustomersPage, _CustomersPageState> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var customersList = CustomersList(tbContext, _pageLinkController, searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
tbContext,
onSearch: (searchText) => _pageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(customersList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/customers?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: customersList
);
}
@override
void dispose() {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -224,7 +224,11 @@ class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
if (widget._home == true && !tbContext.isHomePage()) {
return true;
}
return await _goBack();
if (readyState.value) {
return await _goBack();
} else {
return true;
}
},
child:
ValueListenableBuilder(

View File

@@ -4,9 +4,9 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_list_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart' as dashboardUi;
import 'package:thingsboard_app/modules/dashboard/dashboards_grid.dart';
import 'package:thingsboard_app/modules/tenant/tenants_widget.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
@@ -21,8 +21,6 @@ class HomePage extends TbContextWidget<HomePage, _HomePageState> {
class _HomePageState extends TbContextState<HomePage, _HomePageState> with AutomaticKeepAliveClientMixin<HomePage> {
final EntitiesListWidgetController _entitiesWidgetController = EntitiesListWidgetController();
@override
void initState() {
super.initState();
@@ -35,7 +33,6 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
@override
void dispose() {
_entitiesWidgetController.dispose();
super.dispose();
}
@@ -56,6 +53,16 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
semanticsLabel: 'ThingsBoard Logo')
)
),
actions: [
if (tbClient.isSystemAdmin()) IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
],
),
body: Builder(
builder: (context) {
@@ -81,40 +88,10 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> with Autom
}
}
/* List<Widget> _buildUserHome(BuildContext context) {
if (tbClient.isSystemAdmin()) {
return _buildSysAdminHome(context);
} else if (tbClient.isTenantAdmin()) {
return _buildTenantAdminHome(context);
} else {
return _buildCustomerUserHome(context);
}
} */
Widget _buildSysAdminHome(BuildContext context) {
return RefreshIndicator(
onRefresh: () => _entitiesWidgetController.refresh(),
child: ListView(
children: [Container(child: Text('TODO: Implement'))]
)
);
return TenantsWidget(tbContext);
}
/* List<Widget> _buildTenantAdminHome(BuildContext context) {
return [
AssetsListWidget(tbContext, controller: _entitiesWidgetController),
DevicesListWidget(tbContext, controller: _entitiesWidgetController),
DashboardsListWidget(tbContext, controller: _entitiesWidgetController)
];
}
List<Widget> _buildCustomerUserHome(BuildContext context) {
return [
AssetsListWidget(tbContext, controller: _entitiesWidgetController),
DevicesListWidget(tbContext, controller: _entitiesWidgetController),
DashboardsListWidget(tbContext, controller: _entitiesWidgetController)
];
} */
}
class HomeDashboard extends TbContextWidget<HomeDashboard, _HomeDashboardState> {

View File

@@ -23,7 +23,7 @@ class TbMainNavigationItem {
});
static Map<Authority, Set<String>> mainPageStateMap = {
Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/tenants', '/more']),
Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/more']),
Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
};
@@ -49,12 +49,6 @@ class TbMainNavigationItem {
];
switch(tbContext.tbClient.getAuthUser()!.authority) {
case Authority.SYS_ADMIN:
items.add(TbMainNavigationItem(
page: TextContextWidget(tbContext, 'Tenants TODO'),
title: 'Tenants',
icon: Icon(Icons.supervisor_account),
path: '/tenants'
));
break;
case Authority.TENANT_ADMIN:
case Authority.CUSTOMER_USER:

View File

@@ -29,7 +29,10 @@ class _MorePageState extends TbContextState<MorePage, _MorePageState> {
children: [
Icon(Icons.account_circle, size: 48, color: Color(0xFFAFAFAF)),
Spacer(),
IconButton(icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)), onPressed: () => navigateTo('/profile'))
IconButton(icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)), onPressed: () async {
await navigateTo('/profile');
setState(() {});
})
],
),
SizedBox(height: 22),

View File

@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
class ChangePasswordPage extends TbContextWidget<ChangePasswordPage, _ChangePasswordPageState> {
ChangePasswordPage(TbContext tbContext) : super(tbContext);
@override
_ChangePasswordPageState createState() => _ChangePasswordPageState();
}
class _ChangePasswordPageState extends TbContextState<ChangePasswordPage, _ChangePasswordPageState> {
final _isLoadingNotifier = ValueNotifier<bool>(false);
final _showCurrentPasswordNotifier = ValueNotifier<bool>(false);
final _showNewPasswordNotifier = ValueNotifier<bool>(false);
final _showNewPassword2Notifier = ValueNotifier<bool>(false);
final _changePasswordFormKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: TbAppBar(
tbContext,
title: const Text('Change Password'),
),
body: Stack(
children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(16),
child: FormBuilder(
key: _changePasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
ValueListenableBuilder(
valueListenable: _showCurrentPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'currentPassword',
obscureText: !showPassword,
autofocus: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Current password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showCurrentPasswordNotifier.value = !_showCurrentPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'Current password *'
),
);
}
),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'newPassword',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'New password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showNewPasswordNotifier.value = !_showNewPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'New password *'
),
);
}
),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPassword2Notifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'newPassword2',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'New password again is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showNewPassword2Notifier.value = !_showNewPassword2Notifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'New password again *'
),
);
}
),
SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(child: Text('Change Password'))
)
]
),
)
),
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
} else {
return SizedBox.shrink();
}
}
)
],
)
);
}
Future<void> _changePassword() async {
FocusScope.of(context).unfocus();
if (_changePasswordFormKey.currentState?.saveAndValidate() ?? false) {
var formValue = _changePasswordFormKey.currentState!.value;
String currentPassword = formValue['currentPassword'];
String newPassword = formValue['newPassword'];
String newPassword2 = formValue['newPassword2'];
if (newPassword != newPassword2) {
showErrorNotification('Entered passwords must be same!');
} else {
_isLoadingNotifier.value = true;
try {
await Future.delayed(Duration(milliseconds: 300));
await tbClient.changePassword(currentPassword, newPassword);
pop(true);
} catch(e) {
_isLoadingNotifier.value = false;
}
}
}
}
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:thingsboard_app/modules/profile/change_password_page.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
@@ -20,21 +22,39 @@ class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
late Future<User> userFuture;
final _isLoadingNotifier = ValueNotifier<bool>(true);
final _profileFormKey = GlobalKey<FormBuilderState>();
User? _currentUser;
@override
void initState() {
super.initState();
userFuture = tbClient.getUserService().getUser(tbClient.getAuthUser()!.userId!);
_loadUser();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: TbAppBar(
tbContext,
title: const Text('Profile'),
actions: [
IconButton(
icon: Icon(
Icons.check
),
onPressed: () {
_saveProfile();
}
),
if (widget._fullscreen) IconButton(
icon: Icon(
Icons.logout
@@ -45,22 +65,117 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
)
],
),
body: FutureBuilder<User>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
var user = snapshot.data!;
return ListTile(
title: Text('${user.email}'),
subtitle: Text('${user.firstName} ${user.lastName}'),
);
} else {
return Center(child: TbProgressIndicator(
size: 50.0,
));
}
},
body: Stack(
children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(16),
child: FormBuilder(
key: _profileFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
FormBuilderTextField(
name: 'email',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email *'
),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'firstName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'First Name'
),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'lastName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Last Name'
),
),
SizedBox(height: 24),
OutlinedButton(
style: OutlinedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(child: Text('Change Password'))
)
]
),
)
),
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
} else {
return SizedBox.shrink();
}
}
)
],
)
);
}
Future<void> _loadUser() async {
_isLoadingNotifier.value = true;
_currentUser = await tbClient.getUserService().getUser(tbClient.getAuthUser()!.userId!);
_setUser();
_isLoadingNotifier.value = false;
}
_setUser() {
_profileFormKey.currentState?.patchValue({
'email': _currentUser!.email,
'firstName': _currentUser!.firstName ?? '',
'lastName': _currentUser!.lastName ?? ''
});
}
Future<void> _saveProfile() async {
if (_currentUser != null) {
FocusScope.of(context).unfocus();
if (_profileFormKey.currentState?.saveAndValidate() ?? false) {
var formValue = _profileFormKey.currentState!.value;
_currentUser!.email = formValue['email'];
_currentUser!.firstName = formValue['firstName'];
_currentUser!.lastName = formValue['lastName'];
_isLoadingNotifier.value = true;
_currentUser = await tbClient.getUserService().saveUser(_currentUser!);
tbContext.userDetails = _currentUser;
_setUser();
await Future.delayed(Duration(milliseconds: 300));
_isLoadingNotifier.value = false;
showSuccessNotification('Profile successfully updated', duration: Duration(milliseconds: 1500));
}
}
}
_changePassword() async {
var res = await tbContext.showFullScreenDialog<bool>(new ChangePasswordPage(tbContext));
if (res == true) {
showSuccessNotification('Password successfully changed', duration: Duration(milliseconds: 1500));
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class TenantDetailsPage extends ContactBasedDetailsPage<Tenant> {
TenantDetailsPage(TbContext tbContext, String tenantId):
super(tbContext, entityId: tenantId, defaultTitle: 'Tenant', subTitle: 'Tenant details');
@override
Future<Tenant> fetchEntity(String tenantId) {
return tbClient.getTenantService().getTenant(tenantId);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'tenant_details_page.dart';
import 'tenants_page.dart';
class TenantRoutes extends TbRoutes {
late var tenantsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return TenantsPage(tbContext, searchMode: searchMode);
});
late var tenantDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return TenantDetailsPage(tbContext, params["id"][0]);
});
TenantRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/tenants", handler: tenantsHandler);
router.define("/tenant/:id", handler: tenantDetailsHandler);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin TenantsBase on EntitiesBase<Tenant, PageLink> {
@override
String get title => 'Tenants';
@override
String get noItemsFoundText => 'No tenants found';
@override
Future<PageData<Tenant>> fetchEntities(PageLink pageLink) {
return tbClient.getTenantService().getTenants(pageLink);
}
@override
void onEntityTap(Tenant tenant) {
navigateTo('/tenant/${tenant.id!.id}');
}
}

View File

@@ -0,0 +1,12 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/core/entity/entities_list.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'tenants_base.dart';
class TenantsList extends BaseEntitiesWidget<Tenant, PageLink> with TenantsBase, ContactBasedBase, EntitiesListStateBase {
TenantsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'tenants_list.dart';
class TenantsPage extends TbPageWidget<TenantsPage, _TenantsPageState> {
final bool searchMode;
TenantsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
@override
_TenantsPageState createState() => _TenantsPageState();
}
class _TenantsPageState extends TbPageState<TenantsPage, _TenantsPageState> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var tenantsList = TenantsList(tbContext, _pageLinkController, searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
tbContext,
onSearch: (searchText) => _pageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(tenantsList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: tenantsList
);
}
@override
void dispose() {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'tenants_list.dart';
class TenantsWidget extends TbContextWidget<TenantsWidget, _TenantsWidgetState> {
TenantsWidget(TbContext tbContext) : super(tbContext);
@override
_TenantsWidgetState createState() => _TenantsWidgetState();
}
class _TenantsWidgetState extends TbContextState<TenantsWidget, _TenantsWidgetState> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
return TenantsList(tbContext, _pageLinkController);
}
@override
void dispose() {
_pageLinkController.dispose();
super.dispose();
}
}