refactor: visit synchronisation

release/mobdr-v0.0.1
Frédérik Benoist 2023-06-03 10:38:34 +02:00
parent f2f3bf22fa
commit a42c0847b8
10 changed files with 103 additions and 615 deletions

View File

@ -1,3 +0,0 @@
export 'example_bloc.dart';
export 'example_event.dart';
export 'example_state.dart';

View File

@ -1,38 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:mobdr/network/api_provider.dart';
import './bloc.dart';
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
ExampleBloc() : super(InitialExampleState()) {
on<GetExample>(_getExample);
on<PostExample>(_postExample);
}
}
void _getExample(GetExample event, Emitter<ExampleState> emit) async {
ApiProvider _apiProvider = ApiProvider();
emit(ExampleWaiting());
try {
String data = await _apiProvider.getExample(event.apiToken);
emit(GetExampleSuccess(exampleData: data));
} catch (ex) {
if (ex != 'cancel') {
emit(ExampleError(errorMessage: ex.toString()));
}
}
}
void _postExample(PostExample event, Emitter<ExampleState> emit) async {
ApiProvider _apiProvider = ApiProvider();
emit(ExampleWaiting());
try {
String data = await _apiProvider.postExample(event.id, event.apiToken);
emit(PostExampleSuccess(exampleData: data));
} catch (ex) {
if (ex != 'cancel') {
emit(ExampleError(errorMessage: ex.toString()));
}
}
}

View File

@ -1,15 +0,0 @@
import 'package:meta/meta.dart';
@immutable
abstract class ExampleEvent {}
class GetExample extends ExampleEvent {
final apiToken;
GetExample({@required this.apiToken});
}
class PostExample extends ExampleEvent {
final String id;
final apiToken;
PostExample({required this.id, required this.apiToken});
}

View File

@ -1,26 +0,0 @@
import 'package:meta/meta.dart';
@immutable
abstract class ExampleState {}
class InitialExampleState extends ExampleState {}
class ExampleError extends ExampleState {
final String errorMessage;
ExampleError({
required this.errorMessage,
});
}
class ExampleWaiting extends ExampleState {}
class GetExampleSuccess extends ExampleState {
final String exampleData;
GetExampleSuccess({required this.exampleData});
}
class PostExampleSuccess extends ExampleState {
final String exampleData;
PostExampleSuccess({required this.exampleData});
}

View File

@ -71,7 +71,9 @@ Future<void> main() async {
Wakelock.enable();
eventBus.on().listen((event) {
LoggerUtil.logVerbose('${DateTime.now()} Event: $event');
if (!(event is EmptyEvent)) {
LoggerUtil.logNStackInfo('${DateTime.now()} Event: $event');
}
});
runApp(MyApp());

View File

@ -54,8 +54,8 @@ class VisitModel {
}
static Future<List<VisitModel>> getPreviousVisits() async {
// Retrieve all previsous visits from the database
final visits = await objectbox.getPreviousVisit();
// Retrieve all previous visits from the database
final visits = await objectbox.getActivePreviousVisit();
// Map each retrieved visit to VisiteModel
final visitModelList = visits

View File

@ -1,8 +1,3 @@
/*
This is api provider
This page is used to get data from API
*/
import 'dart:io';
import 'package:dio/dio.dart';
@ -11,10 +6,12 @@ import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:intl/intl.dart';
import 'package:mobdr/config/constant.dart';
import 'package:mobdr/main.dart';
import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/service/logger_util.dart';
class ApiProvider {
Dio dio = Dio();
@ -210,6 +207,12 @@ class ApiProvider {
/// Synchronize all informations about store, competitor, calendar
Future<String> SyncCalendar() async {
DateTime now = DateTime.now();
DateTime end = now.add(Duration(days: 7));
String formattedStartDate = DateFormat('yyyyMMdd').format(now);
String formattedEndDate = DateFormat('yyyyMMdd').format(end);
try {
final responseFutures = Future.wait([
getCrud(
@ -231,7 +234,11 @@ class ApiProvider {
ApiConstants.externalEndpoint +
ApiConstants.restEndpoint +
'/mobDR/visite/calendrier',
{"id_utilisateur": 6, "start": 20230101, "end": 20230531},
{
"id_utilisateur": SharedPrefs().id_utilisateur,
"start": formattedStartDate,
"end": formattedEndDate
},
),
getCrud(
ApiConstants.baseUrl +
@ -313,38 +320,6 @@ class ApiProvider {
}
}
/// Synchronize "Photos"
Future<String> SyncPhotos() async {
Response response;
try {
/// get "Photo typologies"
response = await getCrud(
ApiConstants.baseUrl +
ApiConstants.externalEndpoint +
ApiConstants.restEndpoint +
'/mobDR/visite/typologie',
null);
if (response.statusCode == STATUS_OK) {
// remove all objects
objectbox.PhotoTypologyBox.removeAll();
/// fill box "Photo typologies"
objectbox.addPhotoTypologies(response.data['typologies']);
}
/// all ok
if (response.statusCode == STATUS_OK) {
return 'OK';
} else {
return response.statusMessage ?? 'Unknow error ...';
}
} catch (ex) {
return ex.toString(); // return ex.response!.data;
}
}
Future<int> uploadPhotoServlet(int id_visite, String photoPath) async {
try {
final url = Uri.parse(SERVLET_API);
@ -476,161 +451,13 @@ class ApiProvider {
}
}
Future<String> getExample(apiToken) async {
Response response;
response =
await getConnect(ApiConstants.baseUrl + '/example/getData', apiToken);
print('res : ' + response.toString());
return response.data.toString();
}
Future<String> postExample(String id, apiToken) async {
Response response;
var postData = {'id': id};
response = await postConnect(
ApiConstants.baseUrl + '/example/postData', postData, apiToken);
print('res : ' + response.toString());
return response.data.toString();
}
/*
Future<List<StudentModel>> getStudent(String sessionId, apiToken) async {
var postData = {'session_id': sessionId};
response = await postConnect(
ApiConstants.baseUrl + '/student/getStudent', postData, apiToken);
if (response.data['status'] == STATUS_OK) {
List responseList = response.data['data'];
List<StudentModel> listData =
responseList.map((f) => StudentModel.fromJson(f)).toList();
return listData;
} else {
throw response.data['msg'];
/// Synchronize all informations about store, competitor, calendar
Future<String> SyncLog() async {
try {
LoggerUtil.logNStacktackDebug("Synchronisation LOG à implementer !!");
return 'OK';
} catch (ex) {
return ex.toString();
}
}
*/
Future<List<dynamic>> addStudent(
String sessionId,
String studentName,
String studentPhoneNumber,
String studentGender,
String studentAddress,
apiToken) async {
var postData = {
'session_id': sessionId,
'student_name': studentName,
'student_phone_number': studentPhoneNumber,
'student_gender': studentGender,
'student_address': studentAddress,
};
Response response;
response = await postConnect(
ApiConstants.baseUrl + '/student/addStudent', postData, apiToken);
if (response.data['status'] == STATUS_OK) {
List<dynamic> respList = [];
respList.add(response.data['msg']);
respList.add(response.data['data']['id']);
return respList;
} else {
throw response.data['msg'];
}
}
Future<String> editStudent(
String sessionId,
int studentId,
String studentName,
String studentPhoneNumber,
String studentGender,
String studentAddress,
apiToken) async {
var postData = {
'session_id': sessionId,
'student_id': studentId,
'student_name': studentName,
'student_phone_number': studentPhoneNumber,
'student_gender': studentGender,
'student_address': studentAddress,
};
Response response;
response = await postConnect(
ApiConstants.baseUrl + '/student/editStudent', postData, apiToken);
if (response.data['status'] == STATUS_OK) {
return response.data['msg'];
} else {
throw response.data['msg'];
}
}
Future<String> deleteStudent(
String sessionId, int studentId, apiToken) async {
var postData = {
'session_id': sessionId,
'student_id': studentId,
};
Response response;
response = await postConnect(
ApiConstants.baseUrl + '/student/deleteStudent', postData, apiToken);
if (response.data['status'] == STATUS_OK) {
return response.data['msg'];
} else {
throw response.data['msg'];
}
}
/*
Future<List<LoginModel>> login2(
String email, String password, apiToken) async {
var postData = {
'email': email,
'password': password,
};
response = await postConnect(LOGIN_API, postData, apiToken);
if (response.data['status'] == STATUS_OK) {
List responseList = response.data['data'];
List<LoginModel> listData =
responseList.map((f) => LoginModel.fromJson(f)).toList();
return listData;
} else {
throw response.data['msg'];
}
}
*/
/*
Future<List<ProductGridModel>> getProductGrid(
String sessionId, String skip, String limit, apiToken) async {
var postData = {'session_id': sessionId, 'skip': skip, 'limit': limit};
response = await postConnect(PRODUCT_API, postData, apiToken);
if (response.data['status'] == STATUS_OK) {
List responseList = response.data['data'];
//print('data : '+responseList.toString());
List<ProductGridModel> listData =
responseList.map((f) => ProductGridModel.fromJson(f)).toList();
return listData;
} else {
throw response.data['msg'];
}
}
*/
/*
Future<List<ProductListviewModel>> getProductListview(
String sessionId, String skip, String limit, apiToken) async {
var postData = {'session_id': sessionId, 'skip': skip, 'limit': limit};
response = await postConnect(PRODUCT_API, postData, apiToken);
if (response.data['status'] == STATUS_OK) {
List responseList = response.data['data'];
//print('data : '+responseList.toString());
List<ProductListviewModel> listData =
responseList.map((f) => ProductListviewModel.fromJson(f)).toList();
return listData;
} else {
throw response.data['msg'];
}
}
*/
}

View File

@ -7,6 +7,8 @@ import 'package:mobdr/db/box_visit_tag.dart';
import 'package:mobdr/db/box_visit_photo.dart';
import 'package:mobdr/db/box_photo_typology.dart';
import 'package:mobdr/service/logger_util.dart';
import 'model.dart';
import 'objectbox.g.dart'; // created by `flutter pub run build_runner build`
@ -270,7 +272,7 @@ class ObjectBox {
return builder.find();
}
List<Visit> getPreviousVisit() {
List<Visit> getActivePreviousVisit() {
// Get the previous date at midnight.
final now = DateTime.now();
final midnight = DateTime(now.year, now.month, now.day);
@ -291,6 +293,24 @@ class ObjectBox {
return builder.find();
}
List<Visit> getPreviousVisit() {
// Get the previous date at midnight.
final now = DateTime.now();
final midnight = DateTime(now.year, now.month, now.day);
// Convert the date to an integer.
final millisecondsSinceEpoch = midnight.millisecondsSinceEpoch;
// Query for all visits that match the date criteria, sorted by their date.
final builder = visitBox
.query(Visit_.date_visite.lessThan(millisecondsSinceEpoch))
.order(Visit_.date_visite, flags: Order.descending)
.build();
// Execute the query and return the result.
return builder.find();
}
// A function that converts a response body list into a List<Visite>.
List<Visit> parseVisit(List responseDataList) {
final parsed = responseDataList.cast<Map<String, dynamic>>();
@ -372,6 +392,33 @@ class ObjectBox {
});
}
Future<void> DeletePreviousVisitWithoutPhoto() async {
final visits = getPreviousVisit();
for (final visit in visits) {
final photoCount = getVisitPhotoCount(visit.id_visite);
if (photoCount == 0) {
// Delete the visit
delVisitById(visit.id);
}
}
}
/// Removes a Visito object from the ObjectBox database with the specified ID.
///
/// Parameters:
/// id: The ID of the Visit object to remove.
///
/// Returns:
/// None.
void delVisitById(int id) {
if (visitBox.remove(id)) {
LoggerUtil.logNStacktackDebug("delete visit:${id}");
} else
LoggerUtil.logNStackError("delete visit:${id} KO");
}
int getVisitCount() {
return visitBox.count();
}

View File

@ -1,319 +0,0 @@
import 'package:flutter/material.dart';
import 'package:timelines/timelines.dart';
import 'dart:math';
import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/ui/reusable/reusable_widget.dart';
import 'package:mobdr/network/api_provider.dart';
import 'package:mobdr/main.dart';
const completeColor = Color(0xff5e6172);
const inProgressColor = Color(0xff5ec792);
const todoColor = Color(0xffd1d2d7);
const failedColor = Colors.red;
class TabSyncPage extends StatefulWidget {
@override
_TabSyncPageState createState() => _TabSyncPageState();
}
class _TabSyncPageState extends State<TabSyncPage>
with AutomaticKeepAliveClientMixin {
// initialize global function and reusable widget
//final _globalFunction = GlobalFunction();
final _reusableWidget = ReusableWidget();
final _processes = ['Btqs', 'Params', 'Visites', 'Photos', 'Logs'];
final ApiProvider _apiProvider =
ApiProvider(); // TODO: A voir si bien positionné
// _listKey is used for AnimatedList
//final GlobalKey<AnimatedListState> _listKey = GlobalKey();
int _processIndex = 1;
Color getColor(int index) {
if (index == _processIndex) {
return inProgressColor;
} else if (index < _processIndex) {
return completeColor;
} else {
return todoColor;
}
}
// keep the state to do not refresh when switch navbar
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
// if we used AutomaticKeepAliveClientMixin, we must call super.build(context);
super.build(context);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
iconTheme: IconThemeData(
color: GlobalStyle.appBarIconThemeColor,
),
elevation: GlobalStyle.appBarElevation,
title: Text(
'Synchronisation',
style: GlobalStyle.appBarTitle,
),
backgroundColor: GlobalStyle.appBarBackgroundColor,
systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle,
bottom: _reusableWidget.bottomAppBar(),
),
body: Timeline.tileBuilder(
theme: TimelineThemeData(
direction: Axis.horizontal,
connectorTheme: ConnectorThemeData(
space: 30.0,
thickness: 5.0,
),
),
builder: TimelineTileBuilder.connected(
connectionDirection: ConnectionDirection.before,
itemExtentBuilder: (_, __) =>
MediaQuery.of(context).size.width / _processes.length,
oppositeContentsBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 15.0),
child: Image.asset(
'assets/images/process_timeline/status${index + 1}.png',
width: 50.0,
color: getColor(index),
),
);
},
contentsBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(top: 15.0),
child: Text(
_processes[index],
style: TextStyle(
fontWeight: FontWeight.bold,
color: getColor(index),
),
),
);
},
indicatorBuilder: (_, index) {
var color;
var child;
if (index == _processIndex) {
color = inProgressColor;
child = Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(
strokeWidth: 3.0,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
);
} else if (index < _processIndex) {
color = failedColor;
child = Icon(
Icons.cancel,
color: Colors.white,
size: 20.0,
);
} else {
color = todoColor;
}
if (index <= _processIndex) {
return Stack(
children: [
CustomPaint(
size: Size(30.0, 30.0),
painter: _BezierPainter(
color: color,
drawStart: index > 0,
drawEnd: index < _processIndex,
),
),
DotIndicator(
size: 30.0,
color: color,
child: child,
),
],
);
} else {
return Stack(
children: [
CustomPaint(
size: Size(15.0, 15.0),
painter: _BezierPainter(
color: color,
drawEnd: index < _processes.length - 1,
),
),
OutlinedDotIndicator(
borderWidth: 4.0,
color: color,
),
],
);
}
},
connectorBuilder: (_, index, type) {
if (index > 0) {
if (index == _processIndex) {
final prevColor = getColor(index - 1);
final color = getColor(index);
List<Color> gradientColors;
if (type == ConnectorType.start) {
gradientColors = [Color.lerp(prevColor, color, 0.5)!, color];
} else {
gradientColors = [
prevColor,
Color.lerp(prevColor, color, 0.5)!
];
}
return DecoratedLineConnector(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
),
),
);
} else {
return SolidLineConnector(
color: getColor(index),
);
}
} else {
return null;
}
},
itemCount: _processes.length,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.chevron_right),
onPressed: () async {
var futures = [
//_apiProvider.SyncEtablissements(),
//_apiProvider.SyncVisites(),
_apiProvider.SyncPhotos(),
];
objectbox.etabBox.removeAll();
objectbox.etabCompetitorBox.removeAll();
objectbox.visitBox.removeAll();
objectbox.visitTagBox.removeAll();
objectbox.visitPhotoBox.removeAll();
objectbox.PhotoTypologyBox.removeAll();
var results = await Future.wait(futures);
if (results[0] == 'OK') {
print("SyncEtablissements OK");
} else {
print("SyncEtablissements Error:" + results[0]);
}
if (results[1] == 'OK') {
print("SyncVisites OK");
} else {
print("SyncVisites Error:" + results[1]);
}
if (results[0] == 'OK') {
print("SyncPhotos OK");
} else {
print("SyncPhotos Error:" + results[0]);
}
///
setState(() {
_processIndex = (_processIndex + 1) % _processes.length;
});
},
backgroundColor: inProgressColor,
),
);
}
}
/// hardcoded bezier painter
class _BezierPainter extends CustomPainter {
const _BezierPainter({
required this.color,
this.drawStart = true,
this.drawEnd = true,
});
final Color color;
final bool drawStart;
final bool drawEnd;
Offset _offset(double radius, double angle) {
return Offset(
radius * cos(angle) + radius,
radius * sin(angle) + radius,
);
}
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.fill
..color = color;
final radius = size.width / 2;
var angle;
var offset1;
var offset2;
var path;
if (drawStart) {
angle = 3 * pi / 4;
offset1 = _offset(radius, angle);
offset2 = _offset(radius, -angle);
path = Path()
..moveTo(offset1.dx, offset1.dy)
..quadraticBezierTo(0.0, size.height / 2, -radius, radius)
..quadraticBezierTo(0.0, size.height / 2, offset2.dx, offset2.dy)
..close();
canvas.drawPath(path, paint);
}
if (drawEnd) {
angle = -pi / 4;
offset1 = _offset(radius, angle);
offset2 = _offset(radius, -angle);
path = Path()
..moveTo(offset1.dx, offset1.dy)
..quadraticBezierTo(
size.width, size.height / 2, size.width + radius, radius)
..quadraticBezierTo(size.width, size.height / 2, offset2.dx, offset2.dy)
..close();
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(_BezierPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.drawStart != drawStart ||
oldDelegate.drawEnd != drawEnd;
}
}

View File

@ -25,6 +25,7 @@ class _SynchronizationPageState extends State<SynchronizationPage>
bool _backofficeSyncCompleted = false;
bool _photosSyncCompleted = false;
bool _logSyncCompleted = false;
bool _syncLog = false;
@override
void initState() {
@ -134,16 +135,6 @@ class _SynchronizationPageState extends State<SynchronizationPage>
}
_photosSyncCompleted = (_totalUploaded == _visitPhotosList.length);
// Get unique id_visite values from _photosList
Set<int> uniqueIds =
_visitPhotosList.map((photo) => photo.id_visite).toSet();
// Send VisitPhotoCountEvent for each unique id_visite
for (int id_visite in uniqueIds) {
eventBus.fire(VisitPhotoCountEvent(
id_visite, objectbox.getVisitPhotoCount(id_visite)));
}
}
Future<void> _cleanVisitPhotoDir() async {
@ -221,8 +212,15 @@ class _SynchronizationPageState extends State<SynchronizationPage>
// upload photo to server
await _uploadVisitPhotos(_apiProvider);
// TODO:
// supprimer les visites "ancienne" sans photo !!
final syncLogResult = _syncLog ? await _apiProvider.SyncCalendar() : 'OK';
// log synchronization OK ?
if (syncLogResult == 'OK') {
_logSyncCompleted = true;
}
// delete visits without a photo!
await objectbox.DeletePreviousVisitWithoutPhoto();
// deletes photos that are no longer in any visits
await _cleanVisitPhotoDir();
@ -234,10 +232,7 @@ class _SynchronizationPageState extends State<SynchronizationPage>
SharedPrefs().lastCalendarRefresh =
DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
// TODO: je pense que comme il y a cet event (qui reload tout) on est plus obligé
// de lancer l'autre évent pour les count() de photo.
//
// de plus il faut supprimer les visites qui n'ont plus de photo !!!
// send global event to refresh calendar
eventBus.fire(RefreshCalendarEvent(SharedPrefs().lastCalendarRefresh));
setState(() {
@ -312,7 +307,25 @@ class _SynchronizationPageState extends State<SynchronizationPage>
),
textAlign: TextAlign.center,
),
SizedBox(height: 30),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'Sync Logs',
style: TextStyle(fontSize: 16),
),
Switch(
value: _syncLog,
onChanged: (value) {
setState(() {
_syncLog = value;
});
},
),
],
),
SizedBox(height: 20),
SyncItem(
icon: Icons.business,
title: 'Backoffice',