// 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/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}"); } } } 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, ), ], ), ), title: "CrewLink / Roster", body: ListView.builder( itemCount: duties.length, itemBuilder: (_, index) => AutoScrollTag( key: ValueKey(index), controller: _scrollCtrl, index: index, child: _getItem(_, 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.push("/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.type} ${duty.data}", name: "RosterPage"); switch (duty.type) { case "changed": AwesomeDialog( context: context, dialogType: DialogType.question, animType: AnimType.topSlide, title: 'Notification', desc: 'Load and show pending notification?', btnOkOnPress: () 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 { AwesomeDialog( context: context, dialogType: DialogType.warning, animType: AnimType.topSlide, title: 'Notification', desc: res?["error"] ?? res?["data"]?["msg"] ?? 'No pending notification found !!!', btnOkOnPress: () async {}) .show(); } }, btnCancelOnPress: () {}, btnOkText: "Load notification", showCloseIcon: true) .show(); break; default: } }, child: WDuty(duty: duty), ) ]); } void _showNotif() { { 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); AwesomeDialog( context: context, dialogType: ackok ? DialogType.success : DialogType.warning, animType: AnimType.topSlide, title: 'Notification', desc: // ack["data"]?["msg"] ?? 'Notification for change was acknowledged.', btnOkOnPress: () { context.pop({"notif": true}); }).show(); }, 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(); }); } } 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; }