You've already forked flutter-rp-example
First commit
This commit is contained in:
26
lib/globals.dart
Normal file
26
lib/globals.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Globals {
|
||||
Globals._();
|
||||
|
||||
static const String RP_AUTH_BASE_URL = "auth.reflex-platform.com";
|
||||
static const String RP_AUTH_REALM = "reflex-platform";
|
||||
static const String RP_AUTH_CLIENT_ID = "your_client_id_here";
|
||||
|
||||
static const String RP_GRPC_HOST = 'grpc.core.reflex-platform.com';
|
||||
static const int RP_GRPC_PORT = 443;
|
||||
|
||||
|
||||
static const Color RP_BG_COLOR = Color(0xfffcfcfd);
|
||||
static const Color RP_BG_DARK_COLOR = Color(0xfff5f7f9);
|
||||
|
||||
static const Color RP_PRIMARY_COLOR = Color(0xff002151);
|
||||
static const Color RP_PRIMARY_DARK_COLOR = Color(0xff335373);
|
||||
static const Color RP_PRIMARY_LIGHT_COLOR = Color(0xffd9f1fb);
|
||||
static const Color RP_PRIMARY_HOVER_COLOR = Color(0xff5DCEFF);
|
||||
|
||||
static const Color RP_DANGER_COLOR = Color(0xffdc3545);
|
||||
static const Color RP_SUCCESS_COLOR = Color(0xff28a745);
|
||||
static const Color RP_SECONDARY_COLOR = Color(0xff6c757d);
|
||||
static const Color RP_LIGHT_COLOR = Color(0xffe9ecef);
|
||||
}
|
||||
54
lib/l10n/app_en.arb
Normal file
54
lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"addPhoto": "Add photo",
|
||||
"anomaly": "Anomaly",
|
||||
"anomalyBadHandlingUnit": "Declined - Package in bad condition",
|
||||
"anomalyCustomerRefused": "Declined - Customer declined",
|
||||
"anomalyCustomerUnavailable": "Unavailable - Customer unavailable",
|
||||
"anomalyOther": "Other reason",
|
||||
"anomalyReason": "Anomaly reason",
|
||||
"cancel": "Cancel",
|
||||
"cannotNotifyDelivery": "Cannot notify delivery",
|
||||
"copied": "copied",
|
||||
"dateOn": "the {date} at {time}",
|
||||
"@dateOn": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"time": {
|
||||
"type": "DateTime",
|
||||
"format": "jm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deliveryOk": "Delivery notified as OK",
|
||||
"deliveryWithAnomaly": "Delivery notified with anomaly",
|
||||
"dimensions": "Dimensions (H x W x L)",
|
||||
"error": "Error",
|
||||
"handlingUnitIdentifier": "Handling unit Identifier",
|
||||
"handlingUnitType": "Handling unit type",
|
||||
"informations": "Informations",
|
||||
"items": "Items",
|
||||
"itemId": "Item ID",
|
||||
"missingSendingFilesRights": "You are not allowed to send files in this project",
|
||||
"nextDelivery": "Next delivery",
|
||||
"noHandlingUnitFound": "No handling unit found",
|
||||
"ok": "OK",
|
||||
"or": "OR",
|
||||
"organization": "Organization",
|
||||
"pleaseTryAgain": "Please try again",
|
||||
"pleaseIndicateAnomaly": "Please indicate if the handling unit has an anomaly or not",
|
||||
"project": "Project",
|
||||
"quantity": "Quantity",
|
||||
"scanBarcode": "SCAN BARCODE",
|
||||
"search": "Search",
|
||||
"selectContext": "Select the desired organization and project of your delivery",
|
||||
"selectHandlingUnit": "Select a handling unit by typing or scanning its identifier",
|
||||
"signature": "Signature",
|
||||
"start": "Start tour",
|
||||
"tryAgain": "Try again",
|
||||
"trackingNumber": "Tracking N°",
|
||||
"validDelivery": "OK",
|
||||
"weight": "Weight"
|
||||
}
|
||||
54
lib/l10n/app_fr.arb
Normal file
54
lib/l10n/app_fr.arb
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"addPhoto": "Ajouter une photo",
|
||||
"anomaly": "Anomalie",
|
||||
"anomalyBadHandlingUnit": "Refusé - Colis abimé",
|
||||
"anomalyCustomerRefused": "Refusé - Refus client",
|
||||
"anomalyCustomerUnavailable": "Absence - Client non présent",
|
||||
"anomalyOther": "Autres raisons",
|
||||
"anomalyReason": "Raison de l'anomalie",
|
||||
"cancel": "Annuler",
|
||||
"cannotNotifyDelivery": "Impossible de notifier la livraison",
|
||||
"copied": "copié",
|
||||
"dateOn": "le {date} à {time}",
|
||||
"@dateOn": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "DateTime",
|
||||
"format": "yMd"
|
||||
},
|
||||
"time": {
|
||||
"type": "DateTime",
|
||||
"format": "jm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deliveryOk": "Livraison déclarée conforme",
|
||||
"deliveryWithAnomaly": "Livraison déclarée en anomalie",
|
||||
"dimensions": "Dimensions (H x L x P)",
|
||||
"error": "Erreur",
|
||||
"handlingUnitIdentifier": "Identifiant du colis",
|
||||
"handlingUnitType": "Type de colis",
|
||||
"informations": "Informations",
|
||||
"items": "Articles",
|
||||
"itemId": "Article ID",
|
||||
"missingSendingFilesRights": "Vous n'avez pas le droit d'envoyer de fichiers dans ce projet",
|
||||
"nextDelivery": "Livraison suivante",
|
||||
"noHandlingUnitFound": "Aucun colis trouvé pour cet identifiant de colis.",
|
||||
"ok": "OK",
|
||||
"or": "OU",
|
||||
"organization": "Organisation",
|
||||
"pleaseTryAgain": "Veuillez réessayer",
|
||||
"pleaseIndicateAnomaly": "Veuillez indiquer si le colis présente une anomalie ou non",
|
||||
"project": "Projet",
|
||||
"quantity": "Quantité",
|
||||
"scanBarcode": "SCANNER UN CODE BARRE",
|
||||
"search": "Rechercher",
|
||||
"selectContext": "Sélectionner l'organisation et le projet cible de votre livraison",
|
||||
"selectHandlingUnit": "Sélectionner un colis en scannant ou tapant son numéro ou son identifiant tracking",
|
||||
"signature": "Signature",
|
||||
"start": "Démarrer la tournée",
|
||||
"tryAgain": "Réessayer",
|
||||
"trackingNumber": "N° Tracking",
|
||||
"validDelivery": "Valide",
|
||||
"weight": "Poids"
|
||||
}
|
||||
61
lib/locator.dart
Normal file
61
lib/locator.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:dart_core_sdk/gcs-api.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/handlingunitQuery.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/proj.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/trackingInput.pbgrpc.dart';
|
||||
import 'package:sampleapp/globals.dart';
|
||||
import 'package:sampleapp/router/router.dart';
|
||||
import 'package:sampleapp/services/auth/auth.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:sampleapp/services/gcs.dart';
|
||||
|
||||
import 'package:sampleapp/services/grpc_service.dart';
|
||||
import 'package:sampleapp/utils.dart';
|
||||
|
||||
final locator = GetIt.instance;
|
||||
|
||||
Future<void> setup() async {
|
||||
var redirectURI = getRedirectURI();
|
||||
var callbackUrlScheme = await getCallbackUrlScheme();
|
||||
AuthService authService = AuthService(
|
||||
host: Globals.RP_AUTH_BASE_URL,
|
||||
realm: Globals.RP_AUTH_REALM,
|
||||
clientId: Globals.RP_AUTH_CLIENT_ID,
|
||||
redirectUri: redirectURI,
|
||||
callbackUrlScheme: callbackUrlScheme,
|
||||
);
|
||||
locator.registerLazySingleton<AuthService>(() => authService);
|
||||
locator.registerLazySingleton<ReflexRouterDelegate>(
|
||||
() => ReflexRouterDelegate(authService));
|
||||
|
||||
locator.registerLazySingleton<ProjectServiceClient>(() => GrpcClient.initializeClient(
|
||||
authService,
|
||||
Globals.RP_GRPC_HOST,
|
||||
Globals.RP_GRPC_PORT,
|
||||
(channel, interceptors) => ProjectServiceClient(channel, interceptors: interceptors),
|
||||
) as ProjectServiceClient);
|
||||
|
||||
locator.registerLazySingleton<GcsApiClient>(() => GrpcClient.initializeClient(
|
||||
authService,
|
||||
Globals.RP_GRPC_HOST,
|
||||
Globals.RP_GRPC_PORT,
|
||||
(channel, interceptors) => GcsApiClient(channel, interceptors: interceptors),
|
||||
) as GcsApiClient);
|
||||
|
||||
locator.registerLazySingleton<HandlingunitQueryClient>(() => GrpcClient.initializeClient(
|
||||
authService,
|
||||
Globals.RP_GRPC_HOST,
|
||||
Globals.RP_GRPC_PORT,
|
||||
(channel, interceptors) => HandlingunitQueryClient(channel, interceptors: interceptors),
|
||||
) as HandlingunitQueryClient);
|
||||
|
||||
locator.registerLazySingleton<TrackingInputAPIClient>(() => GrpcClient.initializeClient(
|
||||
authService,
|
||||
Globals.RP_GRPC_HOST,
|
||||
Globals.RP_GRPC_PORT,
|
||||
(channel, interceptors) => TrackingInputAPIClient(channel, interceptors: interceptors),
|
||||
) as TrackingInputAPIClient);
|
||||
|
||||
|
||||
|
||||
locator.registerLazySingleton<GcsService>(() => GcsService());
|
||||
}
|
||||
61
lib/main.dart
Normal file
61
lib/main.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/services/auth/auth.dart';
|
||||
import 'package:sampleapp/router/router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'globals.dart';
|
||||
import 'locator.dart';
|
||||
import 'services/auth/auth_view_model.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Setup injections
|
||||
await setup();
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
@override
|
||||
_MyAppState createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late ReflexRouterDelegate delegate;
|
||||
late AuthService authService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
authService = locator.get<AuthService>();
|
||||
delegate = locator.get<ReflexRouterDelegate>();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider<AuthViewModel>(
|
||||
create: (_) => AuthViewModel(authService),
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('fr'),
|
||||
Locale('es'),
|
||||
Locale('pl'),
|
||||
],
|
||||
theme: ThemeData(
|
||||
scaffoldBackgroundColor: Globals.RP_BG_COLOR,
|
||||
),
|
||||
home: Router(routerDelegate: delegate),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthRepository {}
|
||||
25
lib/pages/home.dart
Normal file
25
lib/pages/home.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/services/auth/auth.dart';
|
||||
import 'package:sampleapp/widgets/home_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../globals.dart';
|
||||
import '../locator.dart';
|
||||
import '../services/auth/auth_view_model.dart';
|
||||
import '../widgets/components/reflex_app_bar.dart';
|
||||
|
||||
class HomePage extends Page {
|
||||
final VoidCallback onLogout;
|
||||
|
||||
const HomePage({required this.onLogout})
|
||||
: super(key: const ValueKey('HomePage'));
|
||||
|
||||
@override
|
||||
Route createRoute(BuildContext context) {
|
||||
return MaterialPageRoute(
|
||||
settings: this,
|
||||
builder: (BuildContext context) {
|
||||
return HomeScreen(onLogout: onLogout);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/pages/login.dart
Normal file
18
lib/pages/login.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/login_screen.dart';
|
||||
|
||||
class LoginPage extends Page {
|
||||
final VoidCallback onLogin;
|
||||
|
||||
const LoginPage({required this.onLogin})
|
||||
: super(key: const ValueKey('LoginPage'));
|
||||
|
||||
@override
|
||||
Route createRoute(BuildContext context) {
|
||||
return MaterialPageRoute(
|
||||
settings: this,
|
||||
builder: (BuildContext context) => LoginScreen(onLogin: onLogin),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/pages/splash.dart
Normal file
18
lib/pages/splash.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/widgets/splash_screen.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
|
||||
class SplashPage extends Page {
|
||||
const SplashPage();
|
||||
|
||||
@override
|
||||
Route createRoute(BuildContext context) {
|
||||
return MaterialPageRoute(
|
||||
settings: this,
|
||||
builder: (BuildContext context) {
|
||||
return const SplashScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
84
lib/router/router.dart
Normal file
84
lib/router/router.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/auth/auth.dart';
|
||||
import '../pages/home.dart';
|
||||
import '../pages/splash.dart';
|
||||
import '../pages/login.dart';
|
||||
|
||||
class ReflexRouterDelegate extends RouterDelegate
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
@override
|
||||
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
|
||||
final GlobalKey<NavigatorState> _navigatorKey;
|
||||
|
||||
bool? _loggedIn;
|
||||
bool? get loggedIn => _loggedIn;
|
||||
set loggedIn(value) {
|
||||
_loggedIn = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final AuthService authService;
|
||||
|
||||
ReflexRouterDelegate(this.authService)
|
||||
: _navigatorKey = GlobalKey<NavigatorState>() {
|
||||
_init();
|
||||
}
|
||||
|
||||
_init() async {
|
||||
try {
|
||||
await authService.init();
|
||||
loggedIn = await authService.isLoggedIn();
|
||||
} catch (e) {
|
||||
showDialog(
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text('Error: $e'),
|
||||
),
|
||||
);
|
||||
loggedIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Page> stack;
|
||||
if (loggedIn == null) {
|
||||
stack = _splashStack;
|
||||
} else if (loggedIn!) {
|
||||
stack = _loggedInStack;
|
||||
} else {
|
||||
stack = _loggedOutStack;
|
||||
}
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !await navigatorKey.currentState!.maybePop(),
|
||||
child: Navigator(
|
||||
key: navigatorKey,
|
||||
pages: stack,
|
||||
onPopPage: (route, result) {
|
||||
if (!route.didPop(result)) return false;
|
||||
return true;
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
List<Page> get _splashStack => [const SplashPage()];
|
||||
|
||||
List<Page> get _loggedOutStack => [
|
||||
LoginPage(onLogin: () {
|
||||
loggedIn = true;
|
||||
})
|
||||
];
|
||||
|
||||
List<Page> get _loggedInStack {
|
||||
return [
|
||||
HomePage(onLogout: () {
|
||||
loggedIn = false;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(configuration) async {/* Do Nothing */}
|
||||
}
|
||||
212
lib/services/auth/auth.dart
Normal file
212
lib/services/auth/auth.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'dart:convert' show jsonDecode;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
class User {
|
||||
const User(
|
||||
{required this.firstname, required this.lastname, required this.email});
|
||||
final String firstname;
|
||||
final String lastname;
|
||||
final String email;
|
||||
|
||||
static User fromJson(jsonDecode) {
|
||||
if (jsonDecode == "") {
|
||||
return const User(firstname: '', lastname: '', email: '');
|
||||
}
|
||||
return User(
|
||||
firstname: jsonDecode['firstname'],
|
||||
lastname: jsonDecode['lastname'],
|
||||
email: jsonDecode['email']);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'firstname': firstname,
|
||||
'lastname': lastname,
|
||||
'email': email,
|
||||
};
|
||||
}
|
||||
|
||||
class TokenRefresher {
|
||||
final AuthService authService;
|
||||
final Duration refreshInterval;
|
||||
late Timer _timer;
|
||||
|
||||
TokenRefresher(this.authService, {required this.refreshInterval});
|
||||
|
||||
void start() {
|
||||
_timer = Timer.periodic(refreshInterval, (timer) async {
|
||||
try {
|
||||
await authService.doRefreshToken();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to refresh token');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_timer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
String clientId;
|
||||
String host;
|
||||
String redirectUri;
|
||||
String realm;
|
||||
String callbackUrlScheme;
|
||||
User? user;
|
||||
TokenRefresher? tokenRefresher;
|
||||
String? accessToken = '';
|
||||
String? refreshToken = '';
|
||||
|
||||
AuthService(
|
||||
{required this.host,
|
||||
required this.realm,
|
||||
required this.clientId,
|
||||
required this.redirectUri,
|
||||
required this.callbackUrlScheme}) ;
|
||||
|
||||
// Add init method to initialize the AuthService, it retrives the access token from the shared preferences
|
||||
Future<void> init() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
accessToken = prefs.getString('accessToken');
|
||||
refreshToken = prefs.getString('refreshToken');
|
||||
}
|
||||
|
||||
Future<bool> login() async {
|
||||
// Build the url
|
||||
final url =
|
||||
Uri.https(host, '/auth/realms/$realm/protocol/openid-connect/auth', {
|
||||
'response_type': 'code',
|
||||
'client_id': clientId,
|
||||
'redirect_uri': '$callbackUrlScheme:/$redirectUri',
|
||||
'scope': 'openid',
|
||||
});
|
||||
|
||||
// Present the dialog to the user
|
||||
try {
|
||||
final result = await FlutterWebAuth2.authenticate(
|
||||
url: url.toString(), callbackUrlScheme: callbackUrlScheme);
|
||||
// Extract code from resulting url
|
||||
final code = Uri.parse(result).queryParameters['code'];
|
||||
|
||||
// Use this code to get an access token
|
||||
final tokenUrl =
|
||||
Uri.https(host, '/auth/realms/$realm/protocol/openid-connect/token');
|
||||
var response = await http.post(tokenUrl, body: {
|
||||
'client_id': clientId,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': '$callbackUrlScheme:/$redirectUri',
|
||||
});
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to login');
|
||||
}
|
||||
|
||||
// Get the access token from the response
|
||||
await setAccessToken(jsonDecode(response.body)['access_token'] as String);
|
||||
await setRefreshToken(jsonDecode(response.body)['refresh_token'] as String);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tokenRefresher = TokenRefresher(this, refreshInterval: const Duration(seconds: 60));
|
||||
tokenRefresher!.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> logout() async {
|
||||
if (accessToken == null || accessToken == "") {
|
||||
throw Exception('Not logged in');
|
||||
}
|
||||
|
||||
final logoutUrl =
|
||||
Uri.https(host, '/auth/realms/$realm/protocol/openid-connect/logout');
|
||||
|
||||
final response = await http.post(logoutUrl, body: {
|
||||
'client_id': clientId,
|
||||
'refresh_token': refreshToken,
|
||||
});
|
||||
if (response.statusCode != 204) {
|
||||
throw Exception('Failed to logout');
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
user = null;
|
||||
|
||||
tokenRefresher!.stop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> doRefreshToken() async {
|
||||
if (refreshToken == "") {
|
||||
throw Exception('Not logged in');
|
||||
}
|
||||
|
||||
final tokenUrl =
|
||||
Uri.https(host, '/auth/realms/$realm/protocol/openid-connect/token');
|
||||
final response = await http.post(tokenUrl, body: {
|
||||
'client_id': clientId,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refreshToken,
|
||||
});
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to refresh token: ${response.body}');
|
||||
}
|
||||
|
||||
// Get the access token from the response
|
||||
await setAccessToken(jsonDecode(response.body)['access_token'] as String);
|
||||
await setRefreshToken(jsonDecode(response.body)['refresh_token'] as String);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn() async {
|
||||
user = await getUser();
|
||||
return user != null;
|
||||
}
|
||||
|
||||
Future<void> setAccessToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
accessToken = token;
|
||||
}
|
||||
|
||||
Future<void> setRefreshToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('refreshToken', token);
|
||||
refreshToken = token;
|
||||
}
|
||||
|
||||
Future<User?> getUser() async {
|
||||
if (this.user != null) {
|
||||
return this.user!;
|
||||
}
|
||||
|
||||
final userInfoEndpoint =
|
||||
Uri.https(host, '/auth/realms/$realm/protocol/openid-connect/userinfo');
|
||||
var response = await http.get(userInfoEndpoint, headers: {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
});
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
final userMap = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
var user = User(
|
||||
firstname: userMap['given_name'] as String,
|
||||
lastname: userMap['family_name'] as String,
|
||||
email: userMap['email'] as String);
|
||||
|
||||
this.user = user;
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
30
lib/services/auth/auth_view_model.dart
Normal file
30
lib/services/auth/auth_view_model.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:sampleapp/services/auth/auth.dart';
|
||||
|
||||
class AuthViewModel extends ChangeNotifier {
|
||||
final AuthService authService;
|
||||
bool loggingIn = false;
|
||||
bool loggingOut = false;
|
||||
|
||||
AuthViewModel(this.authService);
|
||||
|
||||
Future<bool> login() {
|
||||
return Future.delayed(Duration.zero, () async {
|
||||
loggingIn = true;
|
||||
notifyListeners();
|
||||
await authService.login();
|
||||
loggingIn = false;
|
||||
notifyListeners();
|
||||
return authService.isLoggedIn();
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> logout() async {
|
||||
loggingOut = true;
|
||||
notifyListeners();
|
||||
await authService.logout();
|
||||
loggingOut = false;
|
||||
notifyListeners();
|
||||
return !(await authService.isLoggedIn());
|
||||
}
|
||||
}
|
||||
44
lib/services/gcs.dart
Normal file
44
lib/services/gcs.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
class GcsService {
|
||||
// ========================================
|
||||
// ======== GCS GOOGLE API SERVICE ========
|
||||
|
||||
static const GCS_GOOGLE_BASE_URI = "https://storage.googleapis.com";
|
||||
static const GCS_GOOGLE_UPLOAD_ENDPOINT = "/upload/storage/v1/b/";
|
||||
static const GET_BUCKET_STS_GOOGLE_ENDPOINT =
|
||||
"/api.GcsApi/GetBucketSTSGoogle";
|
||||
|
||||
Future<bool> uploadImage(String token, String bucketName, String resourceName,
|
||||
String fileName, File file) async {
|
||||
var headersData = {
|
||||
"Authorization": "Bearer $token",
|
||||
"Content-Type": "image/png",
|
||||
};
|
||||
var paramsData = {
|
||||
'uploadType': 'multipart/related; boundary=image/png',
|
||||
'name': "$resourceName$fileName",
|
||||
};
|
||||
|
||||
var uploadURI = "$GCS_GOOGLE_BASE_URI$GCS_GOOGLE_UPLOAD_ENDPOINT";
|
||||
|
||||
var query = paramsData.entries.map((p) => '${p.key}=${p.value}').join('&');
|
||||
|
||||
var request = http.MultipartRequest(
|
||||
"POST", Uri.parse("$uploadURI$bucketName/o?$resourceName$query"));
|
||||
|
||||
request.headers.addAll(headersData);
|
||||
request.files.add(await http.MultipartFile.fromPath(fileName, file.path,
|
||||
contentType: MediaType('image', 'png')));
|
||||
|
||||
var response = await request.send();
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to upload image.');
|
||||
}
|
||||
|
||||
return response.statusCode == 200;
|
||||
}
|
||||
}
|
||||
31
lib/services/grpc_service.dart
Normal file
31
lib/services/grpc_service.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:sampleapp/services/auth/auth.dart';
|
||||
|
||||
class AuthInterceptor extends ClientInterceptor {
|
||||
final AuthService authService;
|
||||
|
||||
AuthInterceptor(this.authService);
|
||||
|
||||
@override
|
||||
ResponseFuture<R> interceptUnary<Q, R>(
|
||||
ClientMethod<Q, R> method, Q request, CallOptions options, invoker) {
|
||||
final metadata = <String, String>{};
|
||||
metadata['authorization'] = 'Bearer ${authService.accessToken}';
|
||||
options = options.mergedWith(CallOptions(metadata: metadata));
|
||||
return invoker(method, request, options);
|
||||
}
|
||||
}
|
||||
|
||||
class GrpcClient {
|
||||
static Client initializeClient(AuthService authService, String host, int port, Client Function(ClientChannel, List<ClientInterceptor>) clientFactory) {
|
||||
final interceptor = AuthInterceptor(authService);
|
||||
final channel = ClientChannel(
|
||||
host,
|
||||
port: port,
|
||||
options: const ChannelOptions(
|
||||
credentials: ChannelCredentials.secure(),
|
||||
),
|
||||
);
|
||||
return clientFactory(channel, [interceptor]);
|
||||
}
|
||||
}
|
||||
31
lib/utils.dart
Normal file
31
lib/utils.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
String getRedirectURI() {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
return "";
|
||||
} else {
|
||||
return "/localhost:3000/auth.html";
|
||||
}
|
||||
} catch (e) {
|
||||
// Must be web
|
||||
return "/localhost:3000/auth.html";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getCallbackUrlScheme() async {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.packageName;
|
||||
} else {
|
||||
return "http";
|
||||
}
|
||||
} catch (e) {
|
||||
// Must be web
|
||||
return "http";
|
||||
}
|
||||
}
|
||||
97
lib/widgets/components/reflex_alert.dart
Normal file
97
lib/widgets/components/reflex_alert.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_box.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexAlert extends StatefulWidget {
|
||||
final String? title;
|
||||
final String text;
|
||||
final IconData icon;
|
||||
final bool? bigIcon;
|
||||
final Color? color;
|
||||
|
||||
const ReflexAlert(
|
||||
{Key? key,
|
||||
required this.text,
|
||||
required this.icon,
|
||||
this.color,
|
||||
bigIcon,
|
||||
this.title})
|
||||
: bigIcon = bigIcon ?? false,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexAlertState createState() => _ReflexAlertState();
|
||||
}
|
||||
|
||||
class _ReflexAlertState extends State<ReflexAlert> {
|
||||
_buildColumnAlert() {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
||||
Icon(
|
||||
widget.icon,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (widget.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Text(
|
||||
widget.title ?? "",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
softWrap: true,
|
||||
)),
|
||||
Text(
|
||||
widget.text,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
softWrap: true,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildRowAlert() {
|
||||
return Row(children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: Colors.white,
|
||||
)),
|
||||
Flexible(flex: 0, child: const SizedBox(width: 10)),
|
||||
Expanded(
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (widget.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Text(
|
||||
widget.title ?? "",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
softWrap: true,
|
||||
)),
|
||||
Text(
|
||||
widget.text,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
softWrap: true,
|
||||
),
|
||||
]))
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ReflexBox(
|
||||
color: widget.color ?? Globals.RP_PRIMARY_COLOR,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: widget.bigIcon == true
|
||||
? _buildColumnAlert()
|
||||
: _buildRowAlert()));
|
||||
}
|
||||
}
|
||||
33
lib/widgets/components/reflex_app_bar.dart
Normal file
33
lib/widgets/components/reflex_app_bar.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final Function()? onBackCallback;
|
||||
|
||||
const ReflexAppBar({Key? key, required this.actions, this.onBackCallback})
|
||||
: super(key: key);
|
||||
|
||||
final List<Widget> actions;
|
||||
final webHeight = 200;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
leading: onBackCallback != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: onBackCallback,
|
||||
)
|
||||
: null,
|
||||
backgroundColor: Globals.RP_PRIMARY_COLOR,
|
||||
centerTitle: false,
|
||||
actions: actions,
|
||||
title: const Text('Reflex Platform', style: TextStyle(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
27
lib/widgets/components/reflex_box.dart
Normal file
27
lib/widgets/components/reflex_box.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexBox extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Color? color;
|
||||
|
||||
const ReflexBox({Key? key, this.color, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexBoxState createState() => _ReflexBoxState();
|
||||
}
|
||||
|
||||
class _ReflexBoxState extends State<ReflexBox> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5.0))),
|
||||
//backgrround
|
||||
color: widget.color ?? Globals.RP_BG_DARK_COLOR,
|
||||
child: widget.child);
|
||||
}
|
||||
}
|
||||
44
lib/widgets/components/reflex_button.dart
Normal file
44
lib/widgets/components/reflex_button.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexButton extends StatelessWidget implements PreferredSizeWidget {
|
||||
final Function()? onPressed;
|
||||
final String text;
|
||||
final bool? isFullWidth;
|
||||
final Color? color;
|
||||
final Color? textColor;
|
||||
|
||||
const ReflexButton(
|
||||
{Key? key,
|
||||
this.onPressed,
|
||||
required this.text,
|
||||
this.isFullWidth,
|
||||
this.color,
|
||||
this.textColor})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: isFullWidth == true
|
||||
? const Size.fromHeight(36)
|
||||
: const Size(0, 36),
|
||||
backgroundColor: color ?? Globals.RP_PRIMARY_COLOR,
|
||||
foregroundColor: textColor ?? Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(text),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
30
lib/widgets/components/reflex_circular_progress.dart
Normal file
30
lib/widgets/components/reflex_circular_progress.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexCircularProgress extends StatefulWidget {
|
||||
final Color? color;
|
||||
final double? size;
|
||||
|
||||
const ReflexCircularProgress({Key? key, this.color, this.size})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexCircularProgressState createState() => _ReflexCircularProgressState();
|
||||
}
|
||||
|
||||
class _ReflexCircularProgressState extends State<ReflexCircularProgress> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: widget.size ?? 50.0,
|
||||
height: widget.size ?? 50.0,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
widget.color ?? Globals.RP_PRIMARY_COLOR),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/globals.dart';
|
||||
|
||||
class ReflexDropdownButtonFormField<T> extends StatefulWidget {
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final T? value;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final String? label;
|
||||
final Widget? hint;
|
||||
final Widget? disabledHint;
|
||||
final ValueChanged<T?>? onSaved;
|
||||
final FormFieldValidator<T>? validator;
|
||||
final bool isExpanded;
|
||||
final bool isDense;
|
||||
final double? iconSize;
|
||||
final Color? iconEnabledColor;
|
||||
final Color? iconDisabledColor;
|
||||
final Color? dropdownColor;
|
||||
|
||||
const ReflexDropdownButtonFormField({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.value,
|
||||
this.onChanged,
|
||||
this.label,
|
||||
this.hint,
|
||||
this.disabledHint,
|
||||
this.onSaved,
|
||||
this.validator,
|
||||
this.isExpanded = false,
|
||||
this.isDense = false,
|
||||
this.iconSize,
|
||||
this.iconEnabledColor,
|
||||
this.iconDisabledColor,
|
||||
this.dropdownColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexDropdownButtonFormFieldState<T> createState() => _ReflexDropdownButtonFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _ReflexDropdownButtonFormFieldState<T> extends State<ReflexDropdownButtonFormField<T>> {
|
||||
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestFocus(){
|
||||
setState(() {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField<T>(
|
||||
onTap: _requestFocus,
|
||||
focusNode: _focusNode,
|
||||
items: widget.items,
|
||||
value: widget.value,
|
||||
onChanged: widget.onChanged,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Globals.RP_PRIMARY_COLOR, width: 2.0),
|
||||
),
|
||||
labelText: widget.label,
|
||||
labelStyle: TextStyle(
|
||||
color: _focusNode.hasFocus ? Globals.RP_PRIMARY_COLOR : Colors.grey,
|
||||
),
|
||||
),
|
||||
hint: widget.hint,
|
||||
disabledHint: widget.disabledHint,
|
||||
onSaved: widget.onSaved,
|
||||
validator: widget.validator,
|
||||
isExpanded: widget.isExpanded,
|
||||
isDense: widget.isDense,
|
||||
iconSize: widget.iconSize ?? 24.0,
|
||||
iconEnabledColor: widget.iconEnabledColor,
|
||||
iconDisabledColor: widget.iconDisabledColor,
|
||||
dropdownColor: widget.dropdownColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
195
lib/widgets/components/reflex_hu_info.dart
Normal file
195
lib/widgets/components/reflex_hu_info.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:dart_core_sdk/handlingunit.pb.dart';
|
||||
import 'package:sampleapp/globals.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_alert.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_button.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'reflex_box.dart';
|
||||
|
||||
class ReflexHUInfo extends StatefulWidget {
|
||||
final Handlingunit hu;
|
||||
final bool showArticles;
|
||||
|
||||
const ReflexHUInfo({super.key, required this.hu, required this.showArticles});
|
||||
|
||||
@override
|
||||
_ReflexHUInfoState createState() => _ReflexHUInfoState();
|
||||
}
|
||||
|
||||
class _ReflexHUInfoState extends State<ReflexHUInfo> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String _translateUnit(String unit) {
|
||||
switch (unit) {
|
||||
case "KILOGRAM":
|
||||
return "kg";
|
||||
case "CENTIMETER":
|
||||
return "cm";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyToClipboard(String key, String value) {
|
||||
return Clipboard.setData(ClipboardData(text: value)).then((_) =>
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text("$key ${AppLocalizations.of(context)!.copied}"))));
|
||||
}
|
||||
|
||||
List<Widget> _buildArticles() {
|
||||
return [
|
||||
const SizedBox(height: 20),
|
||||
Text(AppLocalizations.of(context)!.items,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 5),
|
||||
Expanded(
|
||||
child: ReflexBox(
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
...widget.hu.payload.preparedContents.map((e) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.itemId,
|
||||
),
|
||||
controller:
|
||||
TextEditingController(text: e.goods.itemID),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.quantity,
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: e.quantity.value!.toString()),
|
||||
)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildHU() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.informations,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 5),
|
||||
ReflexBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(children: [
|
||||
TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.handlingUnitIdentifier,
|
||||
),
|
||||
controller: TextEditingController(text: widget.hu.iD.refID),
|
||||
),
|
||||
TextField(
|
||||
readOnly: true,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => _copyToClipboard(AppLocalizations.of(context)!.trackingNumber,
|
||||
widget.hu.payload.currentTrackingSummary.trackingID),
|
||||
icon: const Icon(Icons.copy)),
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.trackingNumber,
|
||||
labelStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: widget.hu.payload.currentTrackingSummary.trackingID),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.handlingUnitType,
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: widget.hu.payload.information.containerType),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.dimensions,
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text:
|
||||
"${widget.hu.payload.information.height.value} x ${widget.hu.payload.information.width.value} x ${widget.hu.payload.information.length!.value} ${_translateUnit(widget.hu.payload.information.length.unit.name)}"),
|
||||
)),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
enabled: false,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
labelText: AppLocalizations.of(context)!.weight
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text:
|
||||
"${widget.hu.payload.information.weight.value} ${_translateUnit(widget.hu.payload.information.weight.unit.name)}"),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]),
|
||||
)),
|
||||
if (widget.showArticles) ..._buildArticles(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _buildHU();
|
||||
}
|
||||
}
|
||||
69
lib/widgets/components/reflex_signature.dart
Normal file
69
lib/widgets/components/reflex_signature.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexSignature extends StatefulWidget {
|
||||
final GlobalKey<SfSignaturePadState> signaturePadKey;
|
||||
|
||||
ReflexSignature({Key? key, required this.signaturePadKey}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexSignatureState createState() => _ReflexSignatureState();
|
||||
}
|
||||
|
||||
class _ReflexSignatureState extends State<ReflexSignature> {
|
||||
bool isInputFocused = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputDecorator(
|
||||
isFocused: isInputFocused,
|
||||
decoration: InputDecoration(
|
||||
focusColor: Globals.RP_PRIMARY_COLOR,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
const BorderSide(color: Globals.RP_PRIMARY_COLOR, width: 2.0),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
const BorderSide(color: Globals.RP_LIGHT_COLOR, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
labelText: AppLocalizations.of(context)!.signature,
|
||||
),
|
||||
child: Stack(children: [
|
||||
SfSignaturePad(
|
||||
key: widget.signaturePadKey,
|
||||
minimumStrokeWidth: 2,
|
||||
maximumStrokeWidth: 2,
|
||||
strokeColor: Colors.black,
|
||||
onDrawStart: () {
|
||||
setState(() {
|
||||
isInputFocused = true;
|
||||
});
|
||||
return false;
|
||||
},
|
||||
onDrawEnd: () {
|
||||
setState(() {
|
||||
isInputFocused = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Globals.RP_BG_DARK_COLOR,
|
||||
child: IconButton(
|
||||
color: Colors.black,
|
||||
onPressed: () {
|
||||
widget.signaturePadKey.currentState!.clear();
|
||||
},
|
||||
icon: const Icon(Icons.clear))),
|
||||
)
|
||||
]));
|
||||
}
|
||||
}
|
||||
74
lib/widgets/components/reflex_text_form_field.dart
Normal file
74
lib/widgets/components/reflex_text_form_field.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../globals.dart';
|
||||
|
||||
class ReflexTextFormField extends StatefulWidget {
|
||||
final String? initialValue;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final bool obscureText;
|
||||
final TextInputType keyboardType;
|
||||
final String? label;
|
||||
final TextEditingController? controller;
|
||||
|
||||
const ReflexTextFormField({
|
||||
Key? key,
|
||||
this.initialValue,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
this.obscureText = false,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.label,
|
||||
this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ReflexTextFormFieldState createState() => _ReflexTextFormFieldState();
|
||||
}
|
||||
|
||||
class _ReflexTextFormFieldState extends State<ReflexTextFormField> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller ?? TextEditingController(text: widget.initialValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
if (widget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestFocus() {
|
||||
setState(() {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onTap: _requestFocus,
|
||||
onChanged: widget.onChanged,
|
||||
obscureText: widget.obscureText,
|
||||
keyboardType: widget.keyboardType,
|
||||
validator: widget.validator,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Globals.RP_PRIMARY_COLOR, width: 2.0),
|
||||
),
|
||||
labelText: widget.label,
|
||||
labelStyle: TextStyle(color:
|
||||
_focusNode.hasFocus ? Globals.RP_PRIMARY_COLOR : Colors.black
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/widgets/home_screen.dart
Normal file
149
lib/widgets/home_screen.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:dart_core_sdk/handlingunit.pb.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:sampleapp/services/grpc_service.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_circular_progress.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../locator.dart';
|
||||
import '../services/auth/auth.dart';
|
||||
import '../services/auth/auth_view_model.dart';
|
||||
import 'scan_flow/delivery_confirmed.dart' as sf;
|
||||
import 'scan_flow/select_context_screen.dart' as sf;
|
||||
import 'scan_flow/scan_barcode_screen.dart' as sf;
|
||||
import 'scan_flow/scan_result_screen.dart' as sf;
|
||||
|
||||
const routePrefixHome = '/home/';
|
||||
const routeHome = '/home/$routeSelectContext';
|
||||
const routeSelectContext = 'select_context';
|
||||
const routeScanBarCode = 'scan_barcode';
|
||||
const routeScanResult = 'scan_result';
|
||||
const routeDeliveryConfirmed = 'delivery_confirmed';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
final VoidCallback onLogout;
|
||||
|
||||
const HomeScreen({super.key, required this.onLogout});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return HomeScreenState();
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScreenState extends State<HomeScreen> {
|
||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||
String? _projectID;
|
||||
String? _handlingUnitID;
|
||||
Handlingunit? _handlingUnit;
|
||||
bool? _isDeliveryValid;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _logout(AuthViewModel avm) async {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => const Center(child: ReflexCircularProgress()));
|
||||
await avm.logout().then((result) => {
|
||||
Navigator.of(context, rootNavigator: true).pop(),
|
||||
if (result) widget.onLogout()
|
||||
});
|
||||
}
|
||||
|
||||
void _onContextSelected(String projectID) {
|
||||
_projectID = projectID;
|
||||
_navigatorKey.currentState!.pushNamed(routeScanBarCode);
|
||||
}
|
||||
|
||||
void _onBarCodeScanned(String handlingUnitRefId, Handlingunit? handlingUnitResult) {
|
||||
_handlingUnit = handlingUnitResult;
|
||||
_handlingUnitID = handlingUnitRefId;
|
||||
_navigatorKey.currentState!.pushNamed(routeScanResult);
|
||||
}
|
||||
|
||||
void _onDeliveryConfirmed(bool isDeliveryValid) {
|
||||
_isDeliveryValid = isDeliveryValid;
|
||||
_navigatorKey.currentState!.pushNamed(routeDeliveryConfirmed);
|
||||
}
|
||||
|
||||
void _onNextDelivery() {
|
||||
_navigatorKey.currentState!.pushNamedAndRemoveUntil(
|
||||
routeScanBarCode, ModalRoute.withName(routeSelectContext));
|
||||
}
|
||||
|
||||
Route _onGenerateRoute(RouteSettings settings) {
|
||||
late Widget page;
|
||||
switch (settings.name) {
|
||||
case routeSelectContext:
|
||||
page = sf.SelectContext(onContextSelected: _onContextSelected);
|
||||
break;
|
||||
case routeScanBarCode:
|
||||
page = sf.ScanBarcode(
|
||||
onBarcodeScanned: _onBarCodeScanned, projectID: _projectID!);
|
||||
break;
|
||||
case routeScanResult:
|
||||
page = sf.ScanResult(
|
||||
refID: _handlingUnitID!,
|
||||
handlingUnit: _handlingUnit,
|
||||
projectID: _projectID!,
|
||||
onDeliveryConfirmed: _onDeliveryConfirmed);
|
||||
break;
|
||||
case routeDeliveryConfirmed:
|
||||
page = sf.DeliveryConfirmed(
|
||||
projectID: _projectID!,
|
||||
handlingUnit: _handlingUnit!,
|
||||
isDeliveryValid: _isDeliveryValid!,
|
||||
onNextDelivery: _onNextDelivery,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
page = Text(settings.name!);
|
||||
}
|
||||
|
||||
return CupertinoPageRoute<dynamic>(
|
||||
builder: (context) {
|
||||
return page;
|
||||
},
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authViewModel = context.watch<AuthViewModel>();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (_navigatorKey.currentState!.canPop()) {
|
||||
_navigatorKey.currentState!.pop();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: ReflexAppBar(
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.logout,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => _logout(authViewModel)
|
||||
)
|
||||
],
|
||||
onBackCallback: () => {
|
||||
if (_navigatorKey.currentState!.canPop())
|
||||
{_navigatorKey.currentState!.pop()}
|
||||
}),
|
||||
body: Navigator(
|
||||
key: _navigatorKey,
|
||||
initialRoute: routeSelectContext,
|
||||
onGenerateRoute: _onGenerateRoute)));
|
||||
}
|
||||
}
|
||||
48
lib/widgets/login_screen.dart
Normal file
48
lib/widgets/login_screen.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_circular_progress.dart';
|
||||
|
||||
import '../services/auth/auth_view_model.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
final VoidCallback onLogin;
|
||||
|
||||
const LoginScreen({Key? key, required this.onLogin}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LoginScreenState createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
Future<void> _login() async {
|
||||
final authViewModel = context.read<AuthViewModel>();
|
||||
final result = await authViewModel.login();
|
||||
if (result == true) widget.onLogin();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_login();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authViewModel = context.watch<AuthViewModel>();
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: authViewModel.loggingIn
|
||||
? const ReflexCircularProgress()
|
||||
: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await _login();
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text('Log in'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
334
lib/widgets/scan_flow/delivery_confirmed.dart
Normal file
334
lib/widgets/scan_flow/delivery_confirmed.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dart_core_sdk/gcs-api.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/handlingunit.pb.dart';
|
||||
import 'package:dart_core_sdk/trackingInput.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/transportShared.pb.dart';
|
||||
import 'package:dart_core_sdk/transportShared.pbenum.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sampleapp/globals.dart';
|
||||
import 'package:sampleapp/services/gcs.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_alert.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_button.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_circular_progress.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_dropdown_button_form_field.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_signature.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
|
||||
import '../../locator.dart';
|
||||
import '../components/reflex_hu_info.dart';
|
||||
import 'dart:ui';
|
||||
import 'package:badges/badges.dart' as bd;
|
||||
import 'package:dart_core_sdk/shared.pb.dart' as rp;
|
||||
|
||||
class DeliveryConfirmed extends StatefulWidget {
|
||||
final bool isDeliveryValid;
|
||||
final Handlingunit handlingUnit;
|
||||
final String projectID;
|
||||
final void Function() onNextDelivery;
|
||||
|
||||
const DeliveryConfirmed(
|
||||
{super.key,
|
||||
required this.isDeliveryValid,
|
||||
required this.handlingUnit,
|
||||
required this.projectID,
|
||||
required this.onNextDelivery});
|
||||
|
||||
@override
|
||||
_DeliveryConfirmedState createState() => _DeliveryConfirmedState();
|
||||
}
|
||||
|
||||
class _DeliveryConfirmedState extends State<DeliveryConfirmed> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
final GlobalKey<SfSignaturePadState> signaturePadKey = GlobalKey();
|
||||
|
||||
var _selectedAnomalyReason = "";
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final List<File> _imageFileList = [];
|
||||
|
||||
Future<bool> _callTrackingServiceNotified(
|
||||
TrackingEventCode code, String reason) async {
|
||||
TrackingEvent event = TrackingEvent(
|
||||
code: code,
|
||||
date: rp.DateTime(dateTime: DateTime.now().toUtc().toIso8601String(), authorTimeZone: "UTC"),
|
||||
reason: reason);
|
||||
bool notified = false;
|
||||
await locator
|
||||
.get<TrackingInputAPIClient>()
|
||||
.notified(
|
||||
TrackingNotifiedRequest(
|
||||
header: rp.RequestProjectHeader(
|
||||
projectID: widget.projectID,
|
||||
),
|
||||
iD: rp.EntityID(
|
||||
refID: widget.handlingUnit.iD.refID,
|
||||
),
|
||||
payload: TrackingNotifiedPayload(
|
||||
events: [event],
|
||||
)
|
||||
)
|
||||
).then((value) => notified = true);
|
||||
return notified;
|
||||
}
|
||||
|
||||
String _buildFileName(String fileName) {
|
||||
return "${fileName}_${widget.handlingUnit.iD.refID}_${DateTime.now().toUtc().toIso8601String().split('.')[0].replaceAll("-", "_").replaceAll(":", "_")}.png";
|
||||
}
|
||||
|
||||
void _takePhoto() async {
|
||||
XFile? pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.camera, maxHeight: 720, maxWidth: 1280);
|
||||
if (pickedFile == null) return;
|
||||
setState(() => _imageFileList.add(File(pickedFile.path)));
|
||||
}
|
||||
|
||||
Future<void> _uploadPhotos(GetBucketSTSResult stsinfo, String resourceName) async {
|
||||
GcsService gcsService = locator.get<GcsService>();
|
||||
|
||||
for (var i = 0; i < _imageFileList.length; i++) {
|
||||
final String fileName = _buildFileName("photo_$i");
|
||||
await gcsService
|
||||
.uploadImage(
|
||||
stsinfo.accessToken,
|
||||
stsinfo.bucketName,
|
||||
resourceName,
|
||||
"${widget.handlingUnit.iD.refID}/photos/$fileName",
|
||||
_imageFileList[i])
|
||||
.catchError((e) => throw e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePostSuccesfulDelivery() async {
|
||||
// Call the TrackingService to notify the delivery
|
||||
var notified = await _callTrackingServiceNotified(
|
||||
TrackingEventCode.TRACKING_EVENT_IFTSTA_21, "");
|
||||
if (!notified) {
|
||||
throw Exception(AppLocalizations.of(context)!.cannotNotifyDelivery);
|
||||
}
|
||||
|
||||
// Upload Signature
|
||||
// First get a token STS
|
||||
GetBucketSTSResult? stsinfo;
|
||||
|
||||
GcsApiClient gcsApiService = locator.get<GcsApiClient>();
|
||||
|
||||
await gcsApiService.getBucketSTS(GetBucketSTSRequest(projectID: widget.projectID)).then((v) => {
|
||||
stsinfo = v,
|
||||
});
|
||||
// Look for the project id in the resourcelist
|
||||
final String resourceName = stsinfo!.resources.firstWhere(
|
||||
(element) => element.endsWith("${widget.projectID}/"),
|
||||
orElse: () => "");
|
||||
if (resourceName == "") {
|
||||
throw Exception(
|
||||
AppLocalizations.of(context)!.missingSendingFilesRights);
|
||||
}
|
||||
|
||||
// Turn the signature into a png file
|
||||
final String fileName = _buildFileName("signature");
|
||||
final image = await signaturePadKey.currentState!.toImage(pixelRatio: 3.0);
|
||||
final signatureByteData =
|
||||
await image.toByteData(format: ImageByteFormat.png);
|
||||
final Uint8List signatureBytes = signatureByteData!.buffer.asUint8List(
|
||||
signatureByteData.offsetInBytes, signatureByteData.lengthInBytes);
|
||||
|
||||
final String path = (await getTemporaryDirectory()).path;
|
||||
final String tmpfileName = "$path/signature.png";
|
||||
final File signatureFile = File(tmpfileName);
|
||||
await signatureFile.writeAsBytes(signatureBytes);
|
||||
|
||||
// Upload the signature to GCS
|
||||
GcsService gcsService = locator.get<GcsService>();
|
||||
await gcsService
|
||||
.uploadImage(
|
||||
stsinfo!.accessToken,
|
||||
stsinfo!.bucketName,
|
||||
resourceName,
|
||||
"${widget.handlingUnit.iD.refID}/signatures/$fileName",
|
||||
signatureFile)
|
||||
.catchError((e) => throw e);
|
||||
|
||||
// And the photos
|
||||
await _uploadPhotos(stsinfo!, resourceName);
|
||||
}
|
||||
|
||||
Future<bool> _handlePostAnomalyDelivery() async {
|
||||
GetBucketSTSResult? stsinfo;
|
||||
|
||||
GcsApiClient gcsApiClient = locator.get<GcsApiClient>();
|
||||
|
||||
await gcsApiClient.getBucketSTS(GetBucketSTSRequest(projectID: widget.projectID)).then((v) => {
|
||||
stsinfo = v,
|
||||
});
|
||||
// Look for the project id in the resourcelist
|
||||
final String resourceName = stsinfo!.resources.firstWhere(
|
||||
(element) => element.endsWith("${widget.projectID}/"),
|
||||
orElse: () => "");
|
||||
if (resourceName == "") {
|
||||
throw Exception(
|
||||
AppLocalizations.of(context)!.missingSendingFilesRights);
|
||||
}
|
||||
|
||||
await _uploadPhotos(stsinfo!, resourceName);
|
||||
|
||||
var notified = await _callTrackingServiceNotified(
|
||||
TrackingEventCode.TRACKING_EVENT_IFTSTA_56, _selectedAnomalyReason);
|
||||
if (!notified) {
|
||||
throw Exception(AppLocalizations.of(context)!.cannotNotifyDelivery);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _handleOnNextDelivery() async {
|
||||
try {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const Center(child: ReflexCircularProgress()));
|
||||
if (widget.isDeliveryValid) {
|
||||
await _handlePostSuccesfulDelivery();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.deliveryOk),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await _handlePostAnomalyDelivery();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.deliveryWithAnomaly),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
widget.onNextDelivery();
|
||||
} catch (e) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.error),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
{Navigator.of(context).pop(), widget.onNextDelivery()},
|
||||
child: Text(AppLocalizations.of(context)!.nextDelivery),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSignatureBox() {
|
||||
return ReflexSignature(signaturePadKey: signaturePadKey);
|
||||
}
|
||||
|
||||
Widget _buildAnomalyInput() {
|
||||
final List<String> _anomalyReasons = [
|
||||
AppLocalizations.of(context)!.anomalyBadHandlingUnit,
|
||||
AppLocalizations.of(context)!.anomalyCustomerRefused,
|
||||
AppLocalizations.of(context)!.anomalyCustomerUnavailable,
|
||||
AppLocalizations.of(context)!.anomalyOther
|
||||
];
|
||||
|
||||
_selectedAnomalyReason = _anomalyReasons[0];
|
||||
|
||||
return ReflexDropdownButtonFormField(
|
||||
label: AppLocalizations.of(context)!.anomalyReason,
|
||||
value: _anomalyReasons[0],
|
||||
items: _anomalyReasons.map<DropdownMenuItem<String>>((String reason) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: reason,
|
||||
child: Text(reason),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
_selectedAnomalyReason = value!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateNowLocale = AppLocalizations.of(context)!.dateOn(DateTime.now(), DateTime.now());
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ReflexHUInfo(
|
||||
hu: widget.handlingUnit,
|
||||
showArticles: false,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ReflexAlert(
|
||||
bigIcon: true,
|
||||
title: widget.isDeliveryValid
|
||||
? AppLocalizations.of(context)!.deliveryOk
|
||||
: AppLocalizations.of(context)!.deliveryWithAnomaly,
|
||||
text: dateNowLocale,
|
||||
icon: widget.isDeliveryValid ? Icons.check : Icons.error,
|
||||
color: widget.isDeliveryValid
|
||||
? Globals.RP_SUCCESS_COLOR
|
||||
: Globals.RP_DANGER_COLOR,
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Flexible(
|
||||
child: widget.isDeliveryValid
|
||||
? _buildSignatureBox()
|
||||
: _buildAnomalyInput()),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: bd.Badge(
|
||||
badgeStyle: const bd.BadgeStyle(
|
||||
badgeColor: Globals.RP_PRIMARY_COLOR,
|
||||
),
|
||||
badgeContent: Text(
|
||||
_imageFileList.length.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
child: ReflexButton(
|
||||
isFullWidth: true,
|
||||
color: Globals.RP_LIGHT_COLOR,
|
||||
textColor: Colors.black,
|
||||
text: AppLocalizations.of(context)!.addPhoto,
|
||||
onPressed: _takePhoto,
|
||||
))),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: ReflexButton(
|
||||
text: AppLocalizations.of(context)!.nextDelivery,
|
||||
onPressed: _handleOnNextDelivery,
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
122
lib/widgets/scan_flow/scan_barcode_screen.dart
Normal file
122
lib/widgets/scan_flow/scan_barcode_screen.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dart_core_sdk/handlingunit.pb.dart';
|
||||
import 'package:dart_core_sdk/handlingunitQuery.pbgrpc.dart';
|
||||
import 'package:dart_core_sdk/shared.pb.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_button.dart';
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_circular_progress.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_text_form_field.dart';
|
||||
import '../../locator.dart';
|
||||
import '../components/reflex_alert.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ScanBarcode extends StatefulWidget {
|
||||
final String projectID;
|
||||
|
||||
final void Function(String refID, Handlingunit? handlingUnitResult) onBarcodeScanned;
|
||||
|
||||
const ScanBarcode(
|
||||
{super.key, required this.projectID, required this.onBarcodeScanned});
|
||||
|
||||
@override
|
||||
_ScanBarcodeState createState() => _ScanBarcodeState();
|
||||
}
|
||||
|
||||
class _ScanBarcodeState extends State<ScanBarcode> {
|
||||
final _handlingUnitIDController = TextEditingController();
|
||||
|
||||
bool _isButtonDisabled() {
|
||||
return _handlingUnitIDController.text.isEmpty;
|
||||
}
|
||||
|
||||
void _onHandlingUnitLookup() async {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => const Center(child: ReflexCircularProgress()));
|
||||
await locator
|
||||
.get<HandlingunitQueryClient>()
|
||||
.getByIds(HandlingunitByIdQuery(
|
||||
header: QueryProjectHeader(
|
||||
projectID: widget.projectID,
|
||||
),
|
||||
iDs: [EntityID(
|
||||
refID: _handlingUnitIDController.text,
|
||||
)],
|
||||
))
|
||||
.then((res) => {
|
||||
widget.onBarcodeScanned(_handlingUnitIDController.text, res.objects.isNotEmpty ? res.objects.first : null),
|
||||
Navigator.of(context, rootNavigator: true).pop()
|
||||
})
|
||||
.catchError((e) => {
|
||||
Navigator.of(context, rootNavigator: true).pop(),
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.error),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.ok))
|
||||
],
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Form(
|
||||
child: Column(
|
||||
children: [
|
||||
ReflexTextFormField(
|
||||
controller: _handlingUnitIDController,
|
||||
label: AppLocalizations.of(context)!.handlingUnitIdentifier,
|
||||
onChanged: (v) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
// Separator
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.or,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Scan button
|
||||
ReflexButton(
|
||||
text: AppLocalizations.of(context)!.scanBarcode,
|
||||
isFullWidth: true,
|
||||
onPressed: () async {
|
||||
var result = await BarcodeScanner.scan();
|
||||
if (result.rawContent.isNotEmpty) {
|
||||
setState(() {
|
||||
_handlingUnitIDController.text = result.rawContent.trim();
|
||||
});
|
||||
_onHandlingUnitLookup();
|
||||
}
|
||||
}),
|
||||
ReflexAlert(
|
||||
text:
|
||||
AppLocalizations.of(context)!.selectHandlingUnit,
|
||||
icon: Icons.info_outline_rounded,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ReflexButton(
|
||||
text: AppLocalizations.of(context)!.search,
|
||||
onPressed:
|
||||
_isButtonDisabled() ? null : _onHandlingUnitLookup,
|
||||
))
|
||||
],
|
||||
))));
|
||||
}
|
||||
}
|
||||
101
lib/widgets/scan_flow/scan_result_screen.dart
Normal file
101
lib/widgets/scan_flow/scan_result_screen.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dart_core_sdk/handlingunit.pb.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_button.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_hu_info.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../components/reflex_alert.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ScanResult extends StatefulWidget {
|
||||
final String refID;
|
||||
final Handlingunit? handlingUnit;
|
||||
final String projectID;
|
||||
final void Function(bool isDeliveryValid) onDeliveryConfirmed;
|
||||
|
||||
const ScanResult(
|
||||
{super.key,
|
||||
required this.projectID,
|
||||
required this.onDeliveryConfirmed,
|
||||
this.handlingUnit, required this.refID});
|
||||
|
||||
@override
|
||||
_ScanResultState createState() => _ScanResultState();
|
||||
}
|
||||
|
||||
class _ScanResultState extends State<ScanResult> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onAnomalyPressed() async {
|
||||
widget.onDeliveryConfirmed(false);
|
||||
}
|
||||
|
||||
void _onValidPressed() async {
|
||||
widget.onDeliveryConfirmed(true);
|
||||
}
|
||||
|
||||
Widget _buildNoHu() {
|
||||
return Column(children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.handlingUnitIdentifier,
|
||||
),
|
||||
controller: TextEditingController(text: widget.refID),
|
||||
readOnly: true,
|
||||
),
|
||||
ReflexAlert(
|
||||
icon: Icons.access_alarm_rounded,
|
||||
text:
|
||||
"${AppLocalizations.of(context)!.noHandlingUnitFound}\n${AppLocalizations.of(context)!.pleaseTryAgain}.",
|
||||
color: Globals.RP_DANGER_COLOR),
|
||||
ReflexButton(
|
||||
text: AppLocalizations.of(context)!.tryAgain,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
isFullWidth: true,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: widget.handlingUnit == null
|
||||
? _buildNoHu()
|
||||
: Column(children: [
|
||||
Expanded(
|
||||
child: ReflexHUInfo(
|
||||
hu: widget.handlingUnit!,
|
||||
showArticles: true,
|
||||
)),
|
||||
SizedBox(height: 10),
|
||||
ReflexAlert(
|
||||
text:
|
||||
AppLocalizations.of(context)!.pleaseIndicateAnomaly,
|
||||
icon: Icons.info_outline_rounded),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReflexButton(
|
||||
text: AppLocalizations.of(context)!.anomaly,
|
||||
color: Globals.RP_DANGER_COLOR,
|
||||
onPressed: () => _onAnomalyPressed(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ReflexButton(
|
||||
text: AppLocalizations.of(context)!.validDelivery,
|
||||
color: Globals.RP_SUCCESS_COLOR,
|
||||
onPressed: () => _onValidPressed(),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
])));
|
||||
}
|
||||
}
|
||||
119
lib/widgets/scan_flow/select_context_screen.dart
Normal file
119
lib/widgets/scan_flow/select_context_screen.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dart_core_sdk/proj.pbgrpc.dart';
|
||||
import 'package:sampleapp/globals.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_alert.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_button.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_circular_progress.dart';
|
||||
import 'package:sampleapp/widgets/components/reflex_dropdown_button_form_field.dart';
|
||||
import '../../locator.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class SelectContext extends StatefulWidget {
|
||||
final void Function(String projectID) onContextSelected;
|
||||
|
||||
const SelectContext({super.key, required this.onContextSelected});
|
||||
|
||||
@override
|
||||
_SelectContextState createState() => _SelectContextState();
|
||||
}
|
||||
|
||||
class _SelectContextState extends State<SelectContext> {
|
||||
final projectClient = locator.get<ProjectServiceClient>();
|
||||
|
||||
List<MyContext> contexts = [];
|
||||
List<Project> projectList = [];
|
||||
Project? selectedProject;
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadContexts();
|
||||
}
|
||||
|
||||
Future<void> _loadContexts() async {
|
||||
projectClient.getMyUIContext(GetMyContextRequest()).then((value) {
|
||||
contexts = value.myContexts;
|
||||
projectList = contexts.first.projects.map((idName) => Project(iD: idName.iD, name: idName.name)).toList();
|
||||
projectList.sort((a, b) => a.name.compareTo(b.name));
|
||||
selectedProject = projectList.isNotEmpty ? projectList.first : null;
|
||||
}).whenComplete(() => setState(() {
|
||||
isLoading = false;
|
||||
}));
|
||||
}
|
||||
|
||||
Widget _buildContextDropdowns() {
|
||||
return ListView(shrinkWrap: true, children: [
|
||||
ReflexDropdownButtonFormField<String>(
|
||||
label: AppLocalizations.of(context)!.organization,
|
||||
value: contexts.isNotEmpty ? contexts[0].organisation.iD : '',
|
||||
items: contexts.map<DropdownMenuItem<String>>((MyContext c) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: c.organisation.iD,
|
||||
child: Text(c.organisation.name),
|
||||
);
|
||||
}).toList(),
|
||||
// On change, update the project list
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
projectList = contexts.firstWhere((element) => element.organisation.iD == value).projects.map((idName) => Project(iD: idName.iD, name: idName.name)).toList();
|
||||
projectList.sort((a, b) => a.name.compareTo(b.name));
|
||||
selectedProject = projectList.isNotEmpty ? projectList.first : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
ReflexDropdownButtonFormField<String>(
|
||||
label: AppLocalizations.of(context)!.project,
|
||||
value: projectList.isNotEmpty ? projectList[0].iD : '',
|
||||
items: projectList.map<DropdownMenuItem<String>>((Project p) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: p.iD,
|
||||
child: Text(p.name),
|
||||
);
|
||||
}).toList(),
|
||||
// On change, update the selected project
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedProject =
|
||||
projectList.firstWhere((element) => element.iD == value);
|
||||
});
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
bool _isButtonDisabled() {
|
||||
return selectedProject == null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
children: [
|
||||
!isLoading
|
||||
? _buildContextDropdowns()
|
||||
: const ReflexCircularProgress(),
|
||||
const SizedBox(height: 20),
|
||||
ReflexAlert(
|
||||
icon: Icons.info_outline_rounded,
|
||||
text:
|
||||
AppLocalizations.of(context)!.selectContext
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ReflexButton(
|
||||
text: AppLocalizations.of(context)!.start,
|
||||
onPressed: _isButtonDisabled()
|
||||
? null
|
||||
: () {
|
||||
widget.onContextSelected(selectedProject!.iD);
|
||||
},
|
||||
))
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
34
lib/widgets/splash_screen.dart
Normal file
34
lib/widgets/splash_screen.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return SplashState();
|
||||
}
|
||||
}
|
||||
|
||||
class SplashState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Globals.RP_PRIMARY_COLOR,
|
||||
body: Center(
|
||||
child:
|
||||
Image(image: AssetImage('assets/images/LOGO_REFLEX_WHITE.png'),
|
||||
height: 200.0,
|
||||
width: 200.0,)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user