feat: Improves data synchronization and connectivity UI
Implements automatic synchronization of unsynced local reception data (move line quantities) to the backend before fetching new receptions. This ensures local changes are pushed when connectivity is available. Adds a pull-to-refresh mechanism on the reception page, allowing users to manually refresh the list of receptions. Integrates an offline indicator in the main app bar, providing immediate visual feedback on the application's network connectivity status.
This commit is contained in:
parent
18f74daae4
commit
db71578ded
@ -1,5 +1,7 @@
|
|||||||
|
import 'package:e_scan/services/connectivity_service.dart';
|
||||||
import 'package:e_scan/themes/app_theme.dart';
|
import 'package:e_scan/themes/app_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class MainAppbarComponent extends StatelessWidget
|
class MainAppbarComponent extends StatelessWidget
|
||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
@ -52,7 +54,26 @@ class MainAppbarComponent extends StatelessWidget
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: actions,
|
actions: [
|
||||||
|
...actions ?? [],
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final isConnected = ref.watch(isConnectedProvider);
|
||||||
|
if (isConnected) {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.cloud_off, color: Colors.white),
|
||||||
|
Text('Hors ligne', style: TextStyle(color: Colors.white)),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
toolbarHeight: 60,
|
toolbarHeight: 60,
|
||||||
backgroundColor: AppTheme.of(context).primary,
|
backgroundColor: AppTheme.of(context).primary,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
|
@ -35,76 +35,88 @@ class _ReceptionPageState extends ConsumerState<ReceptionPage> {
|
|||||||
title: "Réceptions",
|
title: "Réceptions",
|
||||||
subTitle: "Opérations d'Entrepôt",
|
subTitle: "Opérations d'Entrepôt",
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: RefreshIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
color: AppTheme.of(context).white,
|
||||||
child: ListView(
|
backgroundColor: AppTheme.of(context).primary,
|
||||||
children: [
|
onRefresh: () async {
|
||||||
const SizedBox(height: 16),
|
await ref
|
||||||
QuickActionComponent(
|
.read(receptionPageModelProvider.notifier)
|
||||||
onTapAdd: () {},
|
.getAllReceptions();
|
||||||
onTapScan: () {},
|
},
|
||||||
onTapSearch: () {},
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
const SizedBox(height: 16),
|
child: ListView(
|
||||||
Consumer(
|
children: [
|
||||||
builder: (_, WidgetRef ref, _) {
|
const SizedBox(height: 16),
|
||||||
final state = ref.watch(receptionPageModelProvider);
|
QuickActionComponent(
|
||||||
if (state.loadingReceptions) {
|
onTapAdd: () {},
|
||||||
return Center(child: LoadingProgressComponent());
|
onTapScan: () {},
|
||||||
}
|
onTapSearch: () {},
|
||||||
return Column(
|
),
|
||||||
spacing: 10,
|
const SizedBox(height: 16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Consumer(
|
||||||
children: [
|
builder: (_, WidgetRef ref, _) {
|
||||||
// Text(
|
final state = ref.watch(receptionPageModelProvider);
|
||||||
// 'Réceptions',
|
if (state.loadingReceptions) {
|
||||||
// style: AppTheme.of(
|
return Center(child: LoadingProgressComponent());
|
||||||
// context,
|
}
|
||||||
// ).bodyMedium.copyWith(fontWeight: FontWeight.bold),
|
return Column(
|
||||||
// ),
|
spacing: 10,
|
||||||
state.receptions.isEmpty == true
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? Text('No data')
|
children: [
|
||||||
: ListView.builder(
|
// Text(
|
||||||
physics: NeverScrollableScrollPhysics(),
|
// 'Réceptions',
|
||||||
primary: true,
|
// style: AppTheme.of(
|
||||||
shrinkWrap: true,
|
// context,
|
||||||
itemCount: state.receptions.length,
|
// ).bodyMedium.copyWith(fontWeight: FontWeight.bold),
|
||||||
itemBuilder: (context, index) {
|
// ),
|
||||||
final reception = state.receptions[index];
|
state.receptions.isEmpty == true
|
||||||
return GestureDetector(
|
? Text('No data')
|
||||||
onTap: () {
|
: ListView.builder(
|
||||||
final id = reception.id;
|
physics: NeverScrollableScrollPhysics(),
|
||||||
ReceptionDetailsRoute(
|
primary: true,
|
||||||
receptionId: id,
|
shrinkWrap: true,
|
||||||
).push(context);
|
itemCount: state.receptions.length,
|
||||||
},
|
itemBuilder: (context, index) {
|
||||||
child: StockPickingCard(
|
final reception = state.receptions[index];
|
||||||
synchronized: reception.synchronized == true,
|
return GestureDetector(
|
||||||
margin: EdgeInsets.symmetric(
|
onTap: () {
|
||||||
horizontal: 5,
|
final id = reception.id;
|
||||||
vertical: 5,
|
ReceptionDetailsRoute(
|
||||||
|
receptionId: id,
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
child: StockPickingCard(
|
||||||
|
synchronized:
|
||||||
|
reception.synchronized == true,
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: 5,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
isDone: reception.isDone == true,
|
||||||
|
reference: reception.name ?? '',
|
||||||
|
from: reception
|
||||||
|
.locationId
|
||||||
|
.target
|
||||||
|
?.completeName,
|
||||||
|
to: reception
|
||||||
|
.locationDestId
|
||||||
|
.target
|
||||||
|
?.completeName,
|
||||||
|
contact:
|
||||||
|
reception.partnerId.target?.displayName,
|
||||||
|
origin: reception.origin,
|
||||||
|
status: reception.state,
|
||||||
),
|
),
|
||||||
isDone: reception.isDone == true,
|
);
|
||||||
reference: reception.name ?? '',
|
},
|
||||||
from:
|
),
|
||||||
reception.locationId.target?.completeName,
|
],
|
||||||
to: reception
|
);
|
||||||
.locationDestId
|
},
|
||||||
.target
|
),
|
||||||
?.completeName,
|
],
|
||||||
contact:
|
),
|
||||||
reception.partnerId.target?.displayName,
|
|
||||||
origin: reception.origin,
|
|
||||||
status: reception.state,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:e_scan/backend/api/api_calls.dart';
|
import 'package:e_scan/backend/api/api_calls.dart';
|
||||||
import 'package:e_scan/backend/objectbox/entities/stock_picking/stock_picking_record_entity.dart';
|
import 'package:e_scan/backend/objectbox/entities/stock_picking/stock_picking_record_entity.dart';
|
||||||
|
import 'package:e_scan/backend/objectbox/objectbox_manager.dart';
|
||||||
|
import 'package:e_scan/backend/schema/stock_picking/update_move_line_dto.dart';
|
||||||
import 'package:e_scan/backend/schema/user/user_struct.dart';
|
import 'package:e_scan/backend/schema/user/user_struct.dart';
|
||||||
import 'package:e_scan/services/secure_storage.dart';
|
import 'package:e_scan/services/secure_storage.dart';
|
||||||
import 'package:e_scan/services/token_provider.dart';
|
import 'package:e_scan/services/token_provider.dart';
|
||||||
@ -38,7 +40,57 @@ class ReceptionPageModel extends StateNotifier<ReceptionPageState> {
|
|||||||
state = state.copyWith(user: user, loadingUser: false);
|
state = state.copyWith(user: user, loadingUser: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future save({
|
||||||
|
required int receptionId,
|
||||||
|
VoidCallback? onSuccess,
|
||||||
|
VoidCallback? onError,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final stockPickingRecords = objectboxManager.store
|
||||||
|
.box<StockPickingRecordEntity>();
|
||||||
|
final stockPikingEntity = stockPickingRecords.get(receptionId);
|
||||||
|
final moveLinesDto =
|
||||||
|
stockPikingEntity?.moveLineIdsWithoutPackage
|
||||||
|
.map(
|
||||||
|
(m) => UpdateMoveLineDto(
|
||||||
|
operation: 1,
|
||||||
|
moveLineId: m.id,
|
||||||
|
values: {"quantity": m.quantity},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
final res = await ApiCalls.updateAllMoveLineOnStockPicking(
|
||||||
|
stockPickingId: receptionId,
|
||||||
|
moveLineDto: moveLinesDto,
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
stockPikingEntity?.synchronized = true;
|
||||||
|
stockPickingRecords.put(stockPikingEntity!);
|
||||||
|
onSuccess?.call();
|
||||||
|
} else {
|
||||||
|
onError?.call();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
onError?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> synchroAllData() async {
|
||||||
|
final stockPickingRecords = objectboxManager.store
|
||||||
|
.box<StockPickingRecordEntity>();
|
||||||
|
final records = stockPickingRecords
|
||||||
|
.query(StockPickingRecordEntity_.synchronized.equals(false))
|
||||||
|
.build()
|
||||||
|
.find();
|
||||||
|
for (var rec in records) {
|
||||||
|
await save(receptionId: rec.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future getAllReceptions() async {
|
Future getAllReceptions() async {
|
||||||
|
// sync all data first
|
||||||
|
await synchroAllData();
|
||||||
try {
|
try {
|
||||||
state = state.copyWith(loadingReceptions: true);
|
state = state.copyWith(loadingReceptions: true);
|
||||||
final res = await ApiCalls.getAllStockPiking();
|
final res = await ApiCalls.getAllStockPiking();
|
||||||
|
37
lib/services/connectivity_service.dart
Normal file
37
lib/services/connectivity_service.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
/// Provider qui retourne un booléen: connecté ou non
|
||||||
|
final isConnectedProvider =
|
||||||
|
StateNotifierProvider<ConnectivityBoolNotifier, bool>(
|
||||||
|
(ref) => ConnectivityBoolNotifier(),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ConnectivityBoolNotifier extends StateNotifier<bool> {
|
||||||
|
ConnectivityBoolNotifier() : super(false) {
|
||||||
|
_init();
|
||||||
|
_subscription = Connectivity().onConnectivityChanged.listen(
|
||||||
|
(results) => state = _isConnected(results),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
late final StreamSubscription<List<ConnectivityResult>> _subscription;
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
final result = await Connectivity().checkConnectivity();
|
||||||
|
state = _isConnected(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isConnected(List<ConnectivityResult> result) {
|
||||||
|
return (result).contains(ConnectivityResult.mobile) ||
|
||||||
|
(result).contains(ConnectivityResult.wifi) ||
|
||||||
|
(result).contains(ConnectivityResult.vpn) ||
|
||||||
|
(result).contains(ConnectivityResult.ethernet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user