refactor:synchronization (step 1)

release/mobdr-v0.0.1
Frédérik Benoist 2023-05-22 21:20:10 +02:00
parent fe798b6ae2
commit 06b33e9ec6
14 changed files with 1159 additions and 491 deletions

BIN
assets/images/synchro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -178,7 +178,7 @@ class GlobalStyle {
TextStyle(fontSize: 18, fontWeight: FontWeight.bold);
static const TextStyle cardTitle =
TextStyle(fontSize: 14, color: Colors.black, fontWeight: FontWeight.bold);
TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold);
static const TextStyle textPromo = TextStyle(
fontSize: 12,

View File

@ -12,18 +12,21 @@ class VisitModel {
late String image;
late String type_visite;
late String langage;
late DateTime? date_validation;
VisitModel(
{required this.id,
required this.id_distrib,
required this.id_etab,
required this.id_visite,
required this.name,
required this.photoCount,
required this.date,
required this.image,
required this.type_visite,
required this.langage});
VisitModel({
required this.id,
required this.id_distrib,
required this.id_etab,
required this.id_visite,
required this.name,
required this.photoCount,
required this.date,
required this.image,
required this.type_visite,
required this.langage,
required this.date_validation,
});
static Future<List<VisitModel>> getTodayVisits() async {
// Retrieve all today visits from the database
@ -42,7 +45,8 @@ class VisitModel {
.format(visite.date_visite),
image: visite.url_photo_principale,
type_visite: visite.type_visite,
langage: visite.langage))
langage: visite.langage,
date_validation: visite.date_validation))
.toList();
// Return the list of VisiteModel
@ -66,16 +70,17 @@ class VisitModel {
.format(visite.date_visite),
image: visite.url_photo_principale,
type_visite: visite.type_visite,
langage: visite.langage))
langage: visite.langage,
date_validation: visite.date_validation))
.toList();
// Return the list of VisiteModel
return visitModelList;
}
static List<VisitModel> getToSyncVisits() {
static Future<List<VisitModel>> getToSyncVisits() async {
// Retrieve all visits from the database
final visits = objectbox.getAllVisit();
final visits = await objectbox.getAllVisit();
// Map each retrieved visit to VisiteModel
final visitModelList = visits
@ -90,7 +95,8 @@ class VisitModel {
.format(visite.date_visite),
image: visite.url_photo_principale,
type_visite: visite.type_visite,
langage: visite.langage))
langage: visite.langage,
date_validation: visite.date_validation))
.where((visit) =>
visit.photoCount >
0) // Filter out visits with photoCount equal to 0

View File

@ -78,9 +78,6 @@ class ObjectBox {
.map((query) => query.find());
}
static void _putNotesInTx(Store store, List<Note> notes) =>
store.box<Note>().putMany(notes);
/// Add a note within a transaction.
///
/// To avoid frame drops, run ObjectBox operations that take longer than a

View File

@ -6,7 +6,6 @@ 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';
import 'package:mobdr/ui/home/tab_home.dart';
const completeColor = Color(0xff5e6172);
const inProgressColor = Color(0xff5ec792);

View File

@ -77,38 +77,44 @@ class _HomePageState extends State<HomePage>
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
onTap: (value) {
_currentIndex = value;
_pageController.jumpToPage(value);
// this unfocus is to prevent show keyboard in the wishlist page when focus on search text field
FocusScope.of(context).unfocus();
setState(() {
_currentIndex = value;
_pageController.jumpToPage(value);
FocusScope.of(context).unfocus();
});
},
selectedFontSize: 8,
unselectedFontSize: 8,
iconSize: 28,
selectedItemColor:
Colors.blue, // Couleur de l'icône de l'onglet sélectionné
unselectedItemColor:
CHARCOAL, // Couleur de l'icône des onglets non sélectionnés
selectedLabelStyle: TextStyle(
color: _currentIndex == 1 ? ASSENT_COLOR : PRIMARY_COLOR,
fontWeight: FontWeight.bold),
unselectedLabelStyle:
TextStyle(color: CHARCOAL, fontWeight: FontWeight.bold),
selectedItemColor: _currentIndex == 1 ? ASSENT_COLOR : PRIMARY_COLOR,
unselectedItemColor: CHARCOAL,
color: Colors.blue, // Couleur du texte de l'onglet sélectionné
fontWeight: FontWeight.bold,
),
unselectedLabelStyle: TextStyle(
color: CHARCOAL, // Couleur du texte des onglets non sélectionnés
fontWeight: FontWeight.bold,
),
items: [
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home,
color: _currentIndex == 0 ? PRIMARY_COLOR : CHARCOAL)),
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Synchro',
icon: Icon(Icons.sync,
color: _currentIndex == 1 ? ASSENT_COLOR : CHARCOAL)),
label: 'Synchro',
icon: Icon(Icons.sync),
),
BottomNavigationBarItem(
label: 'MP4',
icon: Icon(Icons.web,
color: _currentIndex == 2 ? PRIMARY_COLOR : CHARCOAL)),
label: 'MP4',
icon: Icon(Icons.web),
),
BottomNavigationBarItem(
label: 'Account',
icon: Icon(Icons.person_outline,
color: _currentIndex == 3 ? PRIMARY_COLOR : CHARCOAL)),
label: 'Account',
icon: Icon(Icons.person_outline),
),
],
),
);

View File

@ -18,7 +18,7 @@ import 'package:mobdr/cubit/language/app_localizations.dart';
import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/events.dart';
import 'package:mobdr/ui/sync/sync_calendar.dart';
import 'package:mobdr/ui/sync/synchronization.dart';
import 'package:mobdr/model/visit_model.dart';
import 'package:mobdr/ui/visit/visit_photo_typology.dart';
import 'package:mobdr/ui/general/chat_us.dart';
@ -188,7 +188,7 @@ class _TabHomePageState extends State<TabHomePage>
return Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: EdgeInsets.fromLTRB(16, 8, 0, 0),
child: Row(
children: [
Text('Last visit access', style: GlobalStyle.horizontalTitle),
@ -197,10 +197,9 @@ class _TabHomePageState extends State<TabHomePage>
),
Container(
margin: EdgeInsets.only(top: 8),
height: boxImageSize * GlobalStyle.cardHeightMultiplication,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 12),
child: buildHorizontalVisitListCard(context, lastVisitData)),
child: buildVisitListCard(context, lastVisitData!)),
],
);
}
@ -238,13 +237,12 @@ class _TabHomePageState extends State<TabHomePage>
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 16), // Ajout de l'espace ici
Text('Aucune visite ce jour'),
ElevatedButton(
TextButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SyncCalendarPage()),
MaterialPageRoute(
builder: (context) => SynchronizationPage()),
);
// Refresh the widget if the synchronization was successful.
@ -254,7 +252,20 @@ class _TabHomePageState extends State<TabHomePage>
});
}
},
child: Text('Synchroniser'),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Synchronize',
style: TextStyle(
color: Colors.blue,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Icon(Icons.chevron_right, color: Colors.blue),
],
),
),
],
)
@ -308,7 +319,100 @@ class _TabHomePageState extends State<TabHomePage>
);
}
Widget buildHorizontalVisitListCard(context, data) {
Widget buildVisitListCard(BuildContext context, VisitModel data) {
final double boxImageSize = (MediaQuery.of(context).size.width / 4);
return GestureDetector(
onTap: () {
Route route = MaterialPageRoute(
builder: (context) => VisitPhotoTypologyPage(pp_visitModel: data),
);
Navigator.push(context, route);
},
child: Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 2,
color: Colors.white,
child: Container(
margin: EdgeInsets.all(8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: buildCacheNetworkImage(
width: boxImageSize,
height: boxImageSize,
url: data.image,
),
),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Text(data.name,
style: GlobalStyle.productPrice),
],
),
),
Container(height: 8),
Text(
data.date,
style: GlobalStyle.productSale,
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
'${data.photoCount} Photo(s)',
style: GlobalStyle.productPrice,
),
),
Container(height: 8),
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Icon(
Icons.store,
color: SOFT_GREY,
size: 20,
),
Text(
' ' + data.type_visite,
style: GlobalStyle.productName.copyWith(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
],
),
],
),
),
),
),
);
}
Widget buildHorizontalVisitListCard(context, VisitModel data) {
final double imageWidth = (MediaQuery.of(context).size.width / 2.3);
final double imageheight = (MediaQuery.of(context).size.width / 3.07);
@ -364,9 +468,15 @@ class _TabHomePageState extends State<TabHomePage>
right: 4, // alignement à droite
child: GestureDetector(
onTap: () {
// TODO si visite validée ce n'est pas la meme url (on il mettre date validation dans le json frederik
SharedPrefs().urlMP4 =
'${ApiConstants.baseUrl}/MobilePortal4/index.html#ajax/visite_modification.html?visite=${data.id_visite}';
// TODO: si visite validée ce n'est pas la meme url
if (data.date_validation == null) {
SharedPrefs().urlMP4 =
'${ApiConstants.baseUrl}/MobilePortal4/index.html#ajax/visite_modification.html?visite=${data.id_visite}';
} else {
SharedPrefs().urlMP4 =
'${ApiConstants.baseUrl}/MobilePortal4/index.html#ajax/visite_modification.html?visite=${data.id_visite}';
}
eventBus.fire(UrlEvent(SharedPrefs().urlMP4));
},
child: Image.asset(
@ -389,7 +499,10 @@ class _TabHomePageState extends State<TabHomePage>
),
badgeContent: Text(data.photoCount.toString(),
style: TextStyle(color: Colors.white)),
child: Icon(Icons.camera_alt_sharp),
child: Icon(
Icons.camera_alt_sharp,
color: Colors.grey,
),
),
),
),

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:mobdr/network/api_provider.dart';
class SyncCalendarPage extends StatefulWidget {
class SyncCalendarPage2 extends StatefulWidget {
@override
_SyncCalendarPageState createState() => _SyncCalendarPageState();
_SyncCalendarPage2State createState() => _SyncCalendarPage2State();
}
class _SyncCalendarPageState extends State<SyncCalendarPage> {
class _SyncCalendarPage2State extends State<SyncCalendarPage2> {
bool _isSyncing = false;
bool _syncSuccessful = false;
String? _syncErrorMessage;

View File

@ -0,0 +1,451 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:intl/intl.dart';
import 'package:mobdr/main.dart';
import 'package:mobdr/config/constant.dart';
import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/events.dart';
import 'package:mobdr/model/visit_model.dart';
import 'package:mobdr/ui/reusable/cache_image_network.dart';
import 'package:mobdr/ui/sync/upload_photos.dart';
import 'package:mobdr/ui/sync/check_connection.dart';
import 'package:mobdr/ui/sync/synchronization.dart';
class SynchronizationPage2 extends StatefulWidget {
@override
_SynchronizationPage2State createState() => _SynchronizationPage2State();
}
class _SynchronizationPage2State extends State<SynchronizationPage2> {
late List<VisitModel> tosyncVisitData = [];
bool isLoading = true;
// _listKey is used for AnimatedList
var _listKey = GlobalKey<AnimatedListState>();
@override
void initState() {
super.initState();
loadData();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final double boxImageSize = (MediaQuery.of(context).size.width / 4);
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(
color: GlobalStyle.appBarIconThemeColor,
),
elevation: GlobalStyle.appBarElevation,
title: Text(
'Data synchronization',
style: GlobalStyle.appBarTitle,
),
backgroundColor: GlobalStyle.appBarBackgroundColor,
systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle),
body: isLoading
? Center(
child: CircularProgressIndicator(),
)
: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Calendar', style: GlobalStyle.horizontalTitle),
Row(
children: [
Icon(Icons.refresh),
Text(SharedPrefs().lastCalendarRefresh.isNotEmpty
? SharedPrefs().lastCalendarRefresh
: "Never"),
],
),
],
),
),
SizedBox(height: 8), // Ajout de l'espace ici
TextButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
//builder: (context) => SyncCalendarPage()),
builder: (context) => SynchronizationPage2()),
);
// Refresh the widget if the synchronization was successful.
if (result == true) {
SharedPrefs().lastCalendarRefresh =
DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
eventBus.fire(RefreshCalendarEvent(
SharedPrefs().lastCalendarRefresh));
setState(() {
loadData();
});
} else
SharedPrefs().lastCalendarRefresh = "";
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Synchronize',
style: TextStyle(
color: Colors.blue,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Icon(Icons.chevron_right, color: Colors.blue),
],
),
),
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
children: [
Text('Visits', style: GlobalStyle.horizontalTitle),
],
),
),
if (tosyncVisitData.isEmpty)
Container(
padding: EdgeInsets.all(16),
child: Text("You didn't take any photos..."),
),
Flexible(
child: AnimatedList(
key: _listKey,
initialItemCount: tosyncVisitData.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index, animation) {
return Dismissible(
key: UniqueKey(),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
// the photo must be removed
setState(() {
tosyncVisitData.removeAt(index);
_listKey = GlobalKey<AnimatedListState>();
});
},
background: Container(
color: Colors.red,
child: Stack(
children: [
Positioned.fill(
child: Align(
alignment: Alignment.center,
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
],
),
),
child: _buildVisitlistCard(tosyncVisitData[index],
boxImageSize, animation, index),
);
},
),
),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(0.0, 1.0), //(x,y)
blurRadius: 2.0,
),
],
),
child: Row(
children: [
Container(
child: GestureDetector(
onTap: () {
// TODO filter functionality to be implemented (view deleted visits)
/*`
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatUsPage()));
*/
},
child: ClipOval(
child: Container(
color: SOFT_BLUE,
padding: EdgeInsets.all(9),
child: Icon(Icons.filter_list,
color: Colors.white, size: 16)),
),
),
),
SizedBox(
width: 10,
),
Expanded(
child: GestureDetector(
onTap: tosyncVisitData.isNotEmpty
? () {
navigateToPage(context, 0, 0);
}
: null,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(12, 8, 12, 8),
margin: EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
width: 1,
color: tosyncVisitData.isNotEmpty
? Colors.red
: Colors.grey),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
child: Text(
'Synchronize ALL visits',
style: TextStyle(
color: tosyncVisitData.isNotEmpty
? Colors.red
: Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
],
),
);
}
Widget _buildVisitlistCard(VisitModel data, boxImageSize, animation, index) {
return SizeTransition(
sizeFactor: animation,
child: Container(
margin: EdgeInsets.fromLTRB(12, 6, 12, 0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 2,
color: Colors.white,
child: Container(
margin: EdgeInsets.all(8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: buildCacheNetworkImage(
width: boxImageSize,
height: boxImageSize,
url: data.image,
),
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Text(data.name, style: GlobalStyle.productPrice)
],
),
),
Container(height: 8),
Text(
data.date,
style: GlobalStyle.productSale,
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
'${data.photoCount} Photo(s)',
style: GlobalStyle.productPrice,
),
),
Container(height: 8),
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Icon(
Icons.store,
color: SOFT_GREY,
size: 20,
),
Text(
' ' + data.type_visite,
style: GlobalStyle.productName.copyWith(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
],
),
),
],
),
)
],
),
Container(
margin: EdgeInsets.only(top: 12),
child: Row(
children: [
Expanded(
child: (data.photoCount == 0)
? TextButton(
style: ButtonStyle(
minimumSize:
MaterialStateProperty.all(Size(0, 30)),
backgroundColor:
MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) =>
Colors.grey[300]!,
),
overlayColor: MaterialStateProperty.all(
Colors.transparent),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
)),
),
onPressed: () {},
child: Text(
'Synchronize',
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.bold,
fontSize: 13),
textAlign: TextAlign.center,
))
: OutlinedButton(
onPressed: () {
navigateToPage(
context, index, data.id_visite);
},
style: ButtonStyle(
minimumSize:
MaterialStateProperty.all(Size(0, 30)),
overlayColor: MaterialStateProperty.all(
Colors.transparent),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
)),
side: MaterialStateProperty.all(
BorderSide(color: SOFT_BLUE, width: 1.0),
)),
child: Text(
'Synchronize',
style: TextStyle(
color: SOFT_BLUE,
fontWeight: FontWeight.bold,
fontSize: 13),
textAlign: TextAlign.center,
)),
),
],
),
)
],
),
),
),
),
);
}
Future<void> navigateToPage(
BuildContext context, int index, int id_visite) async {
var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckConnectionPage(
redirectPage: UploadPhotosPage(pp_id_visite: id_visite),
),
),
);
// the visit must be removed
/*
setState(() {
tosyncVisitData.removeAt(index);
_listKey = GlobalKey<AnimatedListState>();
});
*/
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UploadPhotosPage(pp_id_visite: id_visite)));
}
/*
setState(() {
isLoading = true; // Mettez à jour l'état ici
});
// remove all items
int itemCount = tosyncVisitData.length;
for (int i = itemCount - 1; i >= 0; i--) {
tosyncVisitData.removeAt(i);
_listKey.currentState!.removeItem(
i,
(context, animation) =>
Container()); // Remplacez Container() par le widget à animer lors de la suppression de l'élément
}
loadData();
*/
}
/// Initializes data when the page loads.
Future<void> loadData() async {
// initialization of data with all visits to be synchronized
tosyncVisitData = await VisitModel.getToSyncVisits();
setState(() {
isLoading = false;
});
}
}

View File

@ -1,400 +1,211 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:intl/intl.dart';
import 'package:mobdr/main.dart';
import 'package:mobdr/config/constant.dart';
import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/events.dart';
import 'package:mobdr/model/visit_model.dart';
import 'package:mobdr/ui/reusable/cache_image_network.dart';
import 'package:mobdr/ui/sync/upload_photos.dart';
import 'package:mobdr/ui/sync/check_connection.dart';
import 'package:mobdr/ui/sync/sync_calendar.dart';
class SynchronizationPage extends StatefulWidget {
@override
_SynchronizationPageState createState() => _SynchronizationPageState();
}
class _SynchronizationPageState extends State<SynchronizationPage> {
late List<VisitModel> tosyncVisitData = [];
// _listKey is used for AnimatedList
var _listKey = GlobalKey<AnimatedListState>();
@override
void initState() {
super.initState();
loadData();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final double boxImageSize = (MediaQuery.of(context).size.width / 4);
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(
color: GlobalStyle.appBarIconThemeColor,
),
elevation: GlobalStyle.appBarElevation,
title: Text(
'Data synchronization',
style: GlobalStyle.appBarTitle,
),
backgroundColor: GlobalStyle.appBarBackgroundColor,
systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle),
body: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Calendar', style: GlobalStyle.horizontalTitle),
Row(
children: [
Icon(Icons.refresh),
Text(SharedPrefs().lastCalendarRefresh.isNotEmpty
? SharedPrefs().lastCalendarRefresh
: "Never"),
],
),
],
),
),
SizedBox(height: 8), // Ajout de l'espace ici
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SyncCalendarPage()),
);
// Refresh the widget if the synchronization was successful.
if (result == true) {
SharedPrefs().lastCalendarRefresh =
DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
eventBus.fire(
RefreshCalendarEvent(SharedPrefs().lastCalendarRefresh));
setState(() {
loadData();
});
} else
SharedPrefs().lastCalendarRefresh = "";
},
child: Text('Synchroniser'),
),
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
children: [
Text('Visits', style: GlobalStyle.horizontalTitle),
],
),
),
if (tosyncVisitData.isEmpty)
Container(
padding: EdgeInsets.all(16),
child: Text("You didn't take any photos..."),
),
Flexible(
child: AnimatedList(
key: _listKey,
initialItemCount: tosyncVisitData.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index, animation) {
return Dismissible(
key: UniqueKey(),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
// the photo must be removed
setState(() {
tosyncVisitData.removeAt(index);
_listKey = GlobalKey<AnimatedListState>();
});
},
background: Container(
color: Colors.red,
child: Stack(
children: [
Positioned.fill(
child: Align(
alignment: Alignment.center,
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
],
),
),
child: _buildVisitlistCard(
tosyncVisitData[index], boxImageSize, animation, index),
);
},
),
),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(0.0, 1.0), //(x,y)
blurRadius: 2.0,
),
],
),
child: Row(
children: [
Container(
child: GestureDetector(
onTap: () {
// TODO functionality to be implemented
/*`
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatUsPage()));
*/
},
child: ClipOval(
child: Container(
color: SOFT_BLUE,
padding: EdgeInsets.all(9),
child: Icon(Icons.filter_list,
color: Colors.white, size: 16)),
),
),
),
SizedBox(
width: 10,
),
Expanded(
child: GestureDetector(
onTap: tosyncVisitData.isNotEmpty
? () {
navigateToPage(context, 0);
}
: null,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(12, 8, 12, 8),
margin: EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
width: 1,
color: tosyncVisitData.isNotEmpty
? Colors.red
: Colors.grey),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
child: Text(
'Synchronize ALL visits',
style: TextStyle(
color: tosyncVisitData.isNotEmpty
? Colors.red
: Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
],
),
);
}
Widget _buildVisitlistCard(VisitModel data, boxImageSize, animation, index) {
return SizeTransition(
sizeFactor: animation,
child: Container(
margin: EdgeInsets.fromLTRB(12, 6, 12, 0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 2,
color: Colors.white,
child: Container(
margin: EdgeInsets.all(8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: buildCacheNetworkImage(
width: boxImageSize,
height: boxImageSize,
url: data.image,
),
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Text(data.name, style: GlobalStyle.productPrice)
],
),
),
Container(height: 8),
Text(
data.date,
style: GlobalStyle.productSale,
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
'${data.photoCount} Photo(s)',
style: GlobalStyle.productPrice,
),
),
Container(height: 8),
Container(
margin: EdgeInsets.only(top: 5),
child: Row(
children: [
Icon(
Icons.store,
color: SOFT_GREY,
size: 20,
),
Text(
' ' + data.type_visite,
style: GlobalStyle.productName.copyWith(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
],
),
),
],
),
)
],
),
Container(
margin: EdgeInsets.only(top: 12),
child: Row(
children: [
Expanded(
child: (data.photoCount == 0)
? TextButton(
style: ButtonStyle(
minimumSize:
MaterialStateProperty.all(Size(0, 30)),
backgroundColor:
MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) =>
Colors.grey[300]!,
),
overlayColor: MaterialStateProperty.all(
Colors.transparent),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
)),
),
onPressed: () {},
child: Text(
'Synchronize',
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.bold,
fontSize: 13),
textAlign: TextAlign.center,
))
: OutlinedButton(
onPressed: () {
navigateToPage(context, data.id_visite);
},
style: ButtonStyle(
minimumSize:
MaterialStateProperty.all(Size(0, 30)),
overlayColor: MaterialStateProperty.all(
Colors.transparent),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
)),
side: MaterialStateProperty.all(
BorderSide(color: SOFT_BLUE, width: 1.0),
)),
child: Text(
'Synchronize',
style: TextStyle(
color: SOFT_BLUE,
fontWeight: FontWeight.bold,
fontSize: 13),
textAlign: TextAlign.center,
)),
),
],
),
)
],
),
),
),
),
);
}
Future<void> navigateToPage(BuildContext context, int id_visite) async {
var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CheckConnectionPage(
redirectPage: UploadPhotosPage(pp_id_visite: id_visite),
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UploadPhotosPage(pp_id_visite: id_visite)));
}
}
/// Initializes data when the page loads.
void loadData() {
// initialization of data with all visits to be synchronized
tosyncVisitData = VisitModel.getToSyncVisits();
}
}
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mobdr/service/shared_prefs.dart';
class SynchronizationPage extends StatefulWidget {
@override
_SynchronizationPageState createState() => _SynchronizationPageState();
}
class _SynchronizationPageState extends State<SynchronizationPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
bool _isSyncing = false;
bool _syncCompleted = false;
bool _backofficeSyncCompleted = false;
bool _photosSyncCompleted = false;
bool _logSyncCompleted = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
_rotationAnimation =
Tween<double>(begin: 0, end: 1).animate(_animationController);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void startSync() {
setState(() {
_isSyncing = true;
_syncCompleted = false;
_backofficeSyncCompleted = false;
_photosSyncCompleted = false;
_logSyncCompleted = false;
});
_animationController.repeat();
// Simulation d'une tâche de synchronisation
Future.delayed(const Duration(seconds: 3), () {
SharedPrefs().lastCalendarRefresh =
DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
setState(() {
_isSyncing = false;
_syncCompleted = true;
_backofficeSyncCompleted = true;
_photosSyncCompleted = false;
_logSyncCompleted = false;
});
_animationController.stop();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Data synchronization'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 20),
Center(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
child: IconButton(
icon: AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2.0 * 3.14,
child: Icon(
Icons.refresh,
color: Colors.white,
size: 80,
),
);
},
),
onPressed: _isSyncing ? null : startSync,
),
),
),
SizedBox(height: 20),
Text(
SharedPrefs().lastCalendarRefresh.isNotEmpty
? SharedPrefs().lastCalendarRefresh
: "Never",
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
SizedBox(height: 40),
SyncItem(
icon: Icons.business,
title: 'Backoffice',
description: 'Synchronisation des données du backoffice',
isSyncing: _isSyncing,
syncCompleted: _syncCompleted,
isError: _syncCompleted && !_backofficeSyncCompleted,
),
SizedBox(height: 20),
SyncItem(
icon: Icons.photo,
title: 'Photos',
description: 'Synchronisation des photos',
isSyncing: _isSyncing,
syncCompleted: _syncCompleted,
isError: _syncCompleted && !_photosSyncCompleted,
),
SizedBox(height: 20),
SyncItem(
icon: Icons.warning,
title: 'Log',
description: 'Synchronisation des journaux d\'activité',
isSyncing: _isSyncing,
syncCompleted: _syncCompleted,
isError: _syncCompleted && !_logSyncCompleted,
),
],
),
),
);
}
}
class SyncItem extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final bool isSyncing;
final bool syncCompleted;
final bool isError;
const SyncItem({
required this.icon,
required this.title,
required this.description,
required this.isSyncing,
required this.syncCompleted,
required this.isError,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: Colors.blue,
size: 24,
),
SizedBox(width: 10),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 10),
Text(description),
SizedBox(height: 10),
if (isSyncing)
LinearProgressIndicator(
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
if (syncCompleted)
Row(
children: [
Icon(
isError ? Icons.error_outline : Icons.check,
color: isError ? Colors.red : Colors.green,
size: 20,
),
SizedBox(width: 10),
Text(
isError
? 'Erreur de synchronisation'
: 'Synchronisation terminée',
style: TextStyle(
color: isError ? Colors.red : Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
);
}
}

View File

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/config/global_style.dart';
class SyncPage extends StatefulWidget {
@override
_SyncPageState createState() => _SyncPageState();
}
class _SyncPageState extends State<SyncPage>
with SingleTickerProviderStateMixin {
AnimationController? _animationController;
late Animation<double> _rotationAnimation;
bool _isSyncing = false;
bool _syncCompleted = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(_animationController!);
_animationController!.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
_syncCompleted = true;
});
}
});
// Lancement automatique de la synchronisation au démarrage
//startSync();
}
@override
void dispose() {
_animationController!.dispose();
super.dispose();
}
void startSync() {
setState(() {
_isSyncing = true;
_syncCompleted = false;
});
_animationController!.repeat();
// Simulation d'une tâche de synchronisation
Future.delayed(const Duration(seconds: 3), () {
setState(() {
_isSyncing = false;
_syncCompleted = true;
});
_animationController!.stop();
});
}
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
final double screenHeight = MediaQuery.of(context).size.height;
// Calcul du positionnement du premier cercle
final double circleSize = 55;
final double circleLeft = (screenWidth - circleSize) / 2 - 23;
final double circleTop = (screenHeight - circleSize) / 2 + 30;
// Calcul du positionnement du deuxième cercle
final double secondCircleSize = 29;
final double secondCircleLeft = (screenWidth - circleSize) / 2 + 142;
final double secondCircleTop = (screenHeight - circleSize) / 2 + 75;
// Définition de la couleur du premier cercle en fonction de l'état de synchronisation
Color circleColor;
if (_isSyncing) {
circleColor = Colors.lightBlue;
} else if (_syncCompleted) {
circleColor = Colors.green;
} else {
circleColor = Colors.transparent;
}
return Scaffold(
appBar: AppBar(
title: Text('Page de synchronisation'),
),
body: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/synchro.png'),
fit: BoxFit.contain,
),
),
),
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Calendar', style: GlobalStyle.horizontalTitle),
Row(
children: [
Icon(Icons.refresh),
Text(SharedPrefs().lastCalendarRefresh.isNotEmpty
? SharedPrefs().lastCalendarRefresh
: "Never"),
],
),
],
),
),
if (_isSyncing || _syncCompleted)
Positioned(
left: circleLeft,
top: circleTop,
child: Container(
width: circleSize,
height: circleSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: circleColor,
),
child: _syncCompleted
? Icon(
Icons.check,
color: Colors.white,
size: circleSize - 20,
)
: null,
),
),
if (_isSyncing || _syncCompleted)
Positioned(
left: secondCircleLeft,
top: secondCircleTop,
child: Container(
width: secondCircleSize,
height: secondCircleSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: circleColor,
),
child: _syncCompleted
? Icon(
Icons.check,
color: Colors.white,
size: secondCircleSize - 15,
)
: null,
),
),
if (_isSyncing)
Positioned(
left: circleLeft + (circleSize - 70) / 2,
top: circleTop + (circleSize - 70) / 2,
child: Align(
alignment: Alignment.center,
child: AnimatedBuilder(
animation: _animationController!,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2.0 * 3.14,
child: Icon(
Icons.refresh,
color: Colors.white,
size: 70,
),
);
},
),
),
),
if (_isSyncing)
Positioned(
left: secondCircleLeft + (secondCircleSize - 35) / 2,
top: secondCircleTop + (secondCircleSize - 34) / 2,
child: AnimatedBuilder(
animation: _animationController!,
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2.0 * 3.14,
child: Icon(
Icons.refresh,
color: Colors.white,
size: 35,
),
);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(height: 20),
if (_isSyncing)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: LinearProgressIndicator(),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _isSyncing ? null : startSync,
child: Text('Lancer la synchronisation'),
),
SizedBox(height: 20),
],
),
),
],
),
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:mobdr/main.dart';
import 'package:mobdr/events.dart';
import 'package:mobdr/db/box_visit_photo.dart';
import 'package:mobdr/network/api_provider.dart';
import 'package:mobdr/service/shared_prefs.dart';
class UploadPhotosPage extends StatefulWidget {
final int pp_id_visite;
@ -16,12 +17,12 @@ class UploadPhotosPage extends StatefulWidget {
}
class _UploadPhotosPageState extends State<UploadPhotosPage> {
bool _isUploading = false;
bool _isFinished = false;
bool _isSyncing = false;
bool _syncSuccessful = false;
int _totalUploaded = 0;
int _totalPhotos = 0;
late List<VisitPhoto> _photosList;
late List<VisitPhoto> _visitPhotosList;
ApiProvider _apiProvider = ApiProvider();
@ -29,25 +30,17 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
void initState() {
super.initState();
// "visit" mode
if (widget.pp_id_visite > 0) {
_photosList = objectbox.getAllVisitPhotosByVisit(widget.pp_id_visite);
_totalPhotos = _photosList.length;
// "all" mode
} else {
_photosList = objectbox.getAllVisitPhotos();
_totalPhotos = _photosList.length;
}
loadData();
}
void _uploadPhotos() async {
Future<void> _uploadVisitPhotos() async {
setState(() {
_isUploading = true;
_isFinished = false;
_isSyncing = true;
_syncSuccessful = false;
});
// parse all photos
for (var photo in _photosList) {
for (var photo in _visitPhotosList) {
int id_photo_mp4 = -1;
bool isUpdatePhotoTypologie = true;
bool isUpdatePhotoVisibility = true;
@ -119,7 +112,8 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
}
// Get unique id_visite values from _photosList
Set<int> uniqueIds = _photosList.map((photo) => photo.id_visite).toSet();
Set<int> uniqueIds =
_visitPhotosList.map((photo) => photo.id_visite).toSet();
// Send VisitPhotoCountEvent for each unique id_visite
for (int id_visite in uniqueIds) {
@ -128,11 +122,45 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
}
setState(() {
_isUploading = false;
_isFinished = true;
_isSyncing = false;
_syncSuccessful = true;
});
}
Future<void> _clearVisitPhotoCache() async {
List<VisitPhoto> _visitPhotoListTokeep;
Directory photosDir = Directory(SharedPrefs().photosDir);
_visitPhotoListTokeep = objectbox.getAllVisitPhotos();
// Get a list of all files in the "photos" directory
final List<FileSystemEntity> files = await photosDir.list().toList();
// Check each file in the directory
for (FileSystemEntity file in files) {
if (file is File) {
// Extract the file name from the file path
String fileName = file.path.split('/').last;
// Check if the file exists in the _visitPhotoListTokeep
bool existsInList = _visitPhotoListTokeep
.any((visitPhoto) => visitPhoto.image_name == fileName);
if (!existsInList) {
// Delete the file if it doesn't exist in the _visitPhotoListTokeep
await file.delete();
print('Deleted file: $fileName');
}
}
}
}
void _popScreen() {
if (mounted) {
Navigator.of(context).pop(_syncSuccessful);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -160,7 +188,7 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
children: [
Center(
child: Visibility(
visible: _isUploading,
visible: _isSyncing,
child: Container(
height: 150,
width: 150,
@ -175,7 +203,7 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
),
Center(
child: Visibility(
visible: !_isUploading && _totalUploaded == _totalPhotos,
visible: !_isSyncing && _totalUploaded == _totalPhotos,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -195,7 +223,7 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
),
Center(
child: Visibility(
visible: !_isUploading && _totalUploaded != _totalPhotos,
visible: !_isSyncing && _totalUploaded != _totalPhotos,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -216,17 +244,36 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
],
),
),
if (_syncSuccessful)
Column(
children: [
Icon(Icons.check_circle, size: 100.0, color: Colors.green),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: _popScreen,
child: Text('Synchronisation réussie'),
),
],
),
SizedBox(height: 16),
Visibility(
visible: !_isUploading,
visible: !_isSyncing,
child: ElevatedButton(
child:
Text('Upload Photos ($_totalUploaded / $_totalPhotos)'),
onPressed: _isFinished
onPressed: _syncSuccessful
? null
: () {
_uploadPhotos();
: () async {
// upload photo to server
await _uploadVisitPhotos();
// deletes photos that are no longer in any visits
await _clearVisitPhotoCache();
// go back
Navigator.pop(context);
}
//,
),
),
@ -235,4 +282,18 @@ class _UploadPhotosPageState extends State<UploadPhotosPage> {
),
);
}
/// Initializes data when the page loads.
void loadData() {
// "visit" mode
if (widget.pp_id_visite > 0) {
_visitPhotosList =
objectbox.getAllVisitPhotosByVisit(widget.pp_id_visite);
_totalPhotos = _visitPhotosList.length;
// "all" mode
} else {
_visitPhotosList = objectbox.getAllVisitPhotos();
_totalPhotos = _visitPhotosList.length;
}
}
}

View File

@ -140,11 +140,6 @@ class _VisitPhotoTypologyListPageState
child: GestureDetector(
onTap: () {
ImportImageFromGallery();
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PhotoPickPage()));
*/
},
child: ClipOval(
child: Container(

View File

@ -162,6 +162,7 @@ flutter:
- assets/images/logo_dark.png
- assets/images/logo_mp4.png
- assets/images/logo_horizontal.png
- assets/images/synchro.png
- assets/images/onboarding/search_product.gif
- assets/images/process_timeline/status1.png
- assets/images/process_timeline/status2.png