// ignore_for_file: use_build_context_synchronously import 'dart:developer'; import 'dart:io'; //import 'package:awesome_dialog/awesome_dialog.dart'; 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:scroll_to_index/scroll_to_index.dart'; import 'package:tp5/core/basic_page.dart'; import 'package:tp5/core/core.dart'; import 'package:tp5/csv/data.dart'; import 'package:tp5/fltinfo/view/dutyinfo_page.dart'; import 'package:tp5/fltinfo/view/fltinfo_page.dart'; import 'package:tp5/pdf/pdf_page.dart'; import 'package:tp5/roster/api/crewlink_api.dart'; import 'package:tp5/roster/models/duty.dart'; import 'package:tp5/roster/view/crewlist_page.dart'; import 'package:tp5/roster/widgets/w_day.dart'; import 'package:tp5/roster/widgets/w_duty.dart'; import 'package:tp5/roster/widgets/w_horizontal_month.dart'; class RosterPage extends ConsumerStatefulWidget { const RosterPage({required this.params, super.key}); final RosterPageParams params; @override ConsumerState createState() => _RosterPageState(); } class _RosterPageState extends ConsumerState { late String crewlinkUser; late String crewlinkPass; Map? roster; List duties = []; List get rostermonths => duties .fold( [], (List p, Duty e) => [ ...p, if (!p .map((Jiffy j) => j.format(pattern: "MMMyy")) .contains(e.jdate.format(pattern: "MMMyy"))) e.jdate ]) .where((Jiffy e) => (duties .map((Duty d) => d.jdate.yMd) .contains(e.startOf(Unit.month).yMd) && duties .map((Duty d) => d.jdate.yMd) .contains(e.endOf(Unit.month).yMd))) .toList(); String get _rosterKey => "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}"; String get _rosterMinMaxKey => "minmax_$crewlinkUser"; String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf"); String get fileidnotif => PathTo().crewlinkFile("notif_$crewlinkUser.pdf"); final AutoScrollController _scrollCtrl = AutoScrollController(); late Jiffy now; @override void initState() { // getusername & pass // crewlinkPass = Hive.box("profile").get("crewlink_pass"); crewlinkUser = widget.params.crewlinkuser ?? Hive.box("profile").get("crewlink_user") ?? ""; crewlinkPass = widget.params.crewlinkpass ?? Hive.box("profile").get("crewlink_pass") ?? ""; Future.delayed(Duration.zero, () => _loadRoster()); super.initState(); } Future _loadRosterOnline() async { 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: widget.params.datestart ?? Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy"), end: widget.params.dateend ?? Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy"), fileid: fileidroster); 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, roster); return roster; } return null; } Future _loadRosterOffline() async { return Hive.box("crewlink").get(_rosterKey); } _loadRoster() async { try { ref.read(isLoadingProvider.notifier).state = true; final rosterOffline = await _loadRosterOffline(); if (rosterOffline != null && mounted) { _convertRoster(rosterOffline); setState(() {}); _scrollToDate(); } final rosterOnline = await _loadRosterOnline(); if (rosterOnline != null && mounted) { _convertRoster(rosterOnline); setState(() {}); _scrollToDate(); } } finally { ref.read(isLoadingProvider.notifier).state = false; } } _scrollToDate({Jiffy? date}) { final jdate = date ?? Jiffy.now().toUtc(); bool found = false; int id = 0; for (Duty duty in duties) { if (duty.jdate.yMd == jdate.yMd) { found = true; break; } id++; } Future.delayed(const Duration(milliseconds: 100)).then((value) { if (found && mounted && _scrollCtrl.hasClients) { _scrollCtrl.scrollToIndex(id, duration: const Duration(milliseconds: 1300), preferPosition: AutoScrollPosition.begin); } }); } _convertRoster(Map? input) { roster = input; int i = 0; duties.clear(); final decoded = (input?["data"]?["decoded"]?["roster"] ?? {}); for (var date in decoded.keys) { 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}"); } } _calculHours(); } Duration _fltHrs = Duration.zero; num _nbIso = 0; Duration _credit = Duration.zero; _calculHours() { _fltHrs = duties .where((e) => e.jdate.yM == duties.firstOrNull?.jdate.yM) .fold(Duration.zero, (t, e) { switch (e.type) { case "flight": return t + DTInterval(e.start!, e.end!).duration; default: return t; } }); _nbIso = duties.fold( 0, (t, e) => t + ((((duties .firstWhereOrNull((f) => (f.jdate.yMEd == e.jdate.yMEd && f.type == "credit")) ?.data["credit"]) ?? "0:00") == "0:00") ? 0 : 1)); _credit = duties .where((e) => e.jdate.yM == duties.firstOrNull?.jdate.yM) .fold(Duration.zero, (t, e) { switch (e.type) { case "flight": return t + DTInterval(e.start!, e.end!).duration; case "dhflight": return t + DTInterval(e.start!, e.end!).duration; case "dhlimo": return t + DTInterval(e.start!, e.end!).duration; case "ground": if (e.data["label"].startsWith("SBY")) { return t + const Duration(hours: 6, minutes: 10); } if (["CA", "CM", "OFF", "PP", "FR"].contains(e.data["label"])) { return t; } return t; default: return t; } }).add(_nbIso >= 3 ? Duration.zero : (const Duration(hours: 2, minutes: 10) .multiply(_nbIso.toDouble()))); } final bottomnavstyle = ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), ), backgroundColor: const Color.fromARGB(255, 0, 36, 53) //elevated btton background color ); @override Widget build(BuildContext context) { now = ref.watch(clockProvider); return BasicPage( actions: [ IconButton( onPressed: () => context.push("/crewlink/settings"), icon: const Icon(Icons.settings)), const Gap(10) ], bottomNavigationBar: Container( padding: const EdgeInsets.all(8), // color: Colors.black, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.grey[700]!, Colors.black, ], )), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton.icon( onPressed: () { _showMonths(context); }, icon: const Icon( Icons.calendar_month), //icon data for elevated button label: const Text("Change\nMonth"), //label text style: bottomnavstyle, ), const Gap(10), ElevatedButton.icon( onPressed: () { _showPdf(context); }, icon: Image.asset( 'assets/pdficon.png', width: 28, ), //icon data for elevated button label: const Text("Roster\n PDF"), //label text style: bottomnavstyle, ), const Gap(10), Column(mainAxisSize: MainAxisSize.min, children: [ Text( "Flt: ${_fltHrs.tohhmm}", style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), ), Text( "Credit: [${_credit.tohhmm}]", style: TextStyle(color: Colors.blue), ), ]) ], ), ), title: "CrewLink / Roster", body: ListView.builder( itemCount: duties.length, itemBuilder: (contextx, index) => AutoScrollTag( key: ValueKey(index), controller: _scrollCtrl, index: index, child: _getItem(contextx, index)), shrinkWrap: false, controller: _scrollCtrl, ), ); } Widget _getItem(BuildContext ctx, int i) { final duty = duties.elementAt(i); Duty? firstitem = duties.firstWhereOrNull((Duty el) => el.date == duty.date); bool isfirstitem = false; if (firstitem != null && firstitem.type == duty.type && firstitem.data == duty.data) isfirstitem = true; final String lastday = duty.jdate.subtract(days: 1).format(pattern: "yyyy-MM-dd"); final Duty? dutyendingtoday = (!isfirstitem) ? null : duties.firstWhereOrNull((Duty el) => (el.date == lastday) && (el.end != null) && (el.end!.format(pattern: "yyyy-MM-dd") == duty.date)); return Column(children: [ if (isfirstitem) ...[ const Gap(10), const Divider(), WDay( date: duty.jdate, highlight: now.yMd == duty.jdate.yMd, onTap: () => context.go("/crewlink/crewlist", extra: CrewlistPageParams( datestart: duty.jdate.format(pattern: "ddMMMyy"))), ), if (dutyendingtoday != null) WDuty(duty: dutyendingtoday, date: duty.date), ], InkWell( highlightColor: Colors.yellow[900], onTap: () { log("Tap: ${duty.start?.Hm} ${duty.type} ${duty.data}", name: "RosterPage"); switch (duty.type) { case "changed": AlertDialog( title: const Text('Notification'), content: const Text('Load and show pending notification?'), actions: [ TextButton( child: const Text("Load Notification", style: TextStyle(color: Colors.red, fontSize: 16)), onPressed: () async { ref.read(isLoadingProvider.notifier).state = true; final res = await ref .read(crewlinkapiProvider) .notif(download: true, fileid: fileidnotif); ref.read(isLoadingProvider.notifier).state = false; if (res is Map && res["data"]?["id"] != null) { _showNotif(); } else { AlertDialog( title: const Text('Notification'), content: Text(res?["error"] ?? res?["data"]?["msg"] ?? 'No pending notification found !!!'), actions: [ TextButton( child: const Text("OK"), onPressed: () => context.pop()) ]).show(context); } }), TextButton( child: const Text( "Discard", style: TextStyle(color: Colors.green), ), onPressed: () => context.pop()) ], ).show(context); break; case "flight": context.push("/fltinfo", extra: FltinfoParams( al: duty.data["al"], fnum: duty.data["fnum"], dep: duty.data["dep"], des: duty.data["des"], jdep: duty.start, jdes: duty.end)); case "dhflight": context.push("/fltinfo", extra: FltinfoParams( al: duty.data["al"], fnum: duty.data["fnum"], dep: duty.data["dep"], des: duty.data["des"], jdep: duty.start, jdes: duty.end)); case "dhlimo": final pseudoleg = Pnleg( dep: duty.data["dep"], arr: duty.data["des"], depdate: duty.start?.format(pattern: "dd/MM/yyyy"), deptime: duty.start?.format(pattern: "HHmm"), arrdate: duty.end?.format(pattern: "dd/MM/yyyy"), arrtime: duty.end?.format(pattern: "HHmm"), //label: duty.data["label"], type: "G"); context.push("/dutyinfo", extra: DutyinfoParams( dutytype: pseudoleg.dutytype, jdep: duty.start, jdes: duty.end, dep: duty.data["dep"], des: duty.data["des"])); break; case "ground": final pseudoleg = Pnleg( dep: duty.data["dep"], arr: duty.data["des"], depdate: duty.start?.format(pattern: "dd/MM/yyyy"), deptime: duty.start?.format(pattern: "HHmm"), arrdate: duty.end?.format(pattern: "dd/MM/yyyy"), arrtime: duty.end?.format(pattern: "HHmm"), label: duty.data["label"], //type: "G", ); context.push("/dutyinfo", extra: DutyinfoParams( dutytype: pseudoleg.dutytype, label: pseudoleg.label, jdep: duty.start, jdes: duty.end, start: duty.start, end: duty.end, dep: duty.data["dep"], des: duty.data["des"])); break; default: } }, child: WDuty(duty: duty), ) ]); } void _showNotif() { { context.pop(); context .push("/pdf", extra: PdfPageParams( file: fileidnotif, title: "Roster change", bottom: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red[900], ), onPressed: () async { ref.read(isLoadingProvider.notifier).state = true; final ack = await ref.read(crewlinkapiProvider).confirmNotif(); // final ack = { // "error": null, // "data": { // "msg": null, // "error": null, // "notif": true // } // }; ref.read(isLoadingProvider.notifier).state = false; final ackok = (ack is Map && ack["error"] == null //&& //ack["data"]?["error"] != null ); if (ackok) { AlertDialog( title: const Text('Notification'), content: // ack["data"]?["msg"] ?? const Text( 'Notification for change was acknowledged.'), actions: [ TextButton( child: const Text("OK"), onPressed: () { context.pop({"notif": true}); }) ]).show(context); } else { AlertDialog( title: const Text('Notification'), content: // ack["data"]?["msg"] ?? const Text( 'A Problem has occured. Notification not confirmed.'), actions: [ TextButton( child: const Text("OK"), onPressed: () { context.pop({"notif": false}); }) ]).show(context); } }, child: const Padding( padding: EdgeInsets.all(8.0), child: Text( 'Acknowledge & Confirm changes', style: TextStyle(fontSize: 20, color: Colors.yellow), ), ), ))) .then((res) { // if (res is Map && res["notif"]) _loadRoster(); if (res is Map && res["notif"]) context.go("/crewlink/roster"); }); } } void _showPdf(xcontext) { if (File(fileidroster).existsSync()) { context.push("/pdf", extra: PdfPageParams( file: fileidroster, title: "Roster: ${Jiffy.parse(widget.params.datestart ?? "", pattern: "ddMMMyy", isUtc: true).format(pattern: "MMMM yyyy")}")); } else { context.showError( "Can't find the roster for this month.\n Try to connect to internet and retrieve it from CrewLink?"); } } void _showMonths(xcontext) async { ref.read(isLoadingProvider.notifier).state = true; final resoff = Hive.box("crewlink").get(_rosterMinMaxKey); final reson = await ref.read(crewlinkapiProvider).rosterMinMax(); ref.read(isLoadingProvider.notifier).state = false; dynamic res; if (reson["error"] == null && reson?["data"] != null) { Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]); res = reson["data"]; } else if (resoff == null) { context.showError(reson["error"] ?? "Unknown error"); return; } res = res ?? resoff; showModalBottomSheet( useSafeArea: true, backgroundColor: Colors.blueGrey[900], context: context, builder: (BuildContext bc) { return HorizontalMonth( start: Jiffy.parse(res["mindate"], pattern: "yyyy-MM-dd", isUtc: true) // .add(days: 1), .add(months: 1) .startOf(Unit.month), end: Jiffy.parse(res["maxdate"], pattern: "yyyy-MM-dd", isUtc: true) .subtract(days: 1), selectedmonth: rostermonths, onmonthclick: (date) { bc.push("/crewlink/roster", extra: RosterPageParams( dateend: date.endOf(Unit.month).format(pattern: "ddMMMyy"), datestart: date.startOf(Unit.month).format(pattern: "ddMMMyy"), crewlinkuser: crewlinkUser, crewlinkpass: crewlinkPass)); }); }); } } class RosterPageParams { const RosterPageParams({ this.dateend, this.datestart, this.crewlinkuser, this.crewlinkpass, }); final String? dateend; final String? datestart; final String? crewlinkuser; final String? crewlinkpass; }