crewlist_page.dart 21 KB

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