From 17ce15c98de52075c4e8ec2099102a3343b5c918 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 15 Jun 2021 16:38:37 +0300 Subject: [PATCH] Add Customers/Tenants pages. Improve login page. Implemented profile page, change and request password reset pages. --- lib/config/routes/router.dart | 4 + lib/core/auth/auth_routes.dart | 6 + lib/core/auth/login/login_page.dart | 390 ++++++++---------- .../auth/login/login_page_background.dart | 46 +++ .../login/reset_password_request_page.dart | 122 ++++++ lib/core/context/tb_context.dart | 11 +- lib/core/entity/entities_base.dart | 72 ++++ lib/core/entity/entity_details_page.dart | 115 +++++- lib/modules/asset/asset_details_page.dart | 24 +- lib/modules/asset/assets_base.dart | 102 +++-- .../audit_log/audit_log_details_page.dart | 3 +- lib/modules/audit_log/audit_logs_base.dart | 9 +- .../customer/customer_details_page.dart | 15 + lib/modules/customer/customer_routes.dart | 27 ++ lib/modules/customer/customers_base.dart | 22 + lib/modules/customer/customers_list.dart | 12 + lib/modules/customer/customers_page.dart | 59 +++ lib/modules/dashboard/dashboard.dart | 6 +- lib/modules/home/home_page.dart | 47 +-- lib/modules/main/main_page.dart | 8 +- lib/modules/more/more_page.dart | 5 +- lib/modules/profile/change_password_page.dart | 172 ++++++++ lib/modules/profile/profile_page.dart | 149 ++++++- lib/modules/tenant/tenant_details_page.dart | 15 + lib/modules/tenant/tenant_routes.dart | 27 ++ lib/modules/tenant/tenants_base.dart | 22 + lib/modules/tenant/tenants_list.dart | 12 + lib/modules/tenant/tenants_page.dart | 60 +++ lib/modules/tenant/tenants_widget.dart | 32 ++ lib/utils/utils.dart | 18 + lib/widgets/tb_app_bar.dart | 9 +- pubspec.lock | 16 +- pubspec.yaml | 1 + 33 files changed, 1312 insertions(+), 326 deletions(-) create mode 100644 lib/core/auth/login/login_page_background.dart create mode 100644 lib/core/auth/login/reset_password_request_page.dart create mode 100644 lib/modules/customer/customer_details_page.dart create mode 100644 lib/modules/customer/customer_routes.dart create mode 100644 lib/modules/customer/customers_base.dart create mode 100644 lib/modules/customer/customers_list.dart create mode 100644 lib/modules/customer/customers_page.dart create mode 100644 lib/modules/profile/change_password_page.dart create mode 100644 lib/modules/tenant/tenant_details_page.dart create mode 100644 lib/modules/tenant/tenant_routes.dart create mode 100644 lib/modules/tenant/tenants_base.dart create mode 100644 lib/modules/tenant/tenants_list.dart create mode 100644 lib/modules/tenant/tenants_page.dart create mode 100644 lib/modules/tenant/tenants_widget.dart diff --git a/lib/config/routes/router.dart b/lib/config/routes/router.dart index 01026a9..38d22f7 100644 --- a/lib/config/routes/router.dart +++ b/lib/config/routes/router.dart @@ -7,10 +7,12 @@ import 'package:thingsboard_app/core/init/init_routes.dart'; import 'package:thingsboard_app/modules/alarm/alarm_routes.dart'; import 'package:thingsboard_app/modules/asset/asset_routes.dart'; import 'package:thingsboard_app/modules/audit_log/audit_logs_routes.dart'; +import 'package:thingsboard_app/modules/customer/customer_routes.dart'; import 'package:thingsboard_app/modules/dashboard/dashboard_routes.dart'; import 'package:thingsboard_app/modules/device/device_routes.dart'; import 'package:thingsboard_app/modules/home/home_routes.dart'; import 'package:thingsboard_app/modules/profile/profile_routes.dart'; +import 'package:thingsboard_app/modules/tenant/tenant_routes.dart'; import 'package:thingsboard_app/utils/ui_utils_routes.dart'; class ThingsboardAppRouter { @@ -39,6 +41,8 @@ class ThingsboardAppRouter { AlarmRoutes(_tbContext).registerRoutes(); DashboardRoutes(_tbContext).registerRoutes(); AuditLogsRoutes(_tbContext).registerRoutes(); + CustomerRoutes(_tbContext).registerRoutes(); + TenantRoutes(_tbContext).registerRoutes(); } TbContext get tbContext => _tbContext; diff --git a/lib/core/auth/auth_routes.dart b/lib/core/auth/auth_routes.dart index 0111f47..ad7ff4d 100644 --- a/lib/core/auth/auth_routes.dart +++ b/lib/core/auth/auth_routes.dart @@ -2,6 +2,7 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/config/routes/router.dart'; +import 'package:thingsboard_app/core/auth/login/reset_password_request_page.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'login/login_page.dart'; @@ -12,11 +13,16 @@ class AuthRoutes extends TbRoutes { return LoginPage(tbContext); }); + late var resetPasswordRequestHandler = Handler(handlerFunc: (BuildContext? context, Map params) { + return ResetPasswordRequestPage(tbContext); + }); + AuthRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { router.define("/login", handler: loginHandler); + router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler); } } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index ba5f96f..753f4a0 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:thingsboard_app/constants/assets_path.dart'; @@ -11,6 +12,8 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; +import 'login_page_background.dart'; + class LoginPage extends TbPageWidget { LoginPage(TbContext tbContext) : super(tbContext); @@ -33,8 +36,7 @@ class _LoginPageState extends TbPageState { final _isLoginNotifier = ValueNotifier(false); final _showPasswordNotifier = ValueNotifier(false); - final usernameController = TextEditingController(); - final passwordController = TextEditingController(); + final _loginFormKey = GlobalKey(); @override void initState() { @@ -43,8 +45,6 @@ class _LoginPageState extends TbPageState { @override void dispose() { - usernameController.dispose(); - passwordController.dispose(); super.dispose(); } @@ -53,183 +53,164 @@ class _LoginPageState extends TbPageState { return Scaffold( backgroundColor: Colors.white, resizeToAvoidBottomInset: false, - body: ValueListenableBuilder( - valueListenable: _isLoginNotifier, - builder: (BuildContext context, bool loading, child) { - List children = [ - LoginPageBackground(), - Positioned.fill( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - padding: EdgeInsets.fromLTRB(28, 71, 28, 28), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 28)), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, - height: 25, - color: Theme.of(context).primaryColor, - semanticsLabel: 'ThingsBoard Logo') - ] - ), - Container(height: 32), - Row( - children: [ - Text( - 'Login to your account', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 28, - height: 36 / 28 - ) - )] - ), - Container(height: 48), - if (tbContext.hasOAuthClients) - _buildOAuth2Buttons(tbContext.oauth2ClientInfos!), - if (tbContext.hasOAuthClients) - Padding(padding: EdgeInsets.only(top: 10, bottom: 16), - child: Row( - children: [ - Flexible(child: Divider()), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('OR'), - ), - Flexible(child: Divider()) - ], - ) - ), - TextField( - enabled: !loading, - controller: usernameController, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Email', - hintText: 'Enter valid email id as abc@gmail.com'), - ), - Container(height: 28), - ValueListenableBuilder( - valueListenable: _showPasswordNotifier, - builder: (BuildContext context, bool showPassword, child) { - return TextField( - enabled: !loading, - controller: passwordController, - obscureText: !showPassword, - decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off), - onPressed: loading ? null : () { - _showPasswordNotifier.value = !_showPasswordNotifier.value; - }, - ), - border: OutlineInputBorder(), - labelText: 'Password', - hintText: 'Enter secure password'), - ); - } - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: loading ? null : () { - //TODO FORGOT PASSWORD SCREEN GOES HERE - }, - child: Text( - 'Forgot Password?', - style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, - letterSpacing: 1, - fontSize: 12, - height: 16 / 12), - ), - ) - ], - ), - Spacer(), - ElevatedButton( - child: Text('Log In'), - style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), - onPressed: loading ? null : () async { - _isLoginNotifier.value = true; - try { - await tbClient.login( - LoginRequest(usernameController.text, - passwordController.text)); - } catch (e) { - _isLoginNotifier.value = false; - } - }, - ), - Container( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('New User?', - style: TextStyle( - fontSize: 14, - height: 14 / 20 - )), - TextButton( - onPressed: loading ? null : () { - //TODO CREATE ACCOUNT SCREEN GOES HERE - }, - child: Text( - 'Create Account', - style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, - letterSpacing: 1, - fontSize: 14, - height: 14 / 20), - ), - ) - ], - ) - ] - ), - ) - ) - ); - }, - ) - - - ) - ]; - if (loading) { - var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); - var bottomPadding = data.padding.top; - bottomPadding += kToolbarHeight; - children.add( - SizedBox.expand( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - decoration: new BoxDecoration( - color: Colors.grey.shade200.withOpacity(0.2) + body: Stack( + children: [ + LoginPageBackground(), + Positioned.fill( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: EdgeInsets.fromLTRB(24, 71, 24, 24), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 24)), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, + height: 25, + color: Theme.of(context).primaryColor, + semanticsLabel: 'ThingsBoard Logo') + ] ), - child: Container( - padding: EdgeInsets.only(bottom: bottomPadding), - alignment: Alignment.center, - child: TbProgressIndicator(size: 50.0), - ), - ) + SizedBox(height: 32), + Row( + children: [ + Text( + 'Login to your account', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 28, + height: 36 / 28 + ) + )] + ), + SizedBox(height: 48), + if (tbContext.hasOAuthClients) + _buildOAuth2Buttons(tbContext.oauth2ClientInfos!), + if (tbContext.hasOAuthClients) + Padding(padding: EdgeInsets.only(top: 10, bottom: 16), + child: Row( + children: [ + Flexible(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text('OR'), + ), + Flexible(child: Divider()) + ], + ) + ), + FormBuilder( + key: _loginFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormBuilderTextField( + name: 'username', + 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: 28), + ValueListenableBuilder( + valueListenable: _showPasswordNotifier, + builder: (BuildContext context, bool showPassword, child) { + return FormBuilderTextField( + name: 'password', + obscureText: !showPassword, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(context, errorText: 'Password is required.') + ]), + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off), + onPressed: () { + _showPasswordNotifier.value = !_showPasswordNotifier.value; + }, + ), + border: OutlineInputBorder(), + labelText: 'Password' + ), + ); + } + ) + ], + ) + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + _forgotPassword(); + }, + child: Text( + 'Forgot Password?', + style: TextStyle(color: Theme.of(context).colorScheme.primary, + letterSpacing: 1, + fontSize: 12, + height: 16 / 12), + ), + ) + ], + ), + Spacer(), + ElevatedButton( + child: Text('Log In'), + style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), + onPressed: () { + _login(); + }, + ), + SizedBox(height: 48) + ] + ), ) ) - ) - ); + ); + }, + ) + ), + ValueListenableBuilder( + valueListenable: _isLoginNotifier, + builder: (BuildContext context, bool loading, child) { + if (loading) { + var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); + var bottomPadding = data.padding.top; + bottomPadding += kToolbarHeight; + return SizedBox.expand( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + decoration: new BoxDecoration( + color: Colors.grey.shade200.withOpacity(0.2) + ), + child: Container( + padding: EdgeInsets.only(bottom: bottomPadding), + alignment: Alignment.center, + child: TbProgressIndicator(size: 50.0), + ), + ) + ) + ) + ); + } else { + return SizedBox.shrink(); + } } - return Stack( - children: children, - ); - }) + ) + ] + ) ); } @@ -321,48 +302,23 @@ class _LoginPageState extends TbPageState { _isLoginNotifier.value = false; } } -} -class LoginPageBackground extends StatelessWidget { - - @override - Widget build(BuildContext context) { - return SizedBox.expand( - child: CustomPaint( - painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor), - ) - ); + void _login() async { + FocusScope.of(context).unfocus(); + if (_loginFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _loginFormKey.currentState!.value; + String username = formValue['username']; + String password = formValue['password']; + _isLoginNotifier.value = true; + try { + await tbClient.login(LoginRequest(username, password)); + } catch (e) { + _isLoginNotifier.value = false; + } + } } -} - -class _LoginPageBackgroundPainter extends CustomPainter { - - final Color color; - - const _LoginPageBackgroundPainter({required this.color}); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color.withAlpha(14); - paint.style = PaintingStyle.fill; - var topPath = Path(); - topPath.moveTo(0, 0); - topPath.lineTo(size.width / 2, 0); - topPath.lineTo(0, size.height / 10); - topPath.close(); - canvas.drawPath(topPath, paint); - var bottomPath = Path(); - bottomPath.moveTo(0, size.height * 0.98); - bottomPath.lineTo(size.width, size.height * 0.78); - bottomPath.lineTo(size.width, size.height); - bottomPath.lineTo(0, size.height); - bottomPath.close(); - canvas.drawPath(bottomPath, paint); - } - - @override - bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) { - return color != oldDelegate.color; + void _forgotPassword() async { + navigateTo('/login/resetPasswordRequest'); } } diff --git a/lib/core/auth/login/login_page_background.dart b/lib/core/auth/login/login_page_background.dart new file mode 100644 index 0000000..e86c7ea --- /dev/null +++ b/lib/core/auth/login/login_page_background.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class LoginPageBackground extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: CustomPaint( + painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor), + ) + ); + } + +} + +class _LoginPageBackgroundPainter extends CustomPainter { + + final Color color; + + const _LoginPageBackgroundPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color.withAlpha(14); + paint.style = PaintingStyle.fill; + var topPath = Path(); + topPath.moveTo(0, 0); + topPath.lineTo(size.width / 2, 0); + topPath.lineTo(0, size.height / 10); + topPath.close(); + canvas.drawPath(topPath, paint); + var bottomPath = Path(); + bottomPath.moveTo(0, size.height * 0.98); + bottomPath.lineTo(size.width, size.height * 0.78); + bottomPath.lineTo(size.width, size.height); + bottomPath.lineTo(0, size.height); + bottomPath.close(); + canvas.drawPath(bottomPath, paint); + } + + @override + bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) { + return color != oldDelegate.color; + } +} diff --git a/lib/core/auth/login/reset_password_request_page.dart b/lib/core/auth/login/reset_password_request_page.dart new file mode 100644 index 0000000..a8c14bf --- /dev/null +++ b/lib/core/auth/login/reset_password_request_page.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:thingsboard_app/core/auth/login/login_page_background.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 ResetPasswordRequestPage extends TbPageWidget { + + ResetPasswordRequestPage(TbContext tbContext) : super(tbContext); + + @override + _ResetPasswordRequestPageState createState() => _ResetPasswordRequestPageState(); + +} + +class _ResetPasswordRequestPageState extends TbPageState { + + final _isLoadingNotifier = ValueNotifier(false); + + final _resetPasswordFormKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( children: [ + LoginPageBackground(), + SizedBox.expand( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: TbAppBar( + tbContext, + title: Text('Reset password'), + ), + body: Stack( + children: [ + SizedBox.expand( + child: Padding( + padding: EdgeInsets.all(24), + child: FormBuilder( + key: _resetPasswordFormKey, + autovalidateMode: AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 16), + Text('Enter the email associated with your account and we\'ll send an email with password reset link', + textAlign: TextAlign.center, + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 14, + height: 24 / 14 + ), + ), + SizedBox(height: 61), + FormBuilderTextField( + name: 'email', + autofocus: true, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(context, errorText: 'Email is required.'), + FormBuilderValidators.email(context, errorText: 'Invalid email format.') + ]), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Email *' + ), + ), + Spacer(), + ElevatedButton( + child: Text('Request password reset'), + style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), + onPressed: () { + _requestPasswordReset(); + }, + ) + ] + ) + ) + ) + ), + ValueListenableBuilder( + 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(); + } + } + ) + ] + ) + ) + ) + ]) + ); + } + + void _requestPasswordReset() async { + FocusScope.of(context).unfocus(); + if (_resetPasswordFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _resetPasswordFormKey.currentState!.value; + String email = formValue['email']; + _isLoadingNotifier.value = true; + try { + await Future.delayed(Duration(milliseconds: 300)); + await tbClient.sendResetPasswordLink(email); + _isLoadingNotifier.value = false; + showSuccessNotification('Password reset link was successfully sent!'); + } catch(e) { + _isLoadingNotifier.value = false; + } + } + } +} diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index d8aec33..335c820 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -371,7 +371,7 @@ class TbContext { replace = true; clearStack = true; } - if (isOpenedDashboard) { + if (transition != TransitionType.nativeModal && isOpenedDashboard) { transition = TransitionType.none; } else if (transition == null) { if (replace) { @@ -392,6 +392,15 @@ class TbContext { await _mainDashboardHolder?.navigateToDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar, animate: animate); } + Future showFullScreenDialog(Widget dialog) { + return Navigator.of(currentState!.context).push(new MaterialPageRoute( + builder: (BuildContext context) { + return dialog; + }, + fullscreenDialog: true + )); + } + void pop([T? result, BuildContext? context]) { var targetContext = context ?? currentState?.context; if (targetContext != null) { diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 723c41c..44347e1 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -6,6 +6,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:intl/intl.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/utils/utils.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; const Map entityTypeTranslations = { @@ -75,6 +76,77 @@ mixin EntitiesBase on HasTbContext { } +mixin ContactBasedBase on EntitiesBase { + + @override + Widget buildEntityListCard(BuildContext context, T contact) { + var address = Utils.contactToShortAddress(contact); + return Container( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 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('${contact.getName()}', + style: TextStyle( + color: Color(0xFF282828), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14 + )) + ), + Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(contact.createdTime!)), + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )) + ] + ), + SizedBox(height: 4), + if (contact.email != null) Text(contact.email!, + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )), + if (contact.email == null) + SizedBox(height: 16), + if (address != null) SizedBox(height: 4), + if (address != null) Text(address, + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )), + ], + ) + ), + SizedBox(width: 16), + Icon(Icons.chevron_right, color: Color(0xFFACACAC)), + SizedBox(width: 8) + ], + ), + ); + } +} + abstract class PageKeyController

extends ValueNotifier> { PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey)); diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart index ea1193c..f8fd422 100644 --- a/lib/core/entity/entity_details_page.dart +++ b/lib/core/entity/entity_details_page.dart @@ -9,8 +9,21 @@ import 'package:thingsboard_client/thingsboard_client.dart'; abstract class EntityDetailsPage extends TbPageWidget, _EntityDetailsPageState> { + final labelTextStyle = TextStyle( + color: Color(0xFF757575), + fontSize: 14, + height: 20 / 14 + ); + + final valueTextStyle = TextStyle( + color: Color(0xFF282828), + fontSize: 14, + height: 20 / 14 + ); + final String _defaultTitle; final String _entityId; + final String? _subTitle; final bool _showLoadingIndicator; final bool _hideAppBar; final double? _appBarElevation; @@ -18,11 +31,13 @@ abstract class EntityDetailsPage extends TbPageWidget extends TbPageState( valueListenable: titleValue, - builder: (context, title, widget) { - return FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.centerLeft, - child: Text(title) + builder: (context, title, _widget) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title, + style: widget._subTitle != null ? Theme.of(context).primaryTextTheme.headline6!.copyWith( + fontSize: 16 + ) : null + ) + ), + if (widget._subTitle != null) Text(widget._subTitle!, style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )) + ] ); }, ), @@ -98,3 +129,77 @@ class _EntityDetailsPageState extends TbPageState extends EntityDetailsPage { + + ContactBasedDetailsPage(TbContext tbContext, + { required String defaultTitle, + required String entityId, + String? subTitle, + bool showLoadingIndicator = true, + bool hideAppBar = false, + double? appBarElevation}): + super(tbContext, defaultTitle: defaultTitle, entityId: entityId, + subTitle: subTitle, showLoadingIndicator: showLoadingIndicator, + hideAppBar: hideAppBar, appBarElevation: appBarElevation); + + @override + Widget buildEntityDetails(BuildContext context, T contact) { + return Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('Title', style: labelTextStyle), + Text(contact.getName(), style: valueTextStyle), + SizedBox(height: 16), + Text('Country', style: labelTextStyle), + Text(contact.country ?? '', style: valueTextStyle), + SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('City', style: labelTextStyle), + Text(contact.city ?? '', style: valueTextStyle), + ], + )), + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text('State / Province', style: labelTextStyle), + Text(contact.state ?? '', style: valueTextStyle), + ], + )), + ], + ), + SizedBox(height: 16), + Text('Zip / Postal Code', style: labelTextStyle), + Text(contact.zip ?? '', style: valueTextStyle), + SizedBox(height: 16), + Text('Address', style: labelTextStyle), + Text(contact.address ?? '', style: valueTextStyle), + SizedBox(height: 16), + Text('Address 2', style: labelTextStyle), + Text(contact.address2 ?? '', style: valueTextStyle), + SizedBox(height: 16), + Text('Phone', style: labelTextStyle), + Text(contact.phone ?? '', style: valueTextStyle), + SizedBox(height: 16), + Text('Email', style: labelTextStyle), + Text(contact.email ?? '', style: valueTextStyle), + ] + ) + ); + } +} + diff --git a/lib/modules/asset/asset_details_page.dart b/lib/modules/asset/asset_details_page.dart index 8ed88a3..768a828 100644 --- a/lib/modules/asset/asset_details_page.dart +++ b/lib/modules/asset/asset_details_page.dart @@ -9,7 +9,7 @@ class AssetDetailsPage extends EntityDetailsPage { AssetDetailsPage(TbContext tbContext, String assetId): super(tbContext, entityId: assetId, - defaultTitle: 'Asset'); + defaultTitle: 'Asset', subTitle: 'Asset details'); @override Future fetchEntity(String assetId) { @@ -18,9 +18,25 @@ class AssetDetailsPage extends EntityDetailsPage { @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), + ] + ) ); } diff --git a/lib/modules/asset/assets_base.dart b/lib/modules/asset/assets_base.dart index 4596c1b..0bdcb69 100644 --- a/lib/modules/asset/assets_base.dart +++ b/lib/modules/asset/assets_base.dart @@ -27,12 +27,12 @@ mixin AssetsBase on EntitiesBase { @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 { 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 { )) ], ) - ), - (!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()) - ], - ), + ) + ] + ) ) - ) ] ); diff --git a/lib/modules/audit_log/audit_log_details_page.dart b/lib/modules/audit_log/audit_log_details_page.dart index 39d40de..3bc43f7 100644 --- a/lib/modules/audit_log/audit_log_details_page.dart +++ b/lib/modules/audit_log/audit_log_details_page.dart @@ -44,7 +44,8 @@ class _AuditLogDetailsPageState extends TbContextState( - builder: (BuildContext context) { - return new AuditLogDetailsPage(tbContext, auditLog); - }, - fullscreenDialog: true - )); + tbContext.showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog)); } } diff --git a/lib/modules/customer/customer_details_page.dart b/lib/modules/customer/customer_details_page.dart new file mode 100644 index 0000000..8f30bb6 --- /dev/null +++ b/lib/modules/customer/customer_details_page.dart @@ -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 { + + CustomerDetailsPage(TbContext tbContext, String customerId): + super(tbContext, entityId: customerId, defaultTitle: 'Customer', subTitle: 'Customer details'); + + @override + Future fetchEntity(String customerId) { + return tbClient.getCustomerService().getCustomer(customerId); + } + +} diff --git a/lib/modules/customer/customer_routes.dart b/lib/modules/customer/customer_routes.dart new file mode 100644 index 0000000..edd5c0a --- /dev/null +++ b/lib/modules/customer/customer_routes.dart @@ -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 params) { + var searchMode = params['search']?.first == 'true'; + return CustomersPage(tbContext, searchMode: searchMode); + }); + + late var customerDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map 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); + } + +} diff --git a/lib/modules/customer/customers_base.dart b/lib/modules/customer/customers_base.dart new file mode 100644 index 0000000..e504264 --- /dev/null +++ b/lib/modules/customer/customers_base.dart @@ -0,0 +1,22 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +mixin CustomersBase on EntitiesBase { + + @override + String get title => 'Customers'; + + @override + String get noItemsFoundText => 'No customers found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return tbClient.getCustomerService().getCustomers(pageLink); + } + + @override + void onEntityTap(Customer customer) { + navigateTo('/customer/${customer.id!.id}'); + } + +} diff --git a/lib/modules/customer/customers_list.dart b/lib/modules/customer/customers_list.dart new file mode 100644 index 0000000..151d4b1 --- /dev/null +++ b/lib/modules/customer/customers_list.dart @@ -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 with CustomersBase, ContactBasedBase, EntitiesListStateBase { + + CustomersList(TbContext tbContext, PageKeyController pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode); + +} diff --git a/lib/modules/customer/customers_page.dart b/lib/modules/customer/customers_page.dart new file mode 100644 index 0000000..6b6402c --- /dev/null +++ b/lib/modules/customer/customers_page.dart @@ -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 { + + final bool searchMode; + + CustomersPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); + + @override + _CustomersPageState createState() => _CustomersPageState(); + +} + +class _CustomersPageState extends TbPageState { + + 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(); + } + +} diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index a310c76..629f012 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -224,7 +224,11 @@ class _DashboardState extends TbContextState { if (widget._home == true && !tbContext.isHomePage()) { return true; } - return await _goBack(); + if (readyState.value) { + return await _goBack(); + } else { + return true; + } }, child: ValueListenableBuilder( diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index aa0716a..b508763 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -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 { class _HomePageState extends TbContextState with AutomaticKeepAliveClientMixin { - final EntitiesListWidgetController _entitiesWidgetController = EntitiesListWidgetController(); - @override void initState() { super.initState(); @@ -35,7 +33,6 @@ class _HomePageState extends TbContextState with Autom @override void dispose() { - _entitiesWidgetController.dispose(); super.dispose(); } @@ -56,6 +53,16 @@ class _HomePageState extends TbContextState 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 with Autom } } -/* List _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 _buildTenantAdminHome(BuildContext context) { - return [ - AssetsListWidget(tbContext, controller: _entitiesWidgetController), - DevicesListWidget(tbContext, controller: _entitiesWidgetController), - DashboardsListWidget(tbContext, controller: _entitiesWidgetController) - ]; - } - - List _buildCustomerUserHome(BuildContext context) { - return [ - AssetsListWidget(tbContext, controller: _entitiesWidgetController), - DevicesListWidget(tbContext, controller: _entitiesWidgetController), - DashboardsListWidget(tbContext, controller: _entitiesWidgetController) - ]; - } */ } class HomeDashboard extends TbContextWidget { diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index 4cb38ed..ae15f43 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -23,7 +23,7 @@ class TbMainNavigationItem { }); static Map> 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: diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart index e1710a3..2c021f1 100644 --- a/lib/modules/more/more_page.dart +++ b/lib/modules/more/more_page.dart @@ -29,7 +29,10 @@ class _MorePageState extends TbContextState { 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), diff --git a/lib/modules/profile/change_password_page.dart b/lib/modules/profile/change_password_page.dart new file mode 100644 index 0000000..1c7c3cf --- /dev/null +++ b/lib/modules/profile/change_password_page.dart @@ -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(TbContext tbContext) : super(tbContext); + + @override + _ChangePasswordPageState createState() => _ChangePasswordPageState(); + +} + +class _ChangePasswordPageState extends TbContextState { + + final _isLoadingNotifier = ValueNotifier(false); + + final _showCurrentPasswordNotifier = ValueNotifier(false); + final _showNewPasswordNotifier = ValueNotifier(false); + final _showNewPassword2Notifier = ValueNotifier(false); + + final _changePasswordFormKey = GlobalKey(); + + @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( + 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 _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; + } + } + } + } + +} diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index df5f845..cbdd2a7 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -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 { class _ProfilePageState extends TbPageState { - late Future userFuture; + final _isLoadingNotifier = ValueNotifier(true); + + final _profileFormKey = GlobalKey(); + + 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 { ) ], ), - body: FutureBuilder( - 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( + 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 _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 _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(new ChangePasswordPage(tbContext)); + if (res == true) { + showSuccessNotification('Password successfully changed', duration: Duration(milliseconds: 1500)); + } + } } diff --git a/lib/modules/tenant/tenant_details_page.dart b/lib/modules/tenant/tenant_details_page.dart new file mode 100644 index 0000000..90723c5 --- /dev/null +++ b/lib/modules/tenant/tenant_details_page.dart @@ -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 { + + TenantDetailsPage(TbContext tbContext, String tenantId): + super(tbContext, entityId: tenantId, defaultTitle: 'Tenant', subTitle: 'Tenant details'); + + @override + Future fetchEntity(String tenantId) { + return tbClient.getTenantService().getTenant(tenantId); + } + +} diff --git a/lib/modules/tenant/tenant_routes.dart b/lib/modules/tenant/tenant_routes.dart new file mode 100644 index 0000000..02c3919 --- /dev/null +++ b/lib/modules/tenant/tenant_routes.dart @@ -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 params) { + var searchMode = params['search']?.first == 'true'; + return TenantsPage(tbContext, searchMode: searchMode); + }); + + late var tenantDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map 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); + } + +} diff --git a/lib/modules/tenant/tenants_base.dart b/lib/modules/tenant/tenants_base.dart new file mode 100644 index 0000000..4ecb472 --- /dev/null +++ b/lib/modules/tenant/tenants_base.dart @@ -0,0 +1,22 @@ +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +mixin TenantsBase on EntitiesBase { + + @override + String get title => 'Tenants'; + + @override + String get noItemsFoundText => 'No tenants found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return tbClient.getTenantService().getTenants(pageLink); + } + + @override + void onEntityTap(Tenant tenant) { + navigateTo('/tenant/${tenant.id!.id}'); + } + +} diff --git a/lib/modules/tenant/tenants_list.dart b/lib/modules/tenant/tenants_list.dart new file mode 100644 index 0000000..42265e6 --- /dev/null +++ b/lib/modules/tenant/tenants_list.dart @@ -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 with TenantsBase, ContactBasedBase, EntitiesListStateBase { + + TenantsList(TbContext tbContext, PageKeyController pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode); + +} diff --git a/lib/modules/tenant/tenants_page.dart b/lib/modules/tenant/tenants_page.dart new file mode 100644 index 0000000..4d84341 --- /dev/null +++ b/lib/modules/tenant/tenants_page.dart @@ -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 { + + final bool searchMode; + + TenantsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); + + @override + _TenantsPageState createState() => _TenantsPageState(); + +} + +class _TenantsPageState extends TbPageState { + + 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(); + } + +} diff --git a/lib/modules/tenant/tenants_widget.dart b/lib/modules/tenant/tenants_widget.dart new file mode 100644 index 0000000..bd936bf --- /dev/null +++ b/lib/modules/tenant/tenants_widget.dart @@ -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(TbContext tbContext) : super(tbContext); + + @override + _TenantsWidgetState createState() => _TenantsWidgetState(); + +} + +class _TenantsWidgetState extends TbContextState { + + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + return TenantsList(tbContext, _pageLinkController); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } + +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index d3fb4e6..b0d56f6 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -27,4 +27,22 @@ abstract class Utils { ); } + static String? contactToShortAddress(ContactBased contact) { + var addressParts = []; + if (contact.country != null) { + addressParts.add(contact.country!); + } + if (contact.city != null) { + addressParts.add(contact.city!); + } + if (contact.address != null) { + addressParts.add(contact.address!); + } + if (addressParts.isNotEmpty) { + return addressParts.join(', '); + } else { + return null; + } + } + } diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart index 4f1668c..9e88640 100644 --- a/lib/widgets/tb_app_bar.dart +++ b/lib/widgets/tb_app_bar.dart @@ -68,7 +68,7 @@ class _TbAppBarState extends TbContextState { leading: widget.leading, title: widget.title, actions: widget.actions, - elevation: widget.elevation, + elevation: widget.elevation ?? 8, shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150), ); } @@ -77,6 +77,7 @@ class _TbAppBarState extends TbContextState { class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { final double? elevation; + final Color? shadowColor; final bool showLoadingIndicator; final String? searchHint; final void Function(String searchText)? onSearch; @@ -84,8 +85,8 @@ class TbAppSearchBar extends TbContextWidget