Add Customers/Tenants pages. Improve login page. Implemented profile page, change and request password reset pages.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<String, dynamic> params) {
|
||||
return ResetPasswordRequestPage(tbContext);
|
||||
});
|
||||
|
||||
AuthRoutes(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
void doRegisterRoutes(router) {
|
||||
router.define("/login", handler: loginHandler);
|
||||
router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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, _LoginPageState> {
|
||||
|
||||
LoginPage(TbContext tbContext) : super(tbContext);
|
||||
@@ -33,8 +36,7 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
|
||||
final _isLoginNotifier = ValueNotifier<bool>(false);
|
||||
final _showPasswordNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final _loginFormKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -43,8 +45,6 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
usernameController.dispose();
|
||||
passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -53,183 +53,164 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: ValueListenableBuilder<bool>(
|
||||
valueListenable: _isLoginNotifier,
|
||||
builder: (BuildContext context, bool loading, child) {
|
||||
List<Widget> 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<bool>(
|
||||
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<LoginPage, _LoginPageState> {
|
||||
_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');
|
||||
}
|
||||
}
|
||||
|
||||
46
lib/core/auth/login/login_page_background.dart
Normal file
46
lib/core/auth/login/login_page_background.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
122
lib/core/auth/login/reset_password_request_page.dart
Normal file
122
lib/core/auth/login/reset_password_request_page.dart
Normal file
@@ -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, _ResetPasswordRequestPageState> {
|
||||
|
||||
ResetPasswordRequestPage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_ResetPasswordRequestPageState createState() => _ResetPasswordRequestPageState();
|
||||
|
||||
}
|
||||
|
||||
class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPage, _ResetPasswordRequestPageState> {
|
||||
|
||||
final _isLoadingNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
final _resetPasswordFormKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@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<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();
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T?> showFullScreenDialog<T>(Widget dialog) {
|
||||
return Navigator.of(currentState!.context).push<T>(new MaterialPageRoute<T>(
|
||||
builder: (BuildContext context) {
|
||||
return dialog;
|
||||
},
|
||||
fullscreenDialog: true
|
||||
));
|
||||
}
|
||||
|
||||
void pop<T>([T? result, BuildContext? context]) {
|
||||
var targetContext = context ?? currentState?.context;
|
||||
if (targetContext != null) {
|
||||
|
||||
@@ -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<EntityType, String> entityTypeTranslations = {
|
||||
@@ -75,6 +76,77 @@ mixin EntitiesBase<T, P> on HasTbContext {
|
||||
|
||||
}
|
||||
|
||||
mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
|
||||
|
||||
@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<P> extends ValueNotifier<PageKeyValue<P>> {
|
||||
|
||||
PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey));
|
||||
|
||||
@@ -9,8 +9,21 @@ import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget<EntityDetailsPage<T>, _EntityDetailsPageState<T>> {
|
||||
|
||||
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<T extends BaseData> extends TbPageWidget<Entity
|
||||
EntityDetailsPage(TbContext tbContext,
|
||||
{required String defaultTitle,
|
||||
required String entityId,
|
||||
String? subTitle,
|
||||
bool showLoadingIndicator = true,
|
||||
bool hideAppBar = false,
|
||||
double? appBarElevation}):
|
||||
this._defaultTitle = defaultTitle,
|
||||
this._entityId = entityId,
|
||||
this._subTitle = subTitle,
|
||||
this._showLoadingIndicator = showLoadingIndicator,
|
||||
this._hideAppBar = hideAppBar,
|
||||
this._appBarElevation = appBarElevation,
|
||||
@@ -66,17 +81,33 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: widget._hideAppBar ? null : TbAppBar(
|
||||
tbContext,
|
||||
showLoadingIndicator: widget._showLoadingIndicator,
|
||||
elevation: widget._appBarElevation,
|
||||
title: ValueListenableBuilder<String>(
|
||||
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<T extends BaseData> extends TbPageState<EntityDeta
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDetailsPage<T> {
|
||||
|
||||
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),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
15
lib/modules/customer/customer_details_page.dart
Normal file
15
lib/modules/customer/customer_details_page.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
27
lib/modules/customer/customer_routes.dart
Normal file
27
lib/modules/customer/customer_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
22
lib/modules/customer/customers_base.dart
Normal file
22
lib/modules/customer/customers_base.dart
Normal 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}');
|
||||
}
|
||||
|
||||
}
|
||||
12
lib/modules/customer/customers_list.dart
Normal file
12
lib/modules/customer/customers_list.dart
Normal 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);
|
||||
|
||||
}
|
||||
59
lib/modules/customer/customers_page.dart
Normal file
59
lib/modules/customer/customers_page.dart
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
172
lib/modules/profile/change_password_page.dart
Normal file
172
lib/modules/profile/change_password_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
lib/modules/tenant/tenant_details_page.dart
Normal file
15
lib/modules/tenant/tenant_details_page.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
27
lib/modules/tenant/tenant_routes.dart
Normal file
27
lib/modules/tenant/tenant_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
22
lib/modules/tenant/tenants_base.dart
Normal file
22
lib/modules/tenant/tenants_base.dart
Normal 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}');
|
||||
}
|
||||
|
||||
}
|
||||
12
lib/modules/tenant/tenants_list.dart
Normal file
12
lib/modules/tenant/tenants_list.dart
Normal 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);
|
||||
|
||||
}
|
||||
60
lib/modules/tenant/tenants_page.dart
Normal file
60
lib/modules/tenant/tenants_page.dart
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
32
lib/modules/tenant/tenants_widget.dart
Normal file
32
lib/modules/tenant/tenants_widget.dart
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,4 +27,22 @@ abstract class Utils {
|
||||
);
|
||||
}
|
||||
|
||||
static String? contactToShortAddress(ContactBased contact) {
|
||||
var addressParts = <String>[];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
|
||||
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<TbAppBar, _TbAppBarState> {
|
||||
class TbAppSearchBar extends TbContextWidget<TbAppSearchBar, _TbAppSearchBarState> 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<TbAppSearchBar, _TbAppSearchBarStat
|
||||
@override
|
||||
final Size preferredSize;
|
||||
|
||||
TbAppSearchBar(TbContext tbContext, {this.elevation,
|
||||
this.showLoadingIndicator = false, this.searchHint, this.onSearch}) :
|
||||
TbAppSearchBar(TbContext tbContext, {this.elevation = 8,
|
||||
this.shadowColor, this.showLoadingIndicator = false, this.searchHint, this.onSearch}) :
|
||||
preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
|
||||
super(tbContext);
|
||||
|
||||
@@ -140,6 +141,8 @@ class _TbAppSearchBarState extends TbContextState<TbAppSearchBar, _TbAppSearchBa
|
||||
AppBar buildSearchBar() {
|
||||
return AppBar(
|
||||
centerTitle: true,
|
||||
elevation: widget.elevation ?? 8,
|
||||
shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150),
|
||||
title: TextField(
|
||||
controller: _filter,
|
||||
autofocus: true,
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -91,7 +91,7 @@ packages:
|
||||
name: dart_jsonwebtoken
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.3.0"
|
||||
device_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -139,6 +139,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_form_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_form_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -153,6 +160,11 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -453,7 +465,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "6f8b0eda9890443c635536c5e70051b29233ce16"
|
||||
resolved-ref: c2a264d646ebad8ade84abe6c38b78001bd34ed7
|
||||
url: "git@github.com:thingsboard/dart_thingsboard_client.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies:
|
||||
package_info: ^2.0.2
|
||||
dart_jsonwebtoken: ^2.2.0
|
||||
crypto: ^3.0.1
|
||||
flutter_form_builder: ^6.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user