roster_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. // ignore_for_file: use_build_context_synchronously
  2. import 'dart:developer';
  3. import 'dart:io';
  4. //import 'package:awesome_dialog/awesome_dialog.dart';
  5. import 'package:collection/collection.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter_riverpod/flutter_riverpod.dart';
  8. import 'package:gap/gap.dart';
  9. import 'package:go_router/go_router.dart';
  10. import 'package:hive_flutter/hive_flutter.dart';
  11. import 'package:jiffy/jiffy.dart';
  12. import 'package:scroll_to_index/scroll_to_index.dart';
  13. import 'package:tp5/core/basic_page.dart';
  14. import 'package:tp5/core/core.dart';
  15. import 'package:tp5/csv/data.dart';
  16. import 'package:tp5/fltinfo/view/dutyinfo_page.dart';
  17. import 'package:tp5/fltinfo/view/fltinfo_page.dart';
  18. import 'package:tp5/pdf/pdf_page.dart';
  19. import 'package:tp5/roster/api/crewlink_api.dart';
  20. import 'package:tp5/roster/models/duty.dart';
  21. import 'package:tp5/roster/view/crewlist_page.dart';
  22. import 'package:tp5/roster/widgets/w_day.dart';
  23. import 'package:tp5/roster/widgets/w_duty.dart';
  24. import 'package:tp5/roster/widgets/w_horizontal_month.dart';
  25. class RosterPage extends ConsumerStatefulWidget {
  26. const RosterPage({required this.params, super.key});
  27. final RosterPageParams params;
  28. @override
  29. ConsumerState<ConsumerStatefulWidget> createState() => _RosterPageState();
  30. }
  31. class _RosterPageState extends ConsumerState<RosterPage> {
  32. late String crewlinkUser;
  33. late String crewlinkPass;
  34. Map? roster;
  35. List<Duty> duties = [];
  36. List<Jiffy> get rostermonths => duties
  37. .fold(
  38. <Jiffy>[],
  39. (List<Jiffy> p, Duty e) => [
  40. ...p,
  41. if (!p
  42. .map((Jiffy j) => j.format(pattern: "MMMyy"))
  43. .contains(e.jdate.format(pattern: "MMMyy")))
  44. e.jdate
  45. ])
  46. .where((Jiffy e) => (duties
  47. .map((Duty d) => d.jdate.yMd)
  48. .contains(e.startOf(Unit.month).yMd) &&
  49. duties
  50. .map((Duty d) => d.jdate.yMd)
  51. .contains(e.endOf(Unit.month).yMd)))
  52. .toList();
  53. String get _rosterKey =>
  54. "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}";
  55. String get _rosterMinMaxKey => "minmax_$crewlinkUser";
  56. String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf");
  57. String get fileidnotif => PathTo().crewlinkFile("notif_$crewlinkUser.pdf");
  58. final AutoScrollController _scrollCtrl = AutoScrollController();
  59. late Jiffy now;
  60. @override
  61. void initState() {
  62. // getusername & pass
  63. // crewlinkPass = Hive.box("settings").get("crewlink_pass");
  64. crewlinkUser = widget.params.crewlinkuser ??
  65. Hive.box("settings").get("crewlink_user") ??
  66. "";
  67. crewlinkPass = widget.params.crewlinkpass ??
  68. Hive.box("settings").get("crewlink_pass") ??
  69. "";
  70. Future.delayed(Duration(milliseconds: 700), () => _loadRoster());
  71. super.initState();
  72. }
  73. Future<Map?> _loadRosterOnline() async {
  74. if (!ref.read(crewlinkapiProvider).logged) {
  75. final login = await ref
  76. .read(crewlinkapiProvider)
  77. .login(username: crewlinkUser, password: crewlinkPass);
  78. if (login["data"]?["logged"] != true) {
  79. context.showError(login["error"] ?? "Unknown error");
  80. return null;
  81. }
  82. }
  83. final roster = await ref.read(crewlinkapiProvider).roster(
  84. start: widget.params.datestart ??
  85. Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy"),
  86. end: widget.params.dateend ??
  87. Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy"),
  88. fileid: fileidroster);
  89. if (roster?["data"]?["msg"] != null) {
  90. context.showError(roster?["data"]?["msg"] ?? "Unknown error");
  91. }
  92. if (roster["error"] == null && roster["data"] != null) {
  93. if (roster["msg"] != null) context.showAlert(roster["msg"]);
  94. Hive.box("crewlink").put(_rosterKey, roster);
  95. return roster;
  96. }
  97. return null;
  98. }
  99. Future<Map?> _loadRosterOffline() async {
  100. return Hive.box("crewlink").get(_rosterKey);
  101. }
  102. _loadRoster() async {
  103. try {
  104. ref.read(isLoadingProvider.notifier).state = true;
  105. final rosterOffline = await _loadRosterOffline();
  106. if (rosterOffline != null && mounted) {
  107. _convertRoster(rosterOffline);
  108. setState(() {});
  109. _scrollToDate();
  110. }
  111. final rosterOnline = await _loadRosterOnline();
  112. if (rosterOnline != null && mounted) {
  113. _convertRoster(rosterOnline);
  114. setState(() {});
  115. _scrollToDate();
  116. }
  117. } finally {
  118. if (mounted) {
  119. ref.read(isLoadingProvider.notifier).state = false;
  120. }
  121. }
  122. }
  123. String get tlc => crewlinkUser;
  124. _scrollToDate({Jiffy? date}) {
  125. final jdate = date ?? Jiffy.now().toUtc();
  126. bool found = false;
  127. int id = 0;
  128. for (Duty duty in duties) {
  129. if (duty.jdate.yMd == jdate.yMd) {
  130. found = true;
  131. break;
  132. }
  133. id++;
  134. }
  135. Future.delayed(const Duration(milliseconds: 100)).then((value) {
  136. if (found && mounted && _scrollCtrl.hasClients) {
  137. _scrollCtrl.scrollToIndex(id,
  138. duration: const Duration(milliseconds: 1300),
  139. preferPosition: AutoScrollPosition.begin);
  140. }
  141. });
  142. }
  143. _convertRoster(Map? input) {
  144. roster = input;
  145. int i = 0;
  146. duties.clear();
  147. final decoded = (input?["data"]?["decoded"]?["roster"] ?? {});
  148. for (var date in decoded.keys) {
  149. var dutylist = (decoded[date] as List);
  150. for (var duty in dutylist) {
  151. i++;
  152. duties.add(
  153. Duty(date: date, type: duty["type"], data: duty["data"], order: i));
  154. // print(
  155. // "${(duties.last).jdate.yMd} ${(duties.last).type} ${(duties.last).start?.Hm} ${(duties.last).end?.Hm}");
  156. }
  157. }
  158. _calculHours();
  159. }
  160. Duration _fltHrs = Duration.zero;
  161. num _nbIso = 0;
  162. Duration _credit = Duration.zero;
  163. _calculHours() {
  164. _fltHrs = duties
  165. .where((e) => e.jdate.yM == duties.firstOrNull?.jdate.yM)
  166. .fold(Duration.zero, (t, e) {
  167. switch (e.type) {
  168. case "flight":
  169. return t + DTInterval(e.start!, e.end!).duration;
  170. default:
  171. return t;
  172. }
  173. });
  174. _nbIso = duties.fold(
  175. 0,
  176. (t, e) =>
  177. t +
  178. ((((duties
  179. .firstWhereOrNull((f) =>
  180. (f.jdate.yMEd == e.jdate.yMEd &&
  181. f.type == "credit"))
  182. ?.data["credit"]) ??
  183. "0:00") ==
  184. "0:00")
  185. ? 0
  186. : 1));
  187. _credit = duties
  188. .where((e) => e.jdate.yM == duties.firstOrNull?.jdate.yM)
  189. .fold(Duration.zero, (t, e) {
  190. switch (e.type) {
  191. case "flight":
  192. return t + DTInterval(e.start!, e.end!).duration;
  193. case "dhflight":
  194. return t + DTInterval(e.start!, e.end!).duration;
  195. case "dhlimo":
  196. return t + DTInterval(e.start!, e.end!).duration;
  197. case "ground":
  198. if (e.data["label"].startsWith("SBY")) {
  199. return t + const Duration(hours: 6, minutes: 10);
  200. }
  201. if (["CA", "CM", "OFF", "PP", "FR"].contains(e.data["label"])) {
  202. return t;
  203. }
  204. return t;
  205. default:
  206. return t;
  207. }
  208. }).add(_nbIso >= 3
  209. ? Duration.zero
  210. : (const Duration(hours: 2, minutes: 10)
  211. .multiply(_nbIso.toDouble())));
  212. }
  213. final bottomnavstyle = ElevatedButton.styleFrom(
  214. shape: RoundedRectangleBorder(
  215. borderRadius: BorderRadius.circular(5.0),
  216. ),
  217. backgroundColor:
  218. const Color.fromARGB(255, 0, 36, 53) //elevated btton background color
  219. );
  220. @override
  221. Widget build(BuildContext context) {
  222. now = ref.watch(clockProvider);
  223. return BasicPage(
  224. actions: [
  225. IconButton(
  226. onPressed: () => context.push("/crewlink/settings"),
  227. icon: const Icon(Icons.settings)),
  228. const Gap(10)
  229. ],
  230. bottomNavigationBar: Container(
  231. padding: const EdgeInsets.all(8),
  232. // color: Colors.black,
  233. decoration: BoxDecoration(
  234. gradient: LinearGradient(
  235. begin: Alignment.topCenter,
  236. end: Alignment.bottomCenter,
  237. colors: [
  238. Colors.grey[700]!,
  239. Colors.black,
  240. ],
  241. )),
  242. child: Row(
  243. mainAxisAlignment: MainAxisAlignment.spaceAround,
  244. children: [
  245. ElevatedButton.icon(
  246. onPressed: () {
  247. _showMonths(context);
  248. },
  249. icon: const Icon(
  250. Icons.calendar_month), //icon data for elevated button
  251. label: const Text("Change\nMonth"), //label text
  252. style: bottomnavstyle,
  253. ),
  254. const Gap(10),
  255. ElevatedButton.icon(
  256. onPressed: () {
  257. _showPdf(context);
  258. },
  259. icon: Image.asset(
  260. 'assets/pdficon.png',
  261. width: 28,
  262. ), //icon data for elevated button
  263. label: const Text("Roster\n PDF"), //label text
  264. style: bottomnavstyle,
  265. ),
  266. const Gap(10),
  267. Column(mainAxisSize: MainAxisSize.min, children: [
  268. Text(
  269. "Flt: ${_fltHrs.tohhmm}",
  270. style:
  271. TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
  272. ),
  273. Text(
  274. "Credit: [${_credit.tohhmm}]",
  275. style: TextStyle(color: Colors.blue),
  276. ),
  277. ])
  278. ],
  279. ),
  280. ),
  281. title: "CrewLink / Roster",
  282. body: ListView.builder(
  283. itemCount: duties.length,
  284. itemBuilder: (contextx, index) => AutoScrollTag(
  285. key: ValueKey(index),
  286. controller: _scrollCtrl,
  287. index: index,
  288. child: _getItem(contextx, index)),
  289. shrinkWrap: false,
  290. controller: _scrollCtrl,
  291. ),
  292. );
  293. }
  294. Widget _getItem(BuildContext ctx, int i) {
  295. final duty = duties.elementAt(i);
  296. Duty? firstitem =
  297. duties.firstWhereOrNull((Duty el) => el.date == duty.date);
  298. bool isfirstitem = false;
  299. if (firstitem != null &&
  300. firstitem.type == duty.type &&
  301. firstitem.data == duty.data) isfirstitem = true;
  302. final String lastday =
  303. duty.jdate.subtract(days: 1).format(pattern: "yyyy-MM-dd");
  304. final Duty? dutyendingtoday = (!isfirstitem)
  305. ? null
  306. : duties.firstWhereOrNull((Duty el) =>
  307. (el.date == lastday) &&
  308. (el.end != null) &&
  309. (el.end!.format(pattern: "yyyy-MM-dd") == duty.date));
  310. return Column(children: [
  311. if (isfirstitem) ...[
  312. const Gap(10),
  313. const Divider(),
  314. WDay(
  315. date: duty.jdate,
  316. highlight: now.yMd == duty.jdate.yMd,
  317. onTap: () => context.go("/crewlink/crewlist",
  318. extra: CrewlistPageParams(
  319. datestart: duty.jdate.format(pattern: "ddMMMyy"))),
  320. ),
  321. if (dutyendingtoday != null)
  322. WDuty(duty: dutyendingtoday, date: duty.date),
  323. ],
  324. InkWell(
  325. highlightColor: Colors.yellow[900],
  326. onTap: () {
  327. // log("Tap: ${duty.start?.Hm} ${duty.type} ${duty.data}",name: "RosterPage");
  328. if (duty.type == "changed") {
  329. AlertDialog(
  330. title: const Text('Notification'),
  331. content: const Text('Load and show pending notification?'),
  332. actions: [
  333. TextButton(
  334. child: const Text("Load Notification",
  335. style: TextStyle(color: Colors.red, fontSize: 16)),
  336. onPressed: () async {
  337. ref.read(isLoadingProvider.notifier).state = true;
  338. final res = await ref
  339. .read(crewlinkapiProvider)
  340. .notif(download: true, fileid: fileidnotif);
  341. if (mounted) {
  342. ref.read(isLoadingProvider.notifier).state = false;
  343. }
  344. if (res is Map && res["data"]?["id"] != null) {
  345. _showNotif();
  346. } else {
  347. AlertDialog(
  348. title: const Text('Notification'),
  349. content: Text(res?["error"] ??
  350. res?["data"]?["msg"] ??
  351. 'No pending notification found !!!'),
  352. actions: [
  353. TextButton(
  354. child: const Text("OK"),
  355. onPressed: () => context.pop())
  356. ]).show(context);
  357. }
  358. }),
  359. TextButton(
  360. child: const Text(
  361. "Discard",
  362. style: TextStyle(color: Colors.green),
  363. ),
  364. onPressed: () => context.pop())
  365. ],
  366. ).show(context);
  367. } else if (duty.type == "flight") {
  368. context.push("/fltinfo",
  369. extra: FltinfoParams(
  370. al: duty.data["al"],
  371. fnum: duty.data["fnum"],
  372. dep: duty.data["dep"],
  373. des: duty.data["des"],
  374. jdep: duty.start,
  375. jdes: duty.end));
  376. } else if (duty.type == "dhflight") {
  377. context.push("/fltinfo",
  378. extra: FltinfoParams(
  379. al: duty.data["al"],
  380. fnum: duty.data["fnum"],
  381. dep: duty.data["dep"],
  382. des: duty.data["des"],
  383. jdep: duty.start,
  384. jdes: duty.end));
  385. } else if (duty.type == "dhlimo") {
  386. final pseudoleg = Pnleg(
  387. dep: duty.data["dep"],
  388. arr: duty.data["des"],
  389. depdate: duty.start?.format(pattern: "dd/MM/yyyy"),
  390. deptime: duty.start?.format(pattern: "HHmm"),
  391. arrdate: duty.end?.format(pattern: "dd/MM/yyyy"),
  392. arrtime: duty.end?.format(pattern: "HHmm"),
  393. //label: duty.data["label"],
  394. type: "G");
  395. context.push("/dutyinfo",
  396. extra: DutyinfoParams(
  397. dutytype: pseudoleg.dutytype,
  398. jdep: duty.start,
  399. jdes: duty.end,
  400. dep: duty.data["dep"],
  401. des: duty.data["des"]));
  402. } else if (duty.type == "ground") {
  403. final pseudoleg = Pnleg(
  404. tlc: tlc,
  405. dep: duty.data["dep"],
  406. arr: duty.data["des"],
  407. depdate: duty.start?.format(pattern: "dd/MM/yyyy"),
  408. deptime: duty.start?.format(pattern: "HHmm"),
  409. arrdate: duty.end?.format(pattern: "dd/MM/yyyy"),
  410. arrtime: duty.end?.format(pattern: "HHmm"),
  411. label: duty.data["label"],
  412. type: "A",
  413. );
  414. if (pseudoleg.dutytype == "standby") {
  415. context.push("/dutyinfo",
  416. extra: DutyinfoParams(
  417. dutytype: pseudoleg.dutytype,
  418. tlc: pseudoleg.tlc,
  419. //label: pseudoleg.label,
  420. jdep: pseudoleg.jdep,
  421. jdes: pseudoleg.jarr,
  422. //start: duty.start,
  423. //end: duty.end,
  424. dep: pseudoleg.dep,
  425. des: pseudoleg.arr,
  426. sameday: true,
  427. ));
  428. } else {
  429. context.push("/dutyinfo",
  430. extra: DutyinfoParams(
  431. // dutytype: pseudoleg.dutytype,
  432. label: pseudoleg.label,
  433. jdep: duty.start,
  434. jdes: duty.end,
  435. // start: duty.start,
  436. // end: duty.end,
  437. dep: duty.data["dep"],
  438. des: duty.data["des"],
  439. // sameday: true,
  440. ));
  441. }
  442. }
  443. },
  444. child: WDuty(duty: duty),
  445. )
  446. ]);
  447. }
  448. void _showNotif() {
  449. {
  450. context.pop();
  451. context
  452. .push("/pdf",
  453. extra: PdfPageParams(
  454. file: fileidnotif,
  455. title: "Roster change",
  456. bottom: ElevatedButton(
  457. style: ElevatedButton.styleFrom(
  458. backgroundColor: Colors.red[900],
  459. ),
  460. onPressed: () async {
  461. ref.read(isLoadingProvider.notifier).state = true;
  462. final ack =
  463. await ref.read(crewlinkapiProvider).confirmNotif();
  464. // final ack = {
  465. // "error": null,
  466. // "data": {
  467. // "msg": null,
  468. // "error": null,
  469. // "notif": true
  470. // }
  471. // };
  472. if (mounted) {
  473. ref.read(isLoadingProvider.notifier).state = false;
  474. }
  475. final ackok = (ack is Map && ack["error"] == null
  476. //&&
  477. //ack["data"]?["error"] != null
  478. );
  479. if (ackok) {
  480. AlertDialog(
  481. title: const Text('Notification'),
  482. content:
  483. // ack["data"]?["msg"] ??
  484. const Text(
  485. 'Notification for change was acknowledged.'),
  486. actions: [
  487. TextButton(
  488. child: const Text("OK"),
  489. onPressed: () {
  490. context.pop({"notif": true});
  491. })
  492. ]).show(context);
  493. } else {
  494. AlertDialog(
  495. title: const Text('Notification'),
  496. content:
  497. // ack["data"]?["msg"] ??
  498. const Text(
  499. 'A Problem has occured. Notification not confirmed.'),
  500. actions: [
  501. TextButton(
  502. child: const Text("OK"),
  503. onPressed: () {
  504. context.pop({"notif": false});
  505. })
  506. ]).show(context);
  507. }
  508. },
  509. child: const Padding(
  510. padding: EdgeInsets.all(8.0),
  511. child: Text(
  512. 'Acknowledge & Confirm changes',
  513. style: TextStyle(fontSize: 20, color: Colors.yellow),
  514. ),
  515. ),
  516. )))
  517. .then((res) {
  518. // if (res is Map && res["notif"]) _loadRoster();
  519. //! check if the roster is still the same otherwise call _loadRoster
  520. if (res is Map && res["notif"])
  521. context.pushReplacement("/crewlink/roster");
  522. });
  523. }
  524. }
  525. void _showPdf(xcontext) {
  526. if (File(fileidroster).existsSync()) {
  527. context.push("/pdf",
  528. extra: PdfPageParams(
  529. file: fileidroster,
  530. title:
  531. "Roster: ${Jiffy.parse(widget.params.datestart ?? "", pattern: "ddMMMyy", isUtc: true).format(pattern: "MMMM yyyy")}"));
  532. } else {
  533. context.showError(
  534. "Can't find the roster for this month.\n Try to connect to internet and retrieve it from CrewLink?");
  535. }
  536. }
  537. void _showMonths(xcontext) async {
  538. ref.read(isLoadingProvider.notifier).state = true;
  539. final resoff = Hive.box("crewlink").get(_rosterMinMaxKey);
  540. final reson = await ref.read(crewlinkapiProvider).rosterMinMax();
  541. if (mounted) {
  542. ref.read(isLoadingProvider.notifier).state = false;
  543. }
  544. dynamic res;
  545. if (reson["error"] == null && reson?["data"] != null) {
  546. Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]);
  547. res = reson["data"];
  548. } else if (resoff == null) {
  549. context.showError(reson["error"] ?? "Unknown error");
  550. return;
  551. }
  552. res = res ?? resoff;
  553. showModalBottomSheet(
  554. useSafeArea: true,
  555. backgroundColor: Colors.blueGrey[900],
  556. context: context,
  557. builder: (BuildContext bc) {
  558. return HorizontalMonth(
  559. start: Jiffy.parse(res["mindate"],
  560. pattern: "yyyy-MM-dd", isUtc: true)
  561. // .add(days: 1),
  562. .add(months: 1)
  563. .startOf(Unit.month),
  564. end: Jiffy.parse(res["maxdate"],
  565. pattern: "yyyy-MM-dd", isUtc: true)
  566. .subtract(days: 1),
  567. selectedmonth: rostermonths,
  568. onmonthclick: (date) {
  569. bc.push("/crewlink/roster",
  570. extra: RosterPageParams(
  571. dateend:
  572. date.endOf(Unit.month).format(pattern: "ddMMMyy"),
  573. datestart:
  574. date.startOf(Unit.month).format(pattern: "ddMMMyy"),
  575. crewlinkuser: crewlinkUser,
  576. crewlinkpass: crewlinkPass));
  577. });
  578. });
  579. }
  580. }
  581. class RosterPageParams {
  582. const RosterPageParams({
  583. this.dateend,
  584. this.datestart,
  585. this.crewlinkuser,
  586. this.crewlinkpass,
  587. });
  588. final String? dateend;
  589. final String? datestart;
  590. final String? crewlinkuser;
  591. final String? crewlinkpass;
  592. }