// ignore_for_file: use_build_context_synchronously import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:jiffy/jiffy.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:tp5/core/basic_page.dart'; import 'package:tp5/core/utils.dart'; import 'package:tp5/ftl/provider/ftl.dart'; import 'package:tp5/ftl/widget/w_shadowbox.dart'; import 'package:tp5/roster/api/crewlink_api.dart'; import 'package:tp5/roster/models/duty.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:tp5/roster/widgets/w_airport.dart'; import 'package:tp5/roster/widgets/w_hour.dart'; class FtlPage extends ConsumerStatefulWidget { const FtlPage({super.key, required this.params}); final FtlPageParams params; @override ConsumerState createState() => _FtlPageState(); } class _FtlPageState extends ConsumerState { late String crewlinkUser; late String crewlinkPass; late String startdate; late String enddate; Map? roster; List duties = []; String _rosterKey({String? clUser, String? start, String? end}) => "roster_${clUser ?? crewlinkUser}_${start ?? startdate}_${end ?? enddate}"; String fileidroster({String? clUser, String? start, String? end}) => PathTo().crewlinkFile( "${_rosterKey(clUser: clUser, start: start, end: end)}.pdf"); // String get _rosterKey => // "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}"; // String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf"); final AutoScrollController _scrollCtrl = AutoScrollController(); late Jiffy now; @override void initState() { crewlinkUser = widget.params.crewlinkuser ?? Hive.box("profile").get("crewlink_user") ?? ""; crewlinkPass = widget.params.crewlinkpass ?? Hive.box("profile").get("crewlink_pass") ?? ""; startdate = widget.params.datestart ?? Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy"); enddate = widget.params.dateend ?? Jiffy.now() .toUtc() //.add(months: 1) .endOf(Unit.month) .format(pattern: "ddMMMyy"); Future.delayed(Duration.zero, () async { await _loadMinMax(); //await _loadRoster(); await _loadOldRoster(); await _ftlCalc(base: 'DJE'); }).then( (value) => Future.delayed(const Duration(milliseconds: 200), () async { _scrollToDate(); })); super.initState(); } bool _isLoading = false; Future _loadRosterOnline( {String? clUser, String? start, String? end}) async { //print("FtlPage: Requesting ONline roster $start -> $end"); if (!ref.read(crewlinkapiProvider).logged) { final login = await ref .read(crewlinkapiProvider) .login(username: crewlinkUser, password: crewlinkPass); if (login["data"]?["logged"] != true) { context.showError(login["error"] ?? "Unknown error"); return null; } } final roster = await ref.read(crewlinkapiProvider).roster( start: start ?? startdate //??Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy") , end: end ?? enddate //??Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy") , fileid: fileidroster(clUser: clUser, start: start, end: end)); if (roster?["data"]?["msg"] != null) { context.showError(roster?["data"]?["msg"] ?? "Unknown error"); } if (roster["error"] == null && roster["data"] != null) { if (roster["msg"] != null) context.showAlert(roster["msg"]); Hive.box("crewlink") .put(_rosterKey(clUser: clUser, start: start, end: end), roster); return roster; } return null; } Future _loadRosterOffline( {String? clUser, String? start, String? end}) async { //print("FtlPage: Requesting OFFline roster $start -> $end"); return Hive.box("crewlink") .get(_rosterKey(clUser: clUser, start: start, end: end)); } _loadRoster( {String? clUser, String? start, String? end, bool cachefirst = false}) async { try { ref.read(isLoadingProvider.notifier).state = true; final rosterOffline = await _loadRosterOffline(clUser: clUser, start: start, end: end); if (rosterOffline?["data"]?["decoded"] is Map) { print( "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}"); _convertRoster(rosterOffline); setState(() {}); } if (rosterOffline?["data"]?["decoded"] is! Map || !cachefirst) { final rosterOnline = await _loadRosterOnline(clUser: clUser, start: start, end: end); if (rosterOnline?["data"]?["decoded"] is Map) { print( "FTL Page: found online: $start ${rosterOnline?["data"]?["decoded"] is Map}"); _convertRoster(rosterOnline); setState(() {}); } else if (rosterOffline?["data"]?["decoded"] is Map) { print( "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}"); _convertRoster(rosterOnline); setState(() {}); } } } finally { ref.read(isLoadingProvider.notifier).state = false; } } _loadOldRoster() async { bool exitnow = false; // _rosterMax = Jiffy.now().toUtc().endOf(Unit.month).subtract(hours: 10); Jiffy date1 = Jiffy.parse(startdate, pattern: "ddMMMyy") .subtract(months: 11) .startOf(Unit.day) .startOf(Unit.month) .max(_rosterMin ?? Jiffy.now().toUtc().startOf(Unit.month)); while (!exitnow) { Jiffy date2 = date1 .endOf(Unit.month) .min(_rosterMax?.endOf(Unit.day) ?? Jiffy.now().toUtc().add(days: 3)); if (date2.isAfter(date1) && date2.endOf(Unit.month) == date1.endOf(Unit.month)) { //print("ftlpage: loadoldroster: loading ${DTInterval(date1, date2)}"); await _loadRoster( start: date1.format(pattern: "ddMMMyy"), end: date2.format(pattern: "ddMMMyy"), cachefirst: date1 .endOf(Unit.month) .isBefore(Jiffy.now().endOf(Unit.month))); } else { exitnow = true; } date1 = date1.add(months: 1).startOf(Unit.month); } } String get _rosterMinMaxKey => "minmax_$crewlinkUser"; _loadMinMax() async { final resoff = Hive.box("crewlink").get(_rosterMinMaxKey); //print("ftlpage: resoff: $resoff"); final reson = await ref.read(crewlinkapiProvider).rosterMinMax(); //print("ftlpage: reson: $reson"); ref.read(isLoadingProvider.notifier).state = false; dynamic res; if (reson != null && reson["error"] == null && reson?["data"] != null) { Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]); res = reson["data"]; } else if (resoff == null) { //print(reson["error"] ?? "Unknown error"); } res = res ?? resoff; _rosterMin = Jiffy.parse(res["mindate"], pattern: "yyyy-MM-dd", isUtc: true); _rosterMax = Jiffy.parse(res["maxdate"], pattern: "yyyy-MM-dd", isUtc: true); } Jiffy? _rosterMin; Jiffy? _rosterMax; _scrollToDate({Jiffy? date}) async { final jdate = date ?? Jiffy.now().toUtc(); bool found = false; int id = 0; for (Duty duty in duties) { // if (duty.jdate.yMd == jdate.yMd) { if (duty.jdate.isSameOrAfter(jdate)) { found = true; break; } id++; } if (found && mounted && _scrollCtrl.hasClients) { // await _scrollCtrl.scrollToIndex(duties.length - 30, // duration: const Duration(milliseconds: 500), // preferPosition: AutoScrollPosition.end); await _scrollCtrl.scrollToIndex(0, duration: const Duration(milliseconds: 1000), preferPosition: AutoScrollPosition.end); await _scrollCtrl.scrollToIndex(id, duration: const Duration(milliseconds: 1000), preferPosition: AutoScrollPosition.end); } } int i = 0; _convertRoster(Map? input) { roster = input; final decoded = (input?["data"]?["decoded"]?["roster"] ?? {}); for (var date in decoded.keys) { duties = duties.where((e) => e.date != date).toList(); var dutylist = (decoded[date] as List); for (var duty in dutylist) { i++; duties.add( Duty(date: date, type: duty["type"], data: duty["data"], order: i)); //print("${(duties.last).jdate.yMd} ${(duties.last).type} ${(duties.last).start?.Hm} ${(duties.last).end?.Hm}"); } } } Ftl ftldata = Ftl.fromCrewlink(clDuties: [], base: 'DJE'); Jiffy ftlcalcdate = Jiffy.now().subtract(days: 30).startOf(Unit.month); _ftlCalc({required String base}) async { ref.read(isLoadingProvider.notifier).state = true; ftldata = Ftl.fromCrewlink(clDuties: duties, base: base); //await compute(ftlCalc, {"clDuties": duties, "base": base}); // await ftldata.calcduties(date: null); await ftldata.calcduties(date: ftlcalcdate); ref.read(isLoadingProvider.notifier).state = false; } @override Widget build(BuildContext context) { return BasicPage( actions: [ ElevatedButton( onPressed: _isLoading ? null : () async { setState(() { _isLoading = true; }); await _ftlCalc(base: "DJE"); setState(() { _isLoading = false; }); }, child: const Text("calc")) ], title: "FTL", body: ftldata.duties.isEmpty ? const Text("Please wait, calculations in progress...") : ListView.builder( itemCount: ftldata.duties.length, itemBuilder: (contexte, index) => AutoScrollTag( key: ValueKey(index), controller: _scrollCtrl, index: index, child: (ftldata.duties .elementAt(index) .start! .isSameOrAfter(ftlcalcdate)) ? _getItem(contexte, index) : Container(), ), shrinkWrap: false, controller: _scrollCtrl, ), ); } bool _is36h(DTInterval? e, String station) { if (e == null) return false; final firstnight = DTInterval.fromHm( apartir: Ftl.changeTz(e.start, station).toUtc(), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59), ap: station); final secondnight = DTInterval(firstnight.start.add(days: 1), firstnight.end.add(days: 1)); if (e.duration.inHours >= 36 && e.contains(firstnight) && e.contains(secondnight)) { return true; } else { return false; } } bool _is48h(DTInterval? e, String station) { if (e == null) return false; final localdays = DTInterval.fromHm( apartir: Ftl.changeTz(e.start, station).toUtc(), h: 0, m: 0, duration: const Duration(hours: 47, minutes: 59), ap: station); if (e.contains(localdays)) { return true; } else { return false; } } _showDutyInfo(int i) { final duty = ftldata.duties[i]; //print("ftlpage: ${duty.interval} ${duty.type}"); final margin = ((duty.fdpExt ? duty.fdpExtMax : duty.fdpMax) ?? Duration.zero) .subtract(duty.fdpLength); showMaterialModalBottomSheet( bounce: true, context: context, builder: (context) => Container( padding: const EdgeInsets.all(10), decoration: const BoxDecoration( border: Border(top: BorderSide(width: 3, color: Colors.white70))), //height: 250, child: SingleChildScrollView( child: Column( children: [ const Gap(5), ElevatedButton( onPressed: () { context.pop(); }, child: const Text( "Close", style: TextStyle( color: Colors.red, fontWeight: FontWeight.w700), )), const Gap(5), const Text( "Duty FTL Details", style: TextStyle(fontSize: 22), ), const Gap(10), Row( children: [ const Text("Duty:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(20), _titleInfo(duty.start!.yMMMEd, duty.start!.Hm), const Gap(20), _titleInfo(duty.start!.yMMMEd, duty.end!.Hm) ], ), const Gap(20), if (duty.fdpStart != null) Row( children: [ const Text("FDP:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(20), _titleInfo("FDP Length", duty.fdpLength.tohhmm), const Gap(20), _titleInfo("Fdp Max", duty.fdpMax!.tohhmm), const Gap(20), _titleInfo("Fdp Ext Max", duty.fdpExtMax!.tohhmm), const Gap(20), _titleInfo(margin.isNegative ? "Exceeded" : "Margin", margin.abs().tohhmm, color: margin.isNegative ? Colors.red : Colors.green, sizeinfo: 12), ], ), const Gap(20), if (duty.fdpStart != null) Row( children: [ const Text("Acclim:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(20), _titleInfo("Acclim type", duty.acclim), const Gap(20), _titleInfo("Ref time", duty.reftime), ], ), const Gap(20), if (duty.fdpStart != null) Row( children: [ const Text("Report:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(10), _titleInfo("Report time", duty.start?.Hm ?? "----"), const Gap(10), _titleInfo( "Delayed Report1", duty.reportdelay1?.Hm ?? "----"), const Gap(10), _titleInfo( "Delayed Report1", duty.reportdelay2?.Hm ?? "----"), ], ), const Gap(20), if (duty.fdpStart != null && (duty.lateFinish || duty.earlyStart || duty.nightDuty)) Row( children: [ const Text("Disruptive:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(10), if (duty.earlyStart) const Text("early start duty", style: TextStyle( fontWeight: FontWeight.w800, fontSize: 12, color: Colors.cyan)), const Gap(10), if (duty.lateFinish) const Text("late finish duty", style: TextStyle( fontWeight: FontWeight.w800, fontSize: 12, color: Colors.cyan)), const Gap(10), if (duty.nightDuty) const Text("night duty", style: TextStyle( fontWeight: FontWeight.w800, fontSize: 12, color: Colors.cyan)), const Gap(10), ], ), const Gap(20), Row( children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ const Text("Detail:", style: TextStyle(fontWeight: FontWeight.w800)), ...(duty.dutiesDetailsIndex.map((e) { final detail = ftldata.dutiesDetails[e]; final label = (detail.placeEnd != null) ? "${detail.placeStart}-${detail.placeEnd}" : "${detail.label}"; return [ const Gap(20), _titleInfo(label, "${detail.start?.Hm}-${detail.end?.Hm}", sizeinfo: 10) ]; }).flattened), ]), ), ], ), const Gap(20), Row( children: [ const Text("Tot duties hrs:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(10), _titleInfo("7 days", "${duty.dutyTotal?.dutylast7?.tohhmm}|${Ftl.maxdutylast7.tohhmm}", sizeinfo: 10), const Gap(5), _titleInfo("14 days", "${duty.dutyTotal?.dutylast14?.tohhmm}|${Ftl.maxdutylast14.tohhmm}", sizeinfo: 10), const Gap(5), _titleInfo("28 days", "${duty.dutyTotal?.dutylast28?.tohhmm}|${Ftl.maxdutylast28.tohhmm}", sizeinfo: 10), ], ), const Gap(20), Row( children: [ const Text("Tot Flt hrs:", style: TextStyle(fontWeight: FontWeight.w800)), const Gap(10), _titleInfo("28 days", "${duty.dutyTotal?.fltlast28?.tohhmm}|${Ftl.maxfltlast28.tohhmm}", sizeinfo: 10), const Gap(5), _titleInfo("12 months", "${duty.dutyTotal?.fltlast12?.tohhmm}|${Ftl.maxfltlast12.tohhmm}", sizeinfo: 10), const Gap(5), _titleInfo("year", "${duty.dutyTotal?.fltyear?.tohhmm}|${Ftl.maxfltlastyear.tohhmm}", sizeinfo: 10), ], ), ], ), ), ), ); } _titleInfo(String? title, String? info, {double sizetitle = 10, double sizeinfo = 16, Color color = Colors.blueGrey}) => Column( children: [ Text( title ?? "---", style: TextStyle(color: Colors.grey, fontSize: sizetitle), ), Text(info ?? "---", style: TextStyle(color: color, fontSize: sizeinfo)), ], ); Widget _getItem(BuildContext _, int i) { final duty = ftldata.duties.elementAt(i); final lastduty = i == 0 ? null : ftldata.duties.elementAt(i - 1); final rest = lastduty == null ? null : DTInterval(lastduty.end!, duty.start!); Color legal; if (duty.legals.isEmpty) { legal = Colors.green[900]!; } else if (duty.legals.every((e) => (e.condition ?? "") != "")) { legal = Colors.amber; } else { legal = Colors.red; } return Column( children: [ if (lastduty != null) WShadowbox( child: Column(children: [ Container( decoration: const BoxDecoration( color: Color.fromARGB(255, 8, 9, 9), border: Border(top: BorderSide(width: 5)), borderRadius: BorderRadius.all(Radius.circular(10.0)), ), padding: const EdgeInsets.all(5), // width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( children: [ _titleInfo("Rest", "${(rest?.duration.inDays ?? 0)} days, ${(rest?.duration.inHours ?? 0) % 24} hours, ${(rest?.duration.inMinutes ?? 0) % 60} minutes", sizeinfo: 10), ], ), Column(children: [ if (_is48h(rest, lastduty.placeEnd ?? "UTC")) Container( color: Colors.teal[900], padding: const EdgeInsets.all(5), child: Text( "48h with 2 local days", style: TextStyle( color: Colors.yellow[600]!, fontSize: 11, fontWeight: FontWeight.w600), )), if (_is36h(rest, lastduty.placeEnd ?? "UTC")) Container( color: Colors.teal[800], padding: const EdgeInsets.all(5), child: Text( "36h with 2 local nights", style: TextStyle( color: Colors.yellow[500]!, fontSize: 11, fontWeight: FontWeight.w600), )) ]), ], ), ) ])), InkWell( onTap: () => _showDutyInfo(i), child: WShadowbox( child: Column( children: [ Container( decoration: BoxDecoration( color: const Color.fromARGB(255, 1, 45, 47), border: Border(top: BorderSide(width: 5, color: legal)), borderRadius: const BorderRadius.vertical(top: Radius.circular(10.0)), ), padding: const EdgeInsets.symmetric(horizontal: 20), // width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "${duty.start?.format(pattern: "dd MMM'yy")}", style: const TextStyle(color: Colors.white54), ), if (duty.start != null) WHour(jiffy: duty.start!), ], ), ), Container( width: double.infinity, padding: const EdgeInsets.only(left: 20, right: 10), decoration: const BoxDecoration( color: Colors.black, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: duty.legals .map((e) => Text( "+ ${e.condition ?? ""} ${e.legalCauseMsg}", style: const TextStyle(fontSize: 10), )) .toList()), ), //details ...duty.dutiesDetailsIndex.map((index) { final FtlDutyDetails dutydetail = ftldata.dutiesDetails[index]; return Row( children: [ const Gap(40), SizedBox( width: 50, child: Row(children: [ Text( dutydetail.typeString, style: TextStyle( backgroundColor: Colors.blueGrey[900], color: Colors.yellow, fontSize: 10, fontWeight: FontWeight.w600, ), ) ]), ), SizedBox( width: 60, child: Row( children: [ WHour( jiffy: dutydetail.start!, size: 12, color: Colors.white38, ), const Gap(5), WHour( jiffy: dutydetail.end!, size: 12, color: Colors.white38, ), ], ), ), SizedBox( // width: 80, child: Row(children: [ const Gap(10), if (dutydetail.placeStart != null) WAirport(iata: dutydetail.placeStart!, size: 12), if (dutydetail.placeEnd != null) ...[ const Icon(Icons.arrow_right_alt, size: 9), WAirport(iata: dutydetail.placeEnd!, size: 12) ] else if ((dutydetail.label ?? "") != "") Text(" (${dutydetail.label})", style: const TextStyle( color: Colors.white54, fontSize: 10, letterSpacing: 2), overflow: TextOverflow.ellipsis), ]), ), ], ); }), Container( decoration: const BoxDecoration( color: Color.fromARGB(255, 1, 45, 47), borderRadius: BorderRadius.vertical(bottom: Radius.circular(10.0)), ), padding: const EdgeInsets.symmetric(horizontal: 20), // width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (duty.end != null && duty.start != null) _titleInfo( "Duty length", duty.end!.dateTime .difference(duty.start!.dateTime) .tohhmm), const Row(children: []), Column( mainAxisSize: MainAxisSize.min, children: [ Text( (duty.end?.yMMMd != duty.start?.yMMMd) ? "${duty.end?.format(pattern: "dd MMM")}" : "", style: const TextStyle( color: Colors.white54, fontSize: 12), ), if (duty.end != null) WHour(jiffy: duty.end!), ], ), ], ), ), ], ), ), ), ], ); } } class FtlPageParams { const FtlPageParams({ this.dateend, this.datestart, this.crewlinkuser, this.crewlinkpass, }); final String? dateend; final String? datestart; final String? crewlinkuser; final String? crewlinkpass; }