crewlist_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. // ignore_for_file: use_build_context_synchronously
  2. import 'package:collection/collection.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:gap/gap.dart';
  6. import 'package:go_router/go_router.dart';
  7. import 'package:hive_flutter/hive_flutter.dart';
  8. import 'package:jiffy/jiffy.dart';
  9. import 'package:tp5/core/core.dart';
  10. import 'package:tp5/roster/api/crewlink_api.dart';
  11. import 'package:tp5/roster/models/crewlist_leg.dart';
  12. import 'package:tp5/roster/widgets/w_crewlist.dart';
  13. import 'package:tp5/roster/widgets/w_day.dart';
  14. import 'package:tp5/core/basic_page.dart';
  15. import 'package:scroll_to_index/scroll_to_index.dart';
  16. class CrewlistPage extends ConsumerStatefulWidget {
  17. const CrewlistPage({required this.params, super.key});
  18. final CrewlistPageParams params;
  19. @override
  20. ConsumerState<ConsumerStatefulWidget> createState() => _CrewlistPageState();
  21. }
  22. class _CrewlistPageState extends ConsumerState<CrewlistPage> {
  23. late String crewlinkUser;
  24. late String crewlinkPass;
  25. TextEditingController ctrldep = TextEditingController();
  26. TextEditingController ctrldes = TextEditingController();
  27. TextEditingController ctrlairline = TextEditingController();
  28. TextEditingController ctrlfnum = TextEditingController();
  29. TextEditingController ctrlstartdate = TextEditingController();
  30. TextEditingController ctrlenddate = TextEditingController();
  31. Map? apires;
  32. List<CrewlistLeg> legs = [];
  33. String get _crewlistKey =>
  34. "crewlist_${widget.params.datestart}_${widget.params.dateend}_${widget.params.al}_${widget.params.fnum}_${widget.params.dep}_${widget.params.des}";
  35. String get _crewlistMinMaxKey => "minmax_crewlist";
  36. final AutoScrollController _scrollCtrl = AutoScrollController();
  37. late Jiffy now;
  38. @override
  39. void initState() {
  40. crewlinkUser = widget.params.crewlinkuser!;
  41. crewlinkPass = widget.params.crewlinkpass!;
  42. Future.delayed(Duration.zero, () => _loadCrewlist());
  43. super.initState();
  44. }
  45. Future<Map?> _loadCrewlistOnline() async {
  46. if (!ref.read(crewlinkapiProvider).logged) {
  47. final login = await ref
  48. .read(crewlinkapiProvider)
  49. .login(username: crewlinkUser, password: crewlinkPass);
  50. if (login["data"]?["logged"] != true) {
  51. context.showError(login["error"] ?? "Unknown error");
  52. return null;
  53. }
  54. }
  55. final res = await ref.read(crewlinkapiProvider).showCrew(
  56. crewlist: false,
  57. start: widget.params.datestart!,
  58. end: widget.params.dateend!,
  59. al: widget.params.al!,
  60. fnum: widget.params.fnum!,
  61. dep: widget.params.dep!,
  62. des: widget.params.des!);
  63. if (res?["data"]?["msg"] != null) {
  64. context.showError(res?["data"]?["msg"] ?? "Unknown error");
  65. }
  66. if (res["error"] == null && res["data"] != null) {
  67. if (res["msg"] != null) context.showAlert(res["msg"]);
  68. Hive.box("crewlink").put(_crewlistKey, res);
  69. return res;
  70. }
  71. return null;
  72. }
  73. Future<Map?> _loadCrewlistOffline() async {
  74. return Hive.box("crewlink").get(_crewlistKey);
  75. }
  76. _loadCrewlist() async {
  77. try {
  78. ref.read(isLoadingProvider.notifier).state = true;
  79. final crewlistOffline = await _loadCrewlistOffline();
  80. if (crewlistOffline != null && mounted) {
  81. _convertCrewlist(crewlistOffline);
  82. setState(() {});
  83. _scrollToDate();
  84. }
  85. final crewlistOnline = await _loadCrewlistOnline();
  86. if (crewlistOnline != null && mounted) {
  87. _convertCrewlist(crewlistOnline);
  88. setState(() {});
  89. _scrollToDate();
  90. }
  91. } finally {
  92. ref.read(isLoadingProvider.notifier).state = false;
  93. }
  94. }
  95. _scrollToDate({Jiffy? date}) {
  96. final jdate = date ?? Jiffy.now().toUtc();
  97. print("scroll request to ${jdate.Hm}");
  98. bool found = false;
  99. int id = 0;
  100. if (legs.isNotEmpty &&
  101. jdate.isSameOrAfter(legs.map((e) => e.jdep).first) &&
  102. jdate.isSameOrBefore(legs.map((e) => e.jdep).last)) {
  103. for (CrewlistLeg leg in legs) {
  104. if (leg.jdep.isSameOrAfter(jdate)) {
  105. found = true;
  106. break;
  107. }
  108. id++;
  109. }
  110. }
  111. Future.delayed(const Duration(milliseconds: 100)).then((value) {
  112. if ((found || jdate.yMd == Jiffy.now().toUtc().yMd) &&
  113. mounted &&
  114. _scrollCtrl.hasClients) {
  115. print("scrolling to id:$id ${jdate.Hm}");
  116. _scrollCtrl.scrollToIndex(id,
  117. duration: const Duration(milliseconds: 1300),
  118. preferPosition: AutoScrollPosition.begin);
  119. }
  120. });
  121. }
  122. void _convertCrewlist(Map? input) {
  123. // print("${input?["data"]["leglist"].first[7].runtimeType}");
  124. apires = input;
  125. legs = [];
  126. for (var leg in input?["data"]?["leglist"] ?? []) {
  127. final CrewlistLeg newleg = CrewlistLeg(leg);
  128. if ((newleg.jdep.isSameOrAfter(Jiffy.parse(widget.params.datestart ?? "",
  129. pattern: "ddMMMyy", isUtc: true)
  130. .startOf(Unit.day))) &&
  131. (newleg.jdep.isSameOrBefore(Jiffy.parse(widget.params.dateend ?? "",
  132. pattern: "ddMMMyy", isUtc: true)
  133. .endOf(Unit.day))) &&
  134. (legs.firstWhereOrNull((e) =>
  135. "${e.al}${e.fnum}${e.dep}${e.des}${e.jdep.millisecondsSinceEpoch}" ==
  136. "${newleg.al}${newleg.fnum}${newleg.dep}${newleg.des}${newleg.jdep.millisecondsSinceEpoch}") ==
  137. null)) legs.add(newleg);
  138. }
  139. legs.sort((a, b) {
  140. if (a.jdep.isBefore(b.jdep)) {
  141. return -1;
  142. } else if (a.jdep.isAfter(b.jdep)) {
  143. return 1;
  144. } else {
  145. return 0;
  146. }
  147. });
  148. }
  149. final bottomnavstyle = ElevatedButton.styleFrom(
  150. shape: RoundedRectangleBorder(
  151. borderRadius: BorderRadius.circular(5.0),
  152. ),
  153. backgroundColor:
  154. const Color.fromARGB(255, 0, 36, 53) //elevated btton background color
  155. );
  156. @override
  157. Widget build(BuildContext context) {
  158. now = ref.watch(clockProvider);
  159. return BasicPage(
  160. actions: [
  161. IconButton(
  162. onPressed: () => context.push("/crewlink/settings"),
  163. icon: const Icon(Icons.settings)),
  164. const Gap(10)
  165. ],
  166. bottomNavigationBar: Container(
  167. padding: const EdgeInsets.all(8),
  168. // color: Colors.black,
  169. decoration: BoxDecoration(
  170. gradient: LinearGradient(
  171. begin: Alignment.topCenter,
  172. end: Alignment.bottomCenter,
  173. colors: [
  174. Colors.grey[700]!,
  175. Colors.black,
  176. ],
  177. )),
  178. child: Row(
  179. mainAxisAlignment: MainAxisAlignment.spaceAround,
  180. children: [
  181. ElevatedButton.icon(
  182. onPressed: () {
  183. _changeDay(context);
  184. },
  185. icon: const Icon(
  186. Icons.calendar_month), //icon data for elevated button
  187. label: const Text("Change\n Day"), //label text
  188. style: bottomnavstyle,
  189. ),
  190. const Gap(10),
  191. ElevatedButton.icon(
  192. onPressed: () {
  193. _searchFlights(context);
  194. },
  195. icon: const Icon(Icons.manage_search),
  196. label: const Text("Search\nFlights"),
  197. style: bottomnavstyle,
  198. ),
  199. const Gap(10), //icon data for elevated button
  200. ],
  201. ),
  202. ),
  203. title: "CrewLink / Crewlist",
  204. body: ListView.builder(
  205. itemCount: legs.length,
  206. itemBuilder: (_, index) => AutoScrollTag(
  207. key: ValueKey(index),
  208. controller: _scrollCtrl,
  209. index: index,
  210. child: _getItem(_, index)),
  211. shrinkWrap: true,
  212. controller: _scrollCtrl,
  213. ),
  214. );
  215. }
  216. Widget _getItem(BuildContext ctx, int i) {
  217. final leg = legs.elementAt(i);
  218. CrewlistLeg? firstitem =
  219. legs.firstWhereOrNull((CrewlistLeg el) => el.date == leg.date);
  220. bool isfirstitem = false;
  221. if (firstitem != null && firstitem.data == leg.data) isfirstitem = true;
  222. return Column(
  223. children: [
  224. if (i == 0) ...[
  225. const Gap(20),
  226. InkWell(
  227. onTap: () => context.push("/crewlink/crewlist",
  228. extra: CrewlistPageParams(
  229. al: widget.params.al,
  230. fnum: widget.params.fnum,
  231. dep: widget.params.dep,
  232. des: widget.params.des,
  233. datestart: Jiffy.parse(widget.params.datestart ?? leg.date,
  234. pattern: "ddMMMyy", isUtc: true)
  235. .subtract(days: 1)
  236. .format(pattern: "ddMMMyy"),
  237. dateend: Jiffy.parse(widget.params.dateend ?? leg.date,
  238. pattern: "ddMMMyy", isUtc: true)
  239. .subtract(days: 1)
  240. .format(pattern: "ddMMMyy"),
  241. )),
  242. child: Container(
  243. margin: const EdgeInsets.symmetric(horizontal: 15),
  244. width: double.infinity,
  245. padding: const EdgeInsets.all(5),
  246. decoration:
  247. BoxDecoration(border: Border.all(color: Colors.grey)),
  248. child: const Text(
  249. "Load Previous Day",
  250. textAlign: TextAlign.center,
  251. style: TextStyle(fontSize: 12),
  252. ),
  253. )),
  254. const Gap(20)
  255. ],
  256. if (isfirstitem)
  257. WDay(
  258. date: leg.jdate,
  259. highlight: now.yMd == leg.jdate.yMd,
  260. onTap: () => context.push("/crewlink/crewlist",
  261. extra: CrewlistPageParams(
  262. datestart: leg.jdate.format(pattern: "ddMMMyy"))),
  263. ),
  264. SingleChildScrollView(
  265. scrollDirection: Axis.horizontal,
  266. child: WCrewlist(
  267. leg: leg,
  268. ),
  269. ),
  270. Divider(
  271. height: 1,
  272. thickness: 1,
  273. color: Colors.grey[700],
  274. ),
  275. const Gap(5),
  276. if (i == legs.length - 1) ...[
  277. const Gap(20),
  278. InkWell(
  279. onTap: () => context.push("/crewlink/crewlist",
  280. extra: CrewlistPageParams(
  281. al: widget.params.al,
  282. fnum: widget.params.fnum,
  283. dep: widget.params.dep,
  284. des: widget.params.des,
  285. datestart: Jiffy.parse(widget.params.datestart ?? leg.date,
  286. pattern: "ddMMMyy", isUtc: true)
  287. .add(days: 1)
  288. .format(pattern: "ddMMMyy"),
  289. dateend: Jiffy.parse(widget.params.dateend ?? leg.date,
  290. pattern: "ddMMMyy", isUtc: true)
  291. .add(days: 1)
  292. .format(pattern: "ddMMMyy"),
  293. )),
  294. child: Container(
  295. margin: const EdgeInsets.symmetric(horizontal: 15),
  296. width: double.infinity,
  297. padding: const EdgeInsets.all(5),
  298. decoration:
  299. BoxDecoration(border: Border.all(color: Colors.grey)),
  300. child: const Text(
  301. "Load Next Day",
  302. textAlign: TextAlign.center,
  303. style: TextStyle(fontSize: 12),
  304. ),
  305. )),
  306. const Gap(20)
  307. ],
  308. ],
  309. );
  310. }
  311. void _changeDay(xcontext) async {
  312. ref.read(isLoadingProvider.notifier).state = true;
  313. final resoff = Hive.box("crewlink").get(_crewlistMinMaxKey);
  314. final reson = await ref.read(crewlinkapiProvider).showCrewMinMax();
  315. ref.read(isLoadingProvider.notifier).state = false;
  316. dynamic res;
  317. if (reson["error"] == null && reson?["data"] != null) {
  318. Hive.box("crewlink").put(_crewlistMinMaxKey, reson["data"]);
  319. res = reson["data"];
  320. } else if (resoff == null) {
  321. context.showError(reson["error"] ?? "Unknown error");
  322. return;
  323. }
  324. res = res ?? resoff;
  325. if (res != null) {
  326. final firstDate =
  327. Jiffy.parse(res["mindate"], pattern: "yyyy-MM-dd", isUtc: true);
  328. final lastDate =
  329. Jiffy.parse(res["maxdate"], pattern: "yyyy-MM-dd", isUtc: true);
  330. final initialDate = Jiffy.parse(widget.params.datestart ?? "",
  331. pattern: "ddMMMyy", isUtc: true)
  332. .max(firstDate)
  333. .min(lastDate);
  334. final DateTime? pickedDate = await showDatePicker(
  335. context: xcontext,
  336. confirmText: null,
  337. initialDate: initialDate.dateTime,
  338. firstDate: firstDate.dateTime,
  339. lastDate: lastDate.dateTime,
  340. currentDate: Jiffy.now().toUtc().dateTime,
  341. selectableDayPredicate: (day) => true,
  342. initialDatePickerMode: DatePickerMode.day,
  343. );
  344. if (pickedDate != null) {
  345. context.push("/crewlink/crewlist",
  346. extra: CrewlistPageParams(
  347. datestart: Jiffy.parseFromDateTime(pickedDate)
  348. .format(pattern: "ddMMMyy")));
  349. }
  350. }
  351. }
  352. Widget _searchFilter(BuildContext xcontext, DateTime min, DateTime max) =>
  353. Container(
  354. padding: const EdgeInsets.all(20),
  355. child: Column(
  356. mainAxisSize: MainAxisSize.min,
  357. children: [
  358. Row(
  359. children: [
  360. Expanded(
  361. child: TextField(
  362. decoration:
  363. const InputDecoration(labelText: "Departure Airport"),
  364. maxLength: 3, // Set maximum length
  365. controller: ctrldep,
  366. ),
  367. ),
  368. const SizedBox(width: 10),
  369. Expanded(
  370. child: TextField(
  371. decoration:
  372. const InputDecoration(labelText: "Arrival Airport"),
  373. maxLength: 3, // Set maximum length
  374. controller: ctrldes,
  375. ),
  376. ),
  377. ],
  378. ),
  379. const SizedBox(height: 15),
  380. Row(
  381. children: [
  382. Expanded(
  383. child: TextField(
  384. controller: ctrlairline,
  385. decoration:
  386. const InputDecoration(labelText: "Airline IATA code"),
  387. maxLength: 2, // Set maximum length
  388. ),
  389. ),
  390. const SizedBox(width: 10),
  391. Expanded(
  392. child: TextField(
  393. controller: ctrlfnum,
  394. decoration:
  395. const InputDecoration(labelText: "Flight number"),
  396. maxLength: 6, // Set maximum length
  397. ),
  398. ),
  399. ],
  400. ),
  401. const SizedBox(height: 15),
  402. Row(
  403. children: [
  404. Expanded(
  405. child: TextField(
  406. decoration:
  407. const InputDecoration(labelText: "Starting date"),
  408. controller: ctrlstartdate,
  409. readOnly:
  410. true, //set it true, so that user will not able to edit text
  411. onTap: () async {
  412. DateTime? pickedDate = await showDatePicker(
  413. context: context,
  414. initialDate: DateTime.now().toUtc(),
  415. firstDate:
  416. min, //DateTime.now () - not to allow to choose before today.
  417. lastDate: max);
  418. if (pickedDate != null) {
  419. String formattedDate =
  420. Jiffy.parseFromDateTime(pickedDate).format(
  421. pattern:
  422. "ddMMMyy"); //formatted date output using intl package => 2021-03-16
  423. setState(() {
  424. ctrlstartdate.text = formattedDate;
  425. if (Jiffy.parseFromDateTime(pickedDate).isAfter(
  426. Jiffy.parse(ctrlenddate.text,
  427. pattern: "ddMMMyy", isUtc: true))) {
  428. ctrlenddate.text = formattedDate;
  429. }
  430. });
  431. } else {
  432. print("Date is not selected");
  433. }
  434. }),
  435. ),
  436. const SizedBox(width: 10),
  437. Expanded(
  438. child: TextField(
  439. decoration:
  440. const InputDecoration(labelText: "Ending date"),
  441. controller: ctrlenddate,
  442. readOnly:
  443. true, //set it true, so that user will not able to edit text
  444. onTap: () async {
  445. DateTime? pickedDate = await showDatePicker(
  446. context: context,
  447. initialDate: DateTime.now().toUtc(),
  448. firstDate: min,
  449. lastDate: max);
  450. if (pickedDate != null) {
  451. String formattedDate =
  452. Jiffy.parseFromDateTime(pickedDate)
  453. .format(pattern: "ddMMMyy");
  454. setState(() {
  455. ctrlenddate.text = formattedDate;
  456. if (Jiffy.parseFromDateTime(pickedDate).isBefore(
  457. Jiffy.parse(ctrlstartdate.text,
  458. pattern: "ddMMMyy", isUtc: true))) {
  459. ctrlstartdate.text = formattedDate;
  460. }
  461. });
  462. } else {
  463. print("Date is not selected");
  464. }
  465. }),
  466. ),
  467. ],
  468. ),
  469. const SizedBox(height: 35),
  470. SizedBox(
  471. width: 360,
  472. child: ElevatedButton(
  473. onPressed: () =>
  474. Navigator.pop(context, true), // Close the bottom sheet
  475. style: ElevatedButton.styleFrom(
  476. backgroundColor: Colors.green, // Set button color to green
  477. shape: RoundedRectangleBorder(
  478. borderRadius:
  479. BorderRadius.circular(15), // Add rounded corners
  480. ),
  481. padding:
  482. const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
  483. ),
  484. child: const Text(
  485. 'Search Flights',
  486. style: TextStyle(fontSize: 18, color: Colors.white),
  487. ),
  488. ),
  489. ),
  490. ],
  491. ),
  492. );
  493. void _searchFlights(xcontext) async {
  494. ref.read(isLoadingProvider.notifier).state = true;
  495. final resoff = Hive.box("crewlink").get(_crewlistMinMaxKey);
  496. final reson = await ref.read(crewlinkapiProvider).showCrewMinMax();
  497. ref.read(isLoadingProvider.notifier).state = false;
  498. dynamic res;
  499. if (reson["error"] == null && reson?["data"] != null) {
  500. Hive.box("crewlink").put(_crewlistMinMaxKey, reson["data"]);
  501. res = reson["data"];
  502. } else if (resoff == null) {
  503. context.showError(reson["error"] ?? "Unknown error");
  504. return;
  505. }
  506. res = res ?? resoff;
  507. if (res != null) {
  508. ctrldep.text = widget.params.dep!;
  509. ctrldes.text = widget.params.des!;
  510. ctrlairline.text = widget.params.al!;
  511. ctrlfnum.text = widget.params.fnum!;
  512. ctrlstartdate.text = widget.params.datestart!;
  513. ctrlenddate.text = widget.params.dateend!;
  514. final out = await showModalBottomSheet(
  515. context: context,
  516. builder: (context) => _searchFilter(
  517. xcontext,
  518. Jiffy.parse(res["mindate"], isUtc: true, pattern: "yyyy-MM-dd")
  519. .dateTime,
  520. Jiffy.parse(res["maxdate"], isUtc: true, pattern: "yyyy-MM-dd")
  521. .dateTime));
  522. if (out == true) {
  523. context.push("/crewlink/crewlist",
  524. extra: CrewlistPageParams(
  525. dep: ctrldep.text.toUpperCase(),
  526. des: ctrldes.text.toUpperCase(),
  527. al: ctrlairline.text.toUpperCase(),
  528. fnum: ctrlfnum.text,
  529. datestart: ctrlstartdate.text,
  530. dateend: ctrlenddate.text,
  531. ));
  532. }
  533. }
  534. }
  535. }
  536. class CrewlistPageParams {
  537. const CrewlistPageParams({
  538. this.dateend,
  539. this.datestart,
  540. this.al,
  541. this.fnum,
  542. this.dep,
  543. this.des,
  544. this.crewlinkuser,
  545. this.crewlinkpass,
  546. });
  547. final String? dateend;
  548. final String? datestart;
  549. final String? al;
  550. final String? fnum;
  551. final String? dep;
  552. final String? des;
  553. final String? crewlinkuser;
  554. final String? crewlinkpass;
  555. }