roster_page.dart 22 KB

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