roster_page.dart 21 KB

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