1
0

roster_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  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}",
  325. name: "RosterPage");
  326. switch (duty.type) {
  327. case "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. ref.read(isLoadingProvider.notifier).state = false;
  341. if (res is Map && res["data"]?["id"] != null) {
  342. _showNotif();
  343. } else {
  344. AlertDialog(
  345. title: const Text('Notification'),
  346. content: Text(res?["error"] ??
  347. res?["data"]?["msg"] ??
  348. 'No pending notification found !!!'),
  349. actions: [
  350. TextButton(
  351. child: const Text("OK"),
  352. onPressed: () => context.pop())
  353. ]).show(context);
  354. }
  355. }),
  356. TextButton(
  357. child: const Text(
  358. "Discard",
  359. style: TextStyle(color: Colors.green),
  360. ),
  361. onPressed: () => context.pop())
  362. ],
  363. ).show(context);
  364. break;
  365. case "flight":
  366. context.push("/fltinfo",
  367. extra: FltinfoParams(
  368. al: duty.data["al"],
  369. fnum: duty.data["fnum"],
  370. dep: duty.data["dep"],
  371. des: duty.data["des"],
  372. jdep: duty.start,
  373. jdes: duty.end));
  374. case "dhflight":
  375. context.push("/fltinfo",
  376. extra: FltinfoParams(
  377. al: duty.data["al"],
  378. fnum: duty.data["fnum"],
  379. dep: duty.data["dep"],
  380. des: duty.data["des"],
  381. jdep: duty.start,
  382. jdes: duty.end));
  383. case "dhlimo":
  384. final pseudoleg = Pnleg(
  385. dep: duty.data["dep"],
  386. arr: duty.data["des"],
  387. depdate: duty.start?.format(pattern: "dd/MM/yyyy"),
  388. deptime: duty.start?.format(pattern: "HHmm"),
  389. arrdate: duty.end?.format(pattern: "dd/MM/yyyy"),
  390. arrtime: duty.end?.format(pattern: "HHmm"),
  391. //label: duty.data["label"],
  392. type: "G");
  393. context.push("/dutyinfo",
  394. extra: DutyinfoParams(
  395. dutytype: pseudoleg.dutytype,
  396. jdep: duty.start,
  397. jdes: duty.end,
  398. dep: duty.data["dep"],
  399. des: duty.data["des"]));
  400. break;
  401. case "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: "G",
  411. );
  412. context.push("/dutyinfo",
  413. extra: DutyinfoParams(
  414. dutytype: pseudoleg.dutytype,
  415. label: pseudoleg.label,
  416. jdep: duty.start,
  417. jdes: duty.end,
  418. start: duty.start,
  419. end: duty.end,
  420. dep: duty.data["dep"],
  421. des: duty.data["des"]));
  422. break;
  423. default:
  424. }
  425. },
  426. child: WDuty(duty: duty),
  427. )
  428. ]);
  429. }
  430. void _showNotif() {
  431. {
  432. context.pop();
  433. context
  434. .push("/pdf",
  435. extra: PdfPageParams(
  436. file: fileidnotif,
  437. title: "Roster change",
  438. bottom: ElevatedButton(
  439. style: ElevatedButton.styleFrom(
  440. backgroundColor: Colors.red[900],
  441. ),
  442. onPressed: () async {
  443. ref.read(isLoadingProvider.notifier).state = true;
  444. final ack =
  445. await ref.read(crewlinkapiProvider).confirmNotif();
  446. // final ack = {
  447. // "error": null,
  448. // "data": {
  449. // "msg": null,
  450. // "error": null,
  451. // "notif": true
  452. // }
  453. // };
  454. ref.read(isLoadingProvider.notifier).state = false;
  455. final ackok = (ack is Map && ack["error"] == null
  456. //&&
  457. //ack["data"]?["error"] != null
  458. );
  459. if (ackok) {
  460. AlertDialog(
  461. title: const Text('Notification'),
  462. content:
  463. // ack["data"]?["msg"] ??
  464. const Text(
  465. 'Notification for change was acknowledged.'),
  466. actions: [
  467. TextButton(
  468. child: const Text("OK"),
  469. onPressed: () {
  470. context.pop({"notif": true});
  471. })
  472. ]).show(context);
  473. } else {
  474. AlertDialog(
  475. title: const Text('Notification'),
  476. content:
  477. // ack["data"]?["msg"] ??
  478. const Text(
  479. 'A Problem has occured. Notification not confirmed.'),
  480. actions: [
  481. TextButton(
  482. child: const Text("OK"),
  483. onPressed: () {
  484. context.pop({"notif": false});
  485. })
  486. ]).show(context);
  487. }
  488. },
  489. child: const Padding(
  490. padding: EdgeInsets.all(8.0),
  491. child: Text(
  492. 'Acknowledge & Confirm changes',
  493. style: TextStyle(fontSize: 20, color: Colors.yellow),
  494. ),
  495. ),
  496. )))
  497. .then((res) {
  498. // if (res is Map && res["notif"]) _loadRoster();
  499. if (res is Map && res["notif"]) context.go("/crewlink/roster");
  500. });
  501. }
  502. }
  503. void _showPdf(xcontext) {
  504. if (File(fileidroster).existsSync()) {
  505. context.push("/pdf",
  506. extra: PdfPageParams(
  507. file: fileidroster,
  508. title:
  509. "Roster: ${Jiffy.parse(widget.params.datestart ?? "", pattern: "ddMMMyy", isUtc: true).format(pattern: "MMMM yyyy")}"));
  510. } else {
  511. context.showError(
  512. "Can't find the roster for this month.\n Try to connect to internet and retrieve it from CrewLink?");
  513. }
  514. }
  515. void _showMonths(xcontext) async {
  516. ref.read(isLoadingProvider.notifier).state = true;
  517. final resoff = Hive.box("crewlink").get(_rosterMinMaxKey);
  518. final reson = await ref.read(crewlinkapiProvider).rosterMinMax();
  519. ref.read(isLoadingProvider.notifier).state = false;
  520. dynamic res;
  521. if (reson["error"] == null && reson?["data"] != null) {
  522. Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]);
  523. res = reson["data"];
  524. } else if (resoff == null) {
  525. context.showError(reson["error"] ?? "Unknown error");
  526. return;
  527. }
  528. res = res ?? resoff;
  529. showModalBottomSheet(
  530. useSafeArea: true,
  531. backgroundColor: Colors.blueGrey[900],
  532. context: context,
  533. builder: (BuildContext bc) {
  534. return HorizontalMonth(
  535. start: Jiffy.parse(res["mindate"],
  536. pattern: "yyyy-MM-dd", isUtc: true)
  537. // .add(days: 1),
  538. .add(months: 1)
  539. .startOf(Unit.month),
  540. end: Jiffy.parse(res["maxdate"],
  541. pattern: "yyyy-MM-dd", isUtc: true)
  542. .subtract(days: 1),
  543. selectedmonth: rostermonths,
  544. onmonthclick: (date) {
  545. bc.push("/crewlink/roster",
  546. extra: RosterPageParams(
  547. dateend:
  548. date.endOf(Unit.month).format(pattern: "ddMMMyy"),
  549. datestart:
  550. date.startOf(Unit.month).format(pattern: "ddMMMyy"),
  551. crewlinkuser: crewlinkUser,
  552. crewlinkpass: crewlinkPass));
  553. });
  554. });
  555. }
  556. }
  557. class RosterPageParams {
  558. const RosterPageParams({
  559. this.dateend,
  560. this.datestart,
  561. this.crewlinkuser,
  562. this.crewlinkpass,
  563. });
  564. final String? dateend;
  565. final String? datestart;
  566. final String? crewlinkuser;
  567. final String? crewlinkpass;
  568. }