diff --git a/lib/app/data/database.dart b/lib/app/data/database.dart index 8f6f898..29c7904 100644 --- a/lib/app/data/database.dart +++ b/lib/app/data/database.dart @@ -69,7 +69,7 @@ class AppStorage extends _$AppStorage { } @override - int get schemaVersion => 1; + int get schemaVersion => 2; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/lib/app/data/database.g.dart b/lib/app/data/database.g.dart index b5f44a2..57d4d27 100644 --- a/lib/app/data/database.g.dart +++ b/lib/app/data/database.g.dart @@ -1564,7 +1564,9 @@ class $OrderLinesTable extends OrderLines class OrderStorage extends DataClass implements Insertable { final int id; final String name; - OrderStorage({required this.id, required this.name}); + final int sequenceNumber; + OrderStorage( + {required this.id, required this.name, required this.sequenceNumber}); factory OrderStorage.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return OrderStorage( @@ -1572,6 +1574,8 @@ class OrderStorage extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, name: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}name'])!, + sequenceNumber: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}sequence_number'])!, ); } @override @@ -1579,6 +1583,7 @@ class OrderStorage extends DataClass implements Insertable { final map = {}; map['id'] = Variable(id); map['name'] = Variable(name); + map['sequence_number'] = Variable(sequenceNumber); return map; } @@ -1586,6 +1591,7 @@ class OrderStorage extends DataClass implements Insertable { return OrderStoragesCompanion( id: Value(id), name: Value(name), + sequenceNumber: Value(sequenceNumber), ); } @@ -1595,6 +1601,7 @@ class OrderStorage extends DataClass implements Insertable { return OrderStorage( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), + sequenceNumber: serializer.fromJson(json['sequenceNumber']), ); } @override @@ -1603,55 +1610,70 @@ class OrderStorage extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'name': serializer.toJson(name), + 'sequenceNumber': serializer.toJson(sequenceNumber), }; } - OrderStorage copyWith({int? id, String? name}) => OrderStorage( + OrderStorage copyWith({int? id, String? name, int? sequenceNumber}) => + OrderStorage( id: id ?? this.id, name: name ?? this.name, + sequenceNumber: sequenceNumber ?? this.sequenceNumber, ); @override String toString() { return (StringBuffer('OrderStorage(') ..write('id: $id, ') - ..write('name: $name') + ..write('name: $name, ') + ..write('sequenceNumber: $sequenceNumber') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name); + int get hashCode => Object.hash(id, name, sequenceNumber); @override bool operator ==(Object other) => identical(this, other) || - (other is OrderStorage && other.id == this.id && other.name == this.name); + (other is OrderStorage && + other.id == this.id && + other.name == this.name && + other.sequenceNumber == this.sequenceNumber); } class OrderStoragesCompanion extends UpdateCompanion { final Value id; final Value name; + final Value sequenceNumber; const OrderStoragesCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), + this.sequenceNumber = const Value.absent(), }); OrderStoragesCompanion.insert({ this.id = const Value.absent(), required String name, - }) : name = Value(name); + required int sequenceNumber, + }) : name = Value(name), + sequenceNumber = Value(sequenceNumber); static Insertable custom({ Expression? id, Expression? name, + Expression? sequenceNumber, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (name != null) 'name': name, + if (sequenceNumber != null) 'sequence_number': sequenceNumber, }); } - OrderStoragesCompanion copyWith({Value? id, Value? name}) { + OrderStoragesCompanion copyWith( + {Value? id, Value? name, Value? sequenceNumber}) { return OrderStoragesCompanion( id: id ?? this.id, name: name ?? this.name, + sequenceNumber: sequenceNumber ?? this.sequenceNumber, ); } @@ -1664,6 +1686,9 @@ class OrderStoragesCompanion extends UpdateCompanion { if (name.present) { map['name'] = Variable(name.value); } + if (sequenceNumber.present) { + map['sequence_number'] = Variable(sequenceNumber.value); + } return map; } @@ -1671,7 +1696,8 @@ class OrderStoragesCompanion extends UpdateCompanion { String toString() { return (StringBuffer('OrderStoragesCompanion(') ..write('id: $id, ') - ..write('name: $name') + ..write('name: $name, ') + ..write('sequenceNumber: $sequenceNumber') ..write(')')) .toString(); } @@ -1695,8 +1721,14 @@ class $OrderStoragesTable extends OrderStorages late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, false, type: const StringType(), requiredDuringInsert: true); + final VerificationMeta _sequenceNumberMeta = + const VerificationMeta('sequenceNumber'); @override - List get $columns => [id, name]; + late final GeneratedColumn sequenceNumber = GeneratedColumn( + 'sequence_number', aliasedName, false, + type: const IntType(), requiredDuringInsert: true); + @override + List get $columns => [id, name, sequenceNumber]; @override String get aliasedName => _alias ?? 'order_storages'; @override @@ -1715,6 +1747,14 @@ class $OrderStoragesTable extends OrderStorages } else if (isInserting) { context.missing(_nameMeta); } + if (data.containsKey('sequence_number')) { + context.handle( + _sequenceNumberMeta, + sequenceNumber.isAcceptableOrUnknown( + data['sequence_number']!, _sequenceNumberMeta)); + } else if (isInserting) { + context.missing(_sequenceNumberMeta); + } return context; } diff --git a/lib/app/data/order_storages_dao.dart b/lib/app/data/order_storages_dao.dart index 7459109..36b9b82 100644 --- a/lib/app/data/order_storages_dao.dart +++ b/lib/app/data/order_storages_dao.dart @@ -7,7 +7,7 @@ class OrderStoragesDao extends DatabaseAccessor with _$OrderStorages OrderStoragesDao(AppStorage db) : super(db); Future> getOrderStorages() async { - return (select(orderStorages)..orderBy([(u) => OrderingTerm(expression: u.name)])).get(); + return (select(orderStorages)..orderBy([(u) => OrderingTerm(expression: u.sequenceNumber)])).get(); } Future loadOrderStorages(List orderStorageList) async { diff --git a/lib/app/data/schema.dart b/lib/app/data/schema.dart index 5234e0f..25cd75f 100644 --- a/lib/app/data/schema.dart +++ b/lib/app/data/schema.dart @@ -52,6 +52,7 @@ class OrderLines extends Table { class OrderStorages extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); + IntColumn get sequenceNumber => integer()(); } class JsonConverter extends TypeConverter, String> { diff --git a/lib/app/entities/api_order_storage.dart b/lib/app/entities/api_order_storage.dart index 3f9b598..441b114 100644 --- a/lib/app/entities/api_order_storage.dart +++ b/lib/app/entities/api_order_storage.dart @@ -3,23 +3,27 @@ part of 'entities.dart'; class ApiOrderStorage { final int id; final String name; + final int sequenceNumber; const ApiOrderStorage({ required this.id, required this.name, + required this.sequenceNumber }); factory ApiOrderStorage.fromJson(dynamic json) { return ApiOrderStorage( id: json['id'], - name: json['name'] + name: json['name'], + sequenceNumber: json['sequenceNumber'] ); } OrderStorage toDatabaseEnt() { return OrderStorage( id: id, - name: name + name: name, + sequenceNumber: sequenceNumber ); } } diff --git a/lib/app/pages/accept_payment/accept_payment_view_model.dart b/lib/app/pages/accept_payment/accept_payment_view_model.dart index 3827110..c8ca62d 100644 --- a/lib/app/pages/accept_payment/accept_payment_view_model.dart +++ b/lib/app/pages/accept_payment/accept_payment_view_model.dart @@ -32,7 +32,7 @@ class AcceptPaymentViewModel extends PageViewModel cancelPayment() async { await iboxpro.cancelPayment(); - emit(state.copyWith(message: 'Платеж отменен', canceled: true)); + emit(state.copyWith(message: 'Платеж отменен', canceled: true, status: AcceptPaymentStateStatus.failure)); } Future _connectToDevice() async { @@ -124,7 +124,11 @@ class AcceptPaymentViewModel extends PageViewModel _savePayment([Map? transaction]) async { - emit(state.copyWith(message: 'Сохранение информации об оплате', status: AcceptPaymentStateStatus.savingPayment)); + emit(state.copyWith( + message: 'Сохранение информации об оплате', + status: AcceptPaymentStateStatus.savingPayment, + isCancelable: false + )); try { await _acceptPayment(transaction); diff --git a/lib/app/pages/info/info_page.dart b/lib/app/pages/info/info_page.dart index 572147e..d863abf 100644 --- a/lib/app/pages/info/info_page.dart +++ b/lib/app/pages/info/info_page.dart @@ -128,6 +128,8 @@ class _InfoViewState extends State<_InfoView> { return Card( child: ListTile( onTap: () { + if (vm.state.loading) return; + Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => OrdersPage())); }, isThreeLine: true, diff --git a/lib/app/pages/info/info_state.dart b/lib/app/pages/info/info_state.dart index 2b3b437..7e67a9f 100644 --- a/lib/app/pages/info/info_state.dart +++ b/lib/app/pages/info/info_state.dart @@ -15,25 +15,29 @@ class InfoState { this.status = InfoStateStatus.initial, this.ordersWithLines = const [], this.newVersionAvailable = false, - this.message = '' + this.message = '', + this.loading = false }); final List ordersWithLines; final bool newVersionAvailable; final InfoStateStatus status; final String message; + final bool loading; InfoState copyWith({ InfoStateStatus? status, List? ordersWithLines, bool? newVersionAvailable, - String? message + String? message, + bool? loading }) { return InfoState( status: status ?? this.status, ordersWithLines: ordersWithLines ?? this.ordersWithLines, newVersionAvailable: newVersionAvailable ?? this.newVersionAvailable, - message: message ?? this.message + message: message ?? this.message, + loading: loading ?? this.loading ); } } diff --git a/lib/app/pages/info/info_view_model.dart b/lib/app/pages/info/info_view_model.dart index 5838553..04aa240 100644 --- a/lib/app/pages/info/info_view_model.dart +++ b/lib/app/pages/info/info_view_model.dart @@ -21,22 +21,22 @@ class InfoViewModel extends PageViewModel { emit(state.copyWith( status: InfoStateStatus.dataLoaded, newVersionAvailable: await app.newVersionAvailable, - ordersWithLines: await app.storage.ordersDao.getOrdersWithLines(), + ordersWithLines: await app.storage.ordersDao.getOrdersWithLines() )); } Future getData() async { if (state.status == InfoStateStatus.inProgress) return; - emit(state.copyWith(status: InfoStateStatus.inProgress)); + emit(state.copyWith(status: InfoStateStatus.inProgress, loading: true)); try { await app.loadUserData(); await _getData(); - emit(state.copyWith(status: InfoStateStatus.success, message: 'Данные успешно обновлены')); + emit(state.copyWith(status: InfoStateStatus.success, message: 'Данные успешно обновлены', loading: false)); } on AppError catch(e) { - emit(state.copyWith(status: InfoStateStatus.failure, message: e.message)); + emit(state.copyWith(status: InfoStateStatus.failure, message: e.message, loading: false)); } } diff --git a/lib/app/pages/login/login_page.dart b/lib/app/pages/login/login_page.dart index 8212a9a..0241b95 100644 --- a/lib/app/pages/login/login_page.dart +++ b/lib/app/pages/login/login_page.dart @@ -37,7 +37,7 @@ class _LoginViewState extends State<_LoginView> { final TextEditingController _urlController = TextEditingController(); Future openDialog() async { - showDialog( + showDialog( context: context, builder: (_) => const Center(child: CircularProgressIndicator()), barrierDismissible: false diff --git a/lib/app/pages/order/order_page.dart b/lib/app/pages/order/order_page.dart index 2d091b2..d5fe156 100644 --- a/lib/app/pages/order/order_page.dart +++ b/lib/app/pages/order/order_page.dart @@ -43,9 +43,10 @@ class _OrderViewState extends State<_OrderView> { final TextEditingController _weightController = TextEditingController(); final TextEditingController _volumeController = TextEditingController(); Completer _dialogCompleter = Completer(); + final ButtonStyle _buttonStyle = TextButton.styleFrom(primary: Colors.blue); Future openDialog() async { - showDialog( + showDialog( context: context, builder: (_) => const Center(child: CircularProgressIndicator()), barrierDismissible: false @@ -79,11 +80,11 @@ class _OrderViewState extends State<_OrderView> { Future showAcceptPaymentDialog() async { OrderViewModel vm = context.read(); - String result = await showDialog( + String result = await showDialog( context: context, builder: (_) => AcceptPaymentPage(order: vm.state.order, cardPayment: vm.state.cardPayment), barrierDismissible: false - ); + ) ?? 'Платеж отменен'; vm.finishPayment(result); } @@ -127,6 +128,7 @@ class _OrderViewState extends State<_OrderView> { alignedDropdown: true, child: DropdownButton( isExpanded: true, + menuMaxHeight: 200, value: newOrderStorage, items: vm.state.storages.map((e) => DropdownMenuItem( value: e, @@ -200,7 +202,7 @@ class _OrderViewState extends State<_OrderView> { ), InfoRow( title: const Text('Вес, кг'), - trailing: !vm.state.deliverable || !vm.state.scanned ? Text(weight) : TextFormField( + trailing: !vm.state.deliverable ? Text(weight) : TextFormField( maxLines: 1, autocorrect: false, controller: _weightController, @@ -212,7 +214,7 @@ class _OrderViewState extends State<_OrderView> { ), InfoRow( title: const Text('Объем, м3'), - trailing: !vm.state.deliverable || !vm.state.scanned ? Text(volume) : TextFormField( + trailing: !vm.state.deliverable ? Text(volume) : TextFormField( maxLines: 1, autocorrect: false, controller: _volumeController, @@ -236,83 +238,96 @@ class _OrderViewState extends State<_OrderView> { ), InfoRow( title: const Text('К оплате'), - trailing: Text(Format.numberStr(order.paySum)) + trailing: Row( + children: [ + Text(Format.numberStr(order.paySum)), + !vm.state.payable ? Container() : Row( + children: [ + SizedBox( + width: 48, + child: TextButton( + onPressed: () => vm.tryStartPayment(false), + child: const Icon(Icons.account_balance_wallet, color: Colors.black), + ), + ), + SizedBox( + width: 48, + child: TextButton( + onPressed: () => vm.tryStartPayment(true), + child: const Icon(Icons.credit_card, color: Colors.black), + ) + ) + ] + ) + ] + ) ), ExpansionTile( title: const Text('Позиции', style: TextStyle(fontSize: 14)), initiallyExpanded: false, tilePadding: const EdgeInsets.symmetric(horizontal: 8), children: vm.state.lines.map((e) => _buildOrderLineTile(context, e)).toList() - ) + ), + orderActions(context), + orderDeliveryActions(context) ]; } - List orderActions(BuildContext context) { + Widget orderActions(BuildContext context) { OrderViewModel vm = context.read(); - ButtonStyle style = TextButton.styleFrom(primary: Colors.black); - List sharedActions = [ - TextButton( + + List actions = [ + !(vm.state.transferAcceptable) ? null : TextButton( onPressed: vm.acceptTransferOrder, - child: Column(children: const [Icon(Icons.how_to_reg_sharp), Text('Принять')]), - style: style + child: Column(children: const [Icon(Icons.how_to_reg_sharp, color: Colors.black), Text('Принять')]), + style: _buttonStyle ), - ]; - List storageActions = [ !vm.state.acceptable ? null : TextButton( onPressed: vm.tryAcceptOrder, - child: Column(children: const [Icon(Icons.fact_check), Text('Приемка')]), - style: style + child: Column(children: const [Icon(Icons.fact_check, color: Colors.black), Text('Приемка')]), + style: _buttonStyle ), - TextButton( + !vm.state.transferable ? null : TextButton( onPressed: showOrderTransferDialog, - child: Column(children: const [Icon(Icons.transfer_within_a_station), Text('Передать')]), - style: style - ) - ].whereType().toList(); - List pickupPointActions = [ - !vm.state.payable ? null : TextButton( - onPressed: () { - vm.tryStartPayment(false); - }, - child: Column(children: const [Icon(Icons.account_balance_wallet), Text('Наличными')]), - style: style - ), - !vm.state.payable ? null : TextButton( - onPressed: () { - vm.tryStartPayment(true); - }, - child: Column(children: const [Icon(Icons.credit_card), Text('Картой')]), - style: style + child: Column(children: const [Icon(Icons.transfer_within_a_station, color: Colors.black), Text('Передать')]), + style: _buttonStyle ), + ].whereType().toList(); + + if (actions.isEmpty) return Container(); + + return ExpansionTile( + title: const Text('Передвижение', style: TextStyle(fontSize: 14)), + initiallyExpanded: false, + tilePadding: const EdgeInsets.symmetric(horizontal: 8), + children: [Row(children: actions, mainAxisAlignment: MainAxisAlignment.spaceAround)] + ); + } + + Widget orderDeliveryActions(BuildContext context) { + OrderViewModel vm = context.read(); + + List actions = [ !vm.state.deliverable ? null : TextButton( onPressed: vm.tryConfirmOrder, - child: Column(children: const [Icon(Icons.assignment_turned_in), Text('Выдать')]), - style: style + child: Column(children: const [Icon(Icons.assignment_turned_in, color: Colors.black), Text('Выдать')]), + style: _buttonStyle ), !vm.state.deliverable ? null : TextButton( onPressed: vm.tryCancelOrder, - child: Column(children: const [Icon(Icons.assignment_return), Text('Вернуть')]), - style: style + child: Column(children: const [Icon(Icons.assignment_return, color: Colors.black), Text('Вернуть')]), + style: _buttonStyle ), ].whereType().toList(); - List actions = []; + if (actions.isEmpty) return Container(); - if (vm.state.pickupPointAccess) actions.addAll(pickupPointActions); - if (vm.state.storageAccess) actions.addAll(storageActions); - if (actions.isNotEmpty) actions.addAll(sharedActions); - - if (!vm.state.scanned && actions.isNotEmpty) { - return [ - TextButton( - onPressed: vm.startScan, - child: const Icon(Icons.qr_code_scanner), - style: style - ) - ]; - } - - return actions; + return ExpansionTile( + title: const Text('ПВЗ', style: TextStyle(fontSize: 14)), + initiallyExpanded: false, + tilePadding: const EdgeInsets.symmetric(horizontal: 8), + children: [Row(children: actions, mainAxisAlignment: MainAxisAlignment.spaceAround)] + ); } Widget _buildOrderLineTile(BuildContext context, OrderLine orderLine) { @@ -336,7 +351,7 @@ class _OrderViewState extends State<_OrderView> { Row( children: [ Text(Format.numberStr(orderLine.price) + ' x ', style: style), - !vm.state.deliverable || !vm.state.scanned ? Text(amountStr, style: style) : + !vm.state.deliverable ? Text(amountStr, style: style) : SizedBox( width: 40, height: 36, @@ -362,7 +377,7 @@ class _OrderViewState extends State<_OrderView> { Widget build(BuildContext context) { return BlocConsumer( builder: (context, state) { - List actions = orderActions(context); + OrderViewModel vm = context.read(); return Scaffold( appBar: AppBar( @@ -374,9 +389,9 @@ class _OrderViewState extends State<_OrderView> { ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 24, bottom: 24), - children: orderInfoRows(context)..add(const SizedBox(height: 72)) + children: orderInfoRows(context)..add(SizedBox(height: !vm.state.scannable ? 0 : 72)) ), - actions.isEmpty ? Container() : SizedBox( + !vm.state.scannable ? Container() : SizedBox( height: 72, child: Container( width: MediaQuery.of(context).size.width, @@ -386,7 +401,11 @@ class _OrderViewState extends State<_OrderView> { ), padding: const EdgeInsets.only(bottom: 4, right: 8, left: 8), child: Center( - child: SingleChildScrollView(scrollDirection: Axis.horizontal, child: Row(children: actions)) + child: TextButton( + onPressed: vm.startScan, + child: const Icon(Icons.qr_code_scanner, color: Colors.black), + style: _buttonStyle + ) ) ) ) diff --git a/lib/app/pages/order/order_state.dart b/lib/app/pages/order/order_state.dart index 4ea1871..bcd5aa7 100644 --- a/lib/app/pages/order/order_state.dart +++ b/lib/app/pages/order/order_state.dart @@ -36,9 +36,12 @@ class OrderState { List get lines => orderWithLines.lines; Order get order => orderWithLines.order; - bool get acceptable => order.firstMovementDate == null; - bool get deliverable => order.delivered == null; - bool get payable => !deliverable && order.paySum != 0 && order.paidSum == 0; + bool get transferAcceptable => scanned && (storageAccess || pickupPointAccess); + bool get transferable => scanned && storageAccess; + bool get acceptable => scanned && storageAccess && order.firstMovementDate == null; + bool get deliverable => scanned && pickupPointAccess && order.delivered == null; + bool get scannable => !scanned && (storageAccess || pickupPointAccess); + bool get payable => scanned && pickupPointAccess && !deliverable && order.paySum != 0 && order.paidSum == 0; bool get storageAccess => user?.roles.contains('storage') ?? false; bool get pickupPointAccess => user?.roles.contains('pickup') ?? false; diff --git a/lib/app/pages/order_qr_scan/order_qr_scan_page.dart b/lib/app/pages/order_qr_scan/order_qr_scan_page.dart index 7d97576..f4c0826 100644 --- a/lib/app/pages/order_qr_scan/order_qr_scan_page.dart +++ b/lib/app/pages/order_qr_scan/order_qr_scan_page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; @@ -9,6 +8,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import '/app/data/database.dart'; import '/app/constants/strings.dart'; import '/app/pages/shared/page_view_model.dart'; +import '/app/utils/misc.dart'; import '/app/widgets/widgets.dart'; part 'order_qr_scan_state.dart'; diff --git a/lib/app/pages/order_qr_scan/order_qr_scan_state.dart b/lib/app/pages/order_qr_scan/order_qr_scan_state.dart index 5be5760..887465a 100644 --- a/lib/app/pages/order_qr_scan/order_qr_scan_state.dart +++ b/lib/app/pages/order_qr_scan/order_qr_scan_state.dart @@ -21,7 +21,7 @@ class OrderQRScanState { this.orderPackageScanned = const [], this.message = '', required this.mode, - this.cameras = const [] + this.cameraEnabled = false }); final OrderQRScanStateStatus status; @@ -29,10 +29,9 @@ class OrderQRScanState { final Order order; final List orderPackageScanned; final ScanMode mode; - List cameras; + final bool cameraEnabled; bool get scannerEnabled => Platform.isAndroid; - bool get cameraEnabled => cameras.isNotEmpty; OrderQRScanState copyWith({ OrderQRScanStateStatus? status, @@ -40,7 +39,7 @@ class OrderQRScanState { List? orderPackageScanned, String? message, ScanMode? mode, - List? cameras + bool? cameraEnabled }) { return OrderQRScanState( status: status ?? this.status, @@ -48,7 +47,7 @@ class OrderQRScanState { orderPackageScanned: orderPackageScanned ?? this.orderPackageScanned, message: message ?? this.message, mode: mode ?? this.mode, - cameras: cameras ?? this.cameras + cameraEnabled: cameraEnabled ?? this.cameraEnabled ); } } diff --git a/lib/app/pages/order_qr_scan/order_qr_scan_view_model.dart b/lib/app/pages/order_qr_scan/order_qr_scan_view_model.dart index d074f05..d8c05f8 100644 --- a/lib/app/pages/order_qr_scan/order_qr_scan_view_model.dart +++ b/lib/app/pages/order_qr_scan/order_qr_scan_view_model.dart @@ -16,11 +16,9 @@ class OrderQRScanViewModel extends PageViewModel loadData() async { - List cameras = await availableCameras(); - emit(state.copyWith( status: OrderQRScanStateStatus.dataLoaded, - cameras: cameras + cameraEnabled: await Misc.hasCamera() )); } diff --git a/lib/app/pages/orders/orders_page.dart b/lib/app/pages/orders/orders_page.dart index 9574dde..e1153ba 100644 --- a/lib/app/pages/orders/orders_page.dart +++ b/lib/app/pages/orders/orders_page.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:io'; import 'package:drift/drift.dart' show TableUpdateQuery; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:quiver/core.dart'; import '/app/constants/strings.dart'; @@ -11,6 +13,7 @@ import '/app/entities/entities.dart'; import '/app/pages/order/order_page.dart'; import '/app/pages/shared/page_view_model.dart'; import '/app/services/api.dart'; +import '/app/utils/misc.dart'; part 'orders_state.dart'; part 'orders_view_model.dart'; @@ -38,7 +41,7 @@ class _OrdersViewState extends State<_OrdersView> { Completer _dialogCompleter = Completer(); Future openDialog() async { - showDialog( + showDialog( context: context, builder: (_) => const Center(child: CircularProgressIndicator()), barrierDismissible: false @@ -109,15 +112,43 @@ class _OrdersViewState extends State<_OrdersView> { await vm.findOrder(trackingNumberController.text); } + Future showQrScan() async { + OrdersViewModel vm = context.read(); + + String? result = await showDialog( + context: context, + useSafeArea: false, + builder: (context) { + return _OrderQRFindDialog(); + } + ); + + if (result == null) return; + + await vm.findOrder(result); + } + @override Widget build(BuildContext context) { return BlocConsumer( builder: (context, state) { + OrdersViewModel vm = context.read(); + return Scaffold( - appBar: AppBar(title: const Text('Заказы')), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.search), - onPressed: showManualInput + appBar: AppBar( + title: const Text('Заказы'), + actions: [ + !vm.state.cameraEnabled ? Container() : IconButton( + icon: const Icon(Icons.qr_code), + onPressed: showQrScan, + tooltip: 'Сканировать QR код' + ), + IconButton( + icon: const Icon(Icons.text_fields), + onPressed: showManualInput, + tooltip: 'Указать вручную', + ), + ], ), body: ListView( physics: const AlwaysScrollableScrollPhysics(), @@ -166,3 +197,92 @@ class _OrdersViewState extends State<_OrdersView> { ); } } + +class _OrderQRFindDialog extends StatefulWidget { + @override + _OrderQRFindDialogState createState() => _OrderQRFindDialogState(); +} + +class _OrderQRFindDialogState extends State<_OrderQRFindDialog> { + final GlobalKey _qrKey = GlobalKey(); + QRViewController? _controller; + StreamSubscription? _subscription; + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + _controller!.pauseCamera(); + } else if (Platform.isIOS) { + _controller!.resumeCamera(); + } + } + + @override + void dispose() { + _subscription?.cancel(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + actions: [ + IconButton( + color: Colors.white, + icon: const Icon(Icons.flash_on), + onPressed: () async { + _controller!.toggleFlash(); + } + ), + IconButton( + color: Colors.white, + icon: const Icon(Icons.switch_camera), + onPressed: () async { + _controller!.flipCamera(); + } + ), + ], + ), + extendBodyBehindAppBar: false, + body: Center( + child: QRView( + key: _qrKey, + formatsAllowed: const [ + BarcodeFormat.qrcode + ], + overlay: QrScannerOverlayShape( + borderColor: Colors.white, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 200 + ), + onPermissionSet: (QRViewController controller, bool permission) { + DateTime? lastScan; + + _subscription = _controller!.scannedDataStream.listen((scanData) async { + final currentScan = DateTime.now(); + + if (lastScan == null || currentScan.difference(lastScan!) > const Duration(seconds: 2)) { + lastScan = currentScan; + + List qrCodeData = scanData.code.split(' '); + + if (qrCodeData.length < 3 || qrCodeData[0] != Strings.qrCodeVersion) return; + + Navigator.of(context).pop(qrCodeData[1]); + } + }); + }, + onQRViewCreated: (QRViewController controller) { + _controller = controller; + }, + ) + ) + ); + } +} diff --git a/lib/app/pages/orders/orders_state.dart b/lib/app/pages/orders/orders_state.dart index f216c8d..2cf4994 100644 --- a/lib/app/pages/orders/orders_state.dart +++ b/lib/app/pages/orders/orders_state.dart @@ -15,24 +15,28 @@ class OrdersState { this.ordersWithLines = const [], this.foundOrderWithLine, this.message = '', + this.cameraEnabled = false }); final OrdersStateStatus status; final List ordersWithLines; final OrderWithLines? foundOrderWithLine; final String message; + final bool cameraEnabled; OrdersState copyWith({ OrdersStateStatus? status, List? ordersWithLines, Optional? foundOrderWithLine, - String? message + String? message, + bool? cameraEnabled }) { return OrdersState( status: status ?? this.status, ordersWithLines: ordersWithLines ?? this.ordersWithLines, foundOrderWithLine: foundOrderWithLine != null ? foundOrderWithLine.orNull : this.foundOrderWithLine, - message: message ?? this.message + message: message ?? this.message, + cameraEnabled: cameraEnabled ?? this.cameraEnabled ); } } diff --git a/lib/app/pages/orders/orders_view_model.dart b/lib/app/pages/orders/orders_view_model.dart index e2dd82c..75cbe52 100644 --- a/lib/app/pages/orders/orders_view_model.dart +++ b/lib/app/pages/orders/orders_view_model.dart @@ -17,7 +17,8 @@ class OrdersViewModel extends PageViewModel { Future loadData() async { emit(state.copyWith( status: OrdersStateStatus.dataLoaded, - ordersWithLines: await app.storage.ordersDao.getOrdersWithLines() + ordersWithLines: await app.storage.ordersDao.getOrdersWithLines(), + cameraEnabled: await Misc.hasCamera() )); } diff --git a/lib/app/pages/person/person_page.dart b/lib/app/pages/person/person_page.dart index 30bfce3..3924d0d 100644 --- a/lib/app/pages/person/person_page.dart +++ b/lib/app/pages/person/person_page.dart @@ -39,7 +39,7 @@ class _PersonViewState extends State<_PersonView> { Completer _dialogCompleter = Completer(); Future openDialog() async { - showDialog( + showDialog( context: context, builder: (_) => const Center(child: CircularProgressIndicator()), barrierDismissible: false diff --git a/lib/app/pages/shared/page_view_model.dart b/lib/app/pages/shared/page_view_model.dart index edc3c50..dc20a78 100644 --- a/lib/app/pages/shared/page_view_model.dart +++ b/lib/app/pages/shared/page_view_model.dart @@ -13,8 +13,6 @@ abstract class PageViewModel extends Cubit { late final App app; final BuildContext context; - bool closed = false; - PageViewModel(this.context, T state) : super(state) { initViewModel(); } @@ -26,7 +24,10 @@ abstract class PageViewModel extends Cubit { @mustCallSuper Future initViewModel() async { app = await App.init(); - _subscription = app.storage.tableUpdates(listenForTables).listen((event) => loadData()); + _subscription = app.storage.tableUpdates(listenForTables).listen((event) async { + await Future.delayed(Duration.zero); + await loadData(); + }); await loadData(); } @@ -42,13 +43,12 @@ abstract class PageViewModel extends Cubit { text: status.runtimeType.toString() ); - if (!closed) super.emit(state); + if (!isClosed) super.emit(state); } @override Future close() async { _subscription.cancel(); - closed = true; super.close(); } diff --git a/lib/app/utils/misc.dart b/lib/app/utils/misc.dart index d3d8e03..2f20d42 100644 --- a/lib/app/utils/misc.dart +++ b/lib/app/utils/misc.dart @@ -1,3 +1,4 @@ +import 'package:camera/camera.dart'; import 'package:stack_trace/stack_trace.dart'; class Misc { @@ -18,4 +19,10 @@ class Misc { 'methodName': '', }; } + + static Future hasCamera() async { + List cameras = await availableCameras(); + + return cameras.isNotEmpty; + } }