roster_page.dart 19 KB

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