diff --git a/lib/model/visit_model.dart b/lib/model/visit_model.dart index 071236f..d51e88d 100644 --- a/lib/model/visit_model.dart +++ b/lib/model/visit_model.dart @@ -73,9 +73,9 @@ class VisitModel { return visitModelList; } - static Future> getToSyncVisits() async { + static List getToSyncVisits() { // Retrieve all visits from the database - final visits = await objectbox.getAllVisit(); + final visits = objectbox.getAllVisit(); // Map each retrieved visit to VisiteModel final visitModelList = visits diff --git a/lib/service/shared_prefs.dart b/lib/service/shared_prefs.dart index 6befde2..9a916ba 100644 --- a/lib/service/shared_prefs.dart +++ b/lib/service/shared_prefs.dart @@ -130,4 +130,12 @@ class SharedPrefs { set onboarding(int value) { _sharedPrefs.setInt('key_onboarding', value); } + + /// Get/set last calendar refresh + String get lastCalendarRefresh => + _sharedPrefs.getString('key_lastCalendarRefresh') ?? ""; + + set lastCalendarRefresh(String value) { + _sharedPrefs.setString('key_lastCalendarRefresh', value); + } } diff --git a/lib/ui/home.dart b/lib/ui/home.dart index 823ff38..f7c0f19 100644 --- a/lib/ui/home.dart +++ b/lib/ui/home.dart @@ -5,7 +5,7 @@ import 'package:mobdr/main.dart'; import 'package:mobdr/ui/account/tab_account.dart'; import 'package:mobdr/ui/home/tab_home.dart'; import 'package:mobdr/ui/mp4/tab_mp4.dart'; -import 'package:mobdr/ui/sync/tab_sync.dart'; +import 'package:mobdr/ui/sync/synchronization.dart'; import 'package:mobdr/config/constant.dart'; import 'package:mobdr/events.dart'; @@ -28,7 +28,7 @@ class _HomePageState extends State void initState() { _contentPages = [ TabHomePage(), - TabSyncPage(), + SynchronizationPage(), TabMP4Page(), TabAccountPage(), ]; diff --git a/lib/ui/home/tab_home.dart b/lib/ui/home/tab_home.dart index 90c31f3..d78806c 100644 --- a/lib/ui/home/tab_home.dart +++ b/lib/ui/home/tab_home.dart @@ -167,22 +167,6 @@ class _TabHomePageState extends State _buildLastVisit(boxImageSize), _buildTodayVisits(boxImageSize), _builPreviousVisits(boxImageSize), - ElevatedButton( - onPressed: () async { - final result = await Navigator.push( - context, - MaterialPageRoute(builder: (context) => SyncCalendarPage()), - ); - - // Refresh the widget if the synchronization was successful. - if (result == true) { - setState(() { - loadData(); - }); - } - }, - child: Text('Synchronisation'), - ) ], ), ); diff --git a/lib/ui/sync/synchronization.dart b/lib/ui/sync/synchronization.dart new file mode 100644 index 0000000..8d1a205 --- /dev/null +++ b/lib/ui/sync/synchronization.dart @@ -0,0 +1,395 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:intl/intl.dart'; + +import 'package:mobdr/config/constant.dart'; +import 'package:mobdr/config/global_style.dart'; +import 'package:mobdr/service/shared_prefs.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 { + late List tosyncVisitData = []; + + // _listKey is used for AnimatedList + var _listKey = GlobalKey(); + + @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()); + + 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(); + }); + }, + 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: [ + 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( + (Set 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 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(); + } +} diff --git a/lib/ui/sync/tab_sync.dart b/lib/ui/sync/tab_sync.dart deleted file mode 100644 index 4497c17..0000000 --- a/lib/ui/sync/tab_sync.dart +++ /dev/null @@ -1,385 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; - -import 'package:mobdr/main.dart'; -import 'package:mobdr/config/constant.dart'; -import 'package:mobdr/config/global_style.dart'; -import 'package:mobdr/events.dart'; -import 'package:mobdr/model/visit_model.dart'; -import 'package:mobdr/ui/visit/visit_photo_typology.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'; - -class TabSyncPage extends StatefulWidget { - @override - _TabSyncPageState createState() => _TabSyncPageState(); -} - -class _TabSyncPageState extends State - with AutomaticKeepAliveClientMixin { - // keep the state to do not refresh when switch navbar - @override - bool get wantKeepAlive => true; - - bool _isLoading = true; - String _errorMessage = ''; - - late List tosyncVisitData = []; - - late StreamSubscription subVisitPhotoCountEvent; - - // _listKey is used for AnimatedList - var _listKey = GlobalKey(); - - @override - void initState() { - super.initState(); - - // Listen particular event - subVisitPhotoCountEvent = eventBus.on().listen((e) { - setState(() { - for (int i = 0; i < tosyncVisitData.length; i++) { - if (tosyncVisitData[i].id_visite == e.id_visite) { - tosyncVisitData[i].photoCount = e.photoCount; - break; - } - } - }); - }); - - loadData().then((_) { - setState(() { - _isLoading = false; - }); - }); - } - - @override - void dispose() { - subVisitPhotoCountEvent.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // if we used AutomaticKeepAliveClientMixin, we must call super.build(context); - super.build(context); - final double boxImageSize = (MediaQuery.of(context).size.width / 4); - if (_isLoading) { - return Center(child: CircularProgressIndicator()); - } else if (tosyncVisitData.isEmpty) { - return Center( - child: Text('No visits to synchronise.'), - ); - } - return Scaffold( - appBar: AppBar( - iconTheme: IconThemeData( - color: GlobalStyle.appBarIconThemeColor, - ), - elevation: GlobalStyle.appBarElevation, - title: Text( - 'Synchronization', - style: GlobalStyle.appBarTitle, - ), - backgroundColor: GlobalStyle.appBarBackgroundColor, - systemOverlayStyle: GlobalStyle.appBarSystemOverlayStyle), - body: Column( - children: [ - 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(); - }); - }, - background: Container( - color: Colors.red, - child: Stack( - children: [ - Positioned.fill( - child: Align( - alignment: Alignment.center, - child: Icon( - Icons.delete, - color: Colors.white, - ), - ), - ), - ], - ), - ), - child: _buildVisitelistCard( - 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: () { - navigateToPage(context, 0); - }, - 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: Colors.red), - borderRadius: BorderRadius.all( - Radius.circular(10), - )), - child: Text( - 'Synchronize ALL visits', - style: TextStyle( - color: Colors.red, fontWeight: FontWeight.bold), - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildVisitelistCard(VisitModel data, boxImageSize, animation, index) { - return SizeTransition( - sizeFactor: animation, - child: GestureDetector( - onTap: () { - Route route = MaterialPageRoute( - builder: (context) => - VisitPhotoTypologyPage(pp_visitModel: data)); - Navigator.push(context, route); - }, - 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: [ - 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( - (Set 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 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. - Future loadData() async { - try { - // initialization of data with all visits to be synchronized - tosyncVisitData = await VisitModel.getToSyncVisits(); - } catch (e) { - // set errorMessage for debug - _errorMessage = 'Error loading visits : $e'; - } - } -}