ftl_page.dart 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  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:modal_bottom_sheet/modal_bottom_sheet.dart';
  10. import 'package:super_sliver_list/super_sliver_list.dart';
  11. import 'package:tp5/core/basic_page.dart';
  12. import 'package:tp5/core/utils.dart';
  13. import 'package:tp5/ftl/provider/ftl.dart';
  14. import 'package:tp5/ftl/widget/w_shadowbox.dart';
  15. import 'package:tp5/roster/api/crewlink_api.dart';
  16. import 'package:tp5/roster/models/duty.dart';
  17. import 'package:tp5/roster/widgets/w_airport.dart';
  18. import 'package:tp5/roster/widgets/w_hour.dart';
  19. class FtlPage extends ConsumerStatefulWidget {
  20. const FtlPage({super.key, required this.params});
  21. final FtlPageParams params;
  22. @override
  23. ConsumerState<ConsumerStatefulWidget> createState() => _FtlPageState();
  24. }
  25. class _FtlPageState extends ConsumerState<FtlPage> {
  26. late String crewlinkUser;
  27. late String crewlinkPass;
  28. late String startdate;
  29. late String enddate;
  30. Map? roster;
  31. List<Duty> duties = [];
  32. String _rosterKey({String? clUser, String? start, String? end}) =>
  33. "roster_${clUser ?? crewlinkUser}_${start ?? startdate}_${end ?? enddate}";
  34. String fileidroster({String? clUser, String? start, String? end}) =>
  35. PathTo().crewlinkFile(
  36. "${_rosterKey(clUser: clUser, start: start, end: end)}.pdf");
  37. // String get _rosterKey =>
  38. // "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}";
  39. // String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf");
  40. final ScrollController _scrollCtrl = ScrollController();
  41. final ListController _listCtrl = ListController();
  42. late Jiffy now;
  43. @override
  44. void initState() {
  45. crewlinkUser = widget.params.crewlinkuser ??
  46. Hive.box("profile").get("crewlink_user") ??
  47. "";
  48. crewlinkPass = widget.params.crewlinkpass ??
  49. Hive.box("profile").get("crewlink_pass") ??
  50. "";
  51. startdate = widget.params.datestart ??
  52. Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy");
  53. enddate = widget.params.dateend ??
  54. Jiffy.now()
  55. .toUtc()
  56. //.add(months: 1)
  57. .endOf(Unit.month)
  58. .format(pattern: "ddMMMyy");
  59. Future.delayed(Duration.zero, () async {
  60. await _loadMinMax();
  61. //await _loadRoster();
  62. await _loadOldRoster();
  63. await _ftlCalc(base: 'DJE');
  64. }).then(
  65. (value) => Future.delayed(const Duration(milliseconds: 200), () async {
  66. _scrollToDate();
  67. }));
  68. super.initState();
  69. }
  70. bool _isLoading = false;
  71. Future<Map?> _loadRosterOnline(
  72. {String? clUser, String? start, String? end}) async {
  73. //print("FtlPage: Requesting ONline roster $start -> $end");
  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: start ?? startdate
  85. //??Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy")
  86. ,
  87. end: end ?? enddate
  88. //??Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy")
  89. ,
  90. fileid: fileidroster(clUser: clUser, start: start, end: end));
  91. if (roster?["data"]?["msg"] != null) {
  92. context.showError(roster?["data"]?["msg"] ?? "Unknown error");
  93. }
  94. if (roster["error"] == null && roster["data"] != null) {
  95. if (roster["msg"] != null) context.showAlert(roster["msg"]);
  96. Hive.box("crewlink")
  97. .put(_rosterKey(clUser: clUser, start: start, end: end), roster);
  98. return roster;
  99. }
  100. return null;
  101. }
  102. Future<Map?> _loadRosterOffline(
  103. {String? clUser, String? start, String? end}) async {
  104. //print("FtlPage: Requesting OFFline roster $start -> $end");
  105. return Hive.box("crewlink")
  106. .get(_rosterKey(clUser: clUser, start: start, end: end));
  107. }
  108. _loadRoster(
  109. {String? clUser,
  110. String? start,
  111. String? end,
  112. bool cachefirst = false}) async {
  113. try {
  114. ref.read(isLoadingProvider.notifier).state = true;
  115. final rosterOffline =
  116. await _loadRosterOffline(clUser: clUser, start: start, end: end);
  117. if (rosterOffline?["data"]?["decoded"] is Map) {
  118. print(
  119. "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}");
  120. _convertRoster(rosterOffline);
  121. setState(() {});
  122. }
  123. if (rosterOffline?["data"]?["decoded"] is! Map || !cachefirst) {
  124. final rosterOnline =
  125. await _loadRosterOnline(clUser: clUser, start: start, end: end);
  126. if (rosterOnline?["data"]?["decoded"] is Map) {
  127. print(
  128. "FTL Page: found online: $start ${rosterOnline?["data"]?["decoded"] is Map}");
  129. _convertRoster(rosterOnline);
  130. setState(() {});
  131. } else if (rosterOffline?["data"]?["decoded"] is Map) {
  132. print(
  133. "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}");
  134. _convertRoster(rosterOnline);
  135. setState(() {});
  136. }
  137. }
  138. } finally {
  139. ref.read(isLoadingProvider.notifier).state = false;
  140. }
  141. }
  142. _loadOldRoster() async {
  143. bool exitnow = false;
  144. // _rosterMax = Jiffy.now().toUtc().endOf(Unit.month).subtract(hours: 10);
  145. Jiffy date1 = Jiffy.parse(startdate, pattern: "ddMMMyy")
  146. .subtract(months: 11)
  147. .startOf(Unit.day)
  148. .startOf(Unit.month)
  149. .max(_rosterMin ?? Jiffy.now().toUtc().startOf(Unit.month));
  150. while (!exitnow) {
  151. Jiffy date2 = date1
  152. .endOf(Unit.month)
  153. .min(_rosterMax?.endOf(Unit.day) ?? Jiffy.now().toUtc().add(days: 3));
  154. if (date2.isAfter(date1) &&
  155. date2.endOf(Unit.month) == date1.endOf(Unit.month)) {
  156. //print("ftlpage: loadoldroster: loading ${DTInterval(date1, date2)}");
  157. await _loadRoster(
  158. start: date1.format(pattern: "ddMMMyy"),
  159. end: date2.format(pattern: "ddMMMyy"),
  160. cachefirst: date1
  161. .endOf(Unit.month)
  162. .isBefore(Jiffy.now().endOf(Unit.month)));
  163. } else {
  164. exitnow = true;
  165. }
  166. date1 = date1.add(months: 1).startOf(Unit.month);
  167. }
  168. }
  169. String get _rosterMinMaxKey => "minmax_$crewlinkUser";
  170. _loadMinMax() async {
  171. final resoff = Hive.box("crewlink").get(_rosterMinMaxKey);
  172. //print("ftlpage: resoff: $resoff");
  173. final reson = await ref.read(crewlinkapiProvider).rosterMinMax();
  174. //print("ftlpage: reson: $reson");
  175. ref.read(isLoadingProvider.notifier).state = false;
  176. dynamic res;
  177. if (reson != null && reson["error"] == null && reson?["data"] != null) {
  178. Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]);
  179. res = reson["data"];
  180. } else if (resoff == null) {
  181. //print(reson["error"] ?? "Unknown error");
  182. }
  183. res = res ?? resoff;
  184. _rosterMin =
  185. Jiffy.parse(res["mindate"], pattern: "yyyy-MM-dd", isUtc: true);
  186. _rosterMax =
  187. Jiffy.parse(res["maxdate"], pattern: "yyyy-MM-dd", isUtc: true);
  188. }
  189. Jiffy? _rosterMin;
  190. Jiffy? _rosterMax;
  191. _scrollToDate({Jiffy? date}) async {
  192. final jdate = date ?? Jiffy.now().toUtc();
  193. bool found = false;
  194. int id = 0;
  195. for (FtlDuty duty in dutiestodisplay) {
  196. // if (duty.jdate.yMd == jdate.yMd) {
  197. if (duty.start!.isSameOrAfter(jdate)) {
  198. found = true;
  199. break;
  200. }
  201. id++;
  202. }
  203. if (found && mounted && _scrollCtrl.hasClients) {
  204. // await _scrollCtrl.scrollToIndex(duties.length - 30,
  205. // duration: const Duration(milliseconds: 500),
  206. // preferPosition: AutoScrollPosition.end);
  207. _listCtrl.animateToItem(
  208. duration: (d) => const Duration(milliseconds: 700),
  209. index: id,
  210. scrollController: _scrollCtrl,
  211. alignment: 0.75,
  212. curve: (double estimatedDistance) => Curves.decelerate,
  213. );
  214. }
  215. }
  216. int i = 0;
  217. _convertRoster(Map? input) {
  218. roster = input;
  219. final decoded = (input?["data"]?["decoded"]?["roster"] ?? {});
  220. for (var date in decoded.keys) {
  221. duties = duties.where((e) => e.date != date).toList();
  222. var dutylist = (decoded[date] as List);
  223. for (var duty in dutylist) {
  224. i++;
  225. duties.add(
  226. Duty(date: date, type: duty["type"], data: duty["data"], order: i));
  227. //print("${(duties.last).jdate.yMd} ${(duties.last).type} ${(duties.last).start?.Hm} ${(duties.last).end?.Hm}");
  228. }
  229. }
  230. }
  231. Ftl ftldata = Ftl.fromCrewlink(clDuties: [], base: 'DJE');
  232. Jiffy ftlcalcdate = Jiffy.now().subtract(days: 30).startOf(Unit.month);
  233. _ftlCalc({required String base}) async {
  234. ref.read(isLoadingProvider.notifier).state = true;
  235. ftldata = Ftl.fromCrewlink(clDuties: duties, base: base);
  236. //await compute(ftlCalc, {"clDuties": duties, "base": base});
  237. // await ftldata.calcduties(date: null);
  238. await ftldata.calcduties(date: ftlcalcdate);
  239. ref.read(isLoadingProvider.notifier).state = false;
  240. }
  241. List<FtlDuty> get dutiestodisplay => ftldata.duties
  242. .whereIndexed((i, e) => e.start!.isSameOrAfter(ftlcalcdate))
  243. .toList();
  244. @override
  245. Widget build(BuildContext context) {
  246. return BasicPage(
  247. actions: [
  248. ElevatedButton(
  249. onPressed: _isLoading
  250. ? null
  251. : () async {
  252. setState(() {
  253. _isLoading = true;
  254. });
  255. await _ftlCalc(base: "DJE");
  256. setState(() {
  257. _isLoading = false;
  258. });
  259. },
  260. child: const Text("calc"))
  261. ],
  262. title: "FTL",
  263. body: ftldata.duties.isEmpty
  264. ? const Text("Please wait, calculations in progress...")
  265. : SuperListView.builder(
  266. itemCount: dutiestodisplay.length,
  267. itemBuilder: (contexte, index) => _getItem(contexte, index),
  268. listController: _listCtrl,
  269. shrinkWrap: false,
  270. controller: _scrollCtrl,
  271. ),
  272. );
  273. }
  274. bool _is36h(DTInterval? e, String station) {
  275. if (e == null) return false;
  276. final firstnight = DTInterval.fromHm(
  277. apartir: Ftl.changeTz(e.start, station).toUtc(),
  278. h: 23,
  279. m: 0,
  280. duration: const Duration(hours: 7, minutes: 59),
  281. ap: station);
  282. final secondnight =
  283. DTInterval(firstnight.start.add(days: 1), firstnight.end.add(days: 1));
  284. if (e.duration.inHours >= 36 &&
  285. e.contains(firstnight) &&
  286. e.contains(secondnight)) {
  287. return true;
  288. } else {
  289. return false;
  290. }
  291. }
  292. bool _is48h(DTInterval? e, String station) {
  293. if (e == null) return false;
  294. final localdays = DTInterval.fromHm(
  295. apartir: Ftl.changeTz(e.start, station).toUtc(),
  296. h: 0,
  297. m: 0,
  298. duration: const Duration(hours: 47, minutes: 59),
  299. ap: station);
  300. if (e.contains(localdays)) {
  301. return true;
  302. } else {
  303. return false;
  304. }
  305. }
  306. _showDutyInfo(int i) {
  307. final duty = dutiestodisplay[i];
  308. //print("ftlpage: ${duty.interval} ${duty.type}");
  309. final margin =
  310. ((duty.fdpExt ? duty.fdpExtMax : duty.fdpMax) ?? Duration.zero)
  311. .subtract(duty.fdpLength);
  312. showMaterialModalBottomSheet(
  313. bounce: true,
  314. context: context,
  315. builder: (context) => Container(
  316. padding: const EdgeInsets.all(10),
  317. decoration: const BoxDecoration(
  318. border: Border(top: BorderSide(width: 3, color: Colors.white70))),
  319. //height: 250,
  320. child: SingleChildScrollView(
  321. child: Column(
  322. children: [
  323. const Gap(5),
  324. ElevatedButton(
  325. onPressed: () {
  326. context.pop();
  327. },
  328. child: const Text(
  329. "Close",
  330. style: TextStyle(
  331. color: Colors.red, fontWeight: FontWeight.w700),
  332. )),
  333. const Gap(5),
  334. const Text(
  335. "Duty FTL Details",
  336. style: TextStyle(fontSize: 22),
  337. ),
  338. const Gap(10),
  339. Row(
  340. children: [
  341. const Text("Duty:",
  342. style: TextStyle(fontWeight: FontWeight.w800)),
  343. const Gap(20),
  344. _titleInfo(duty.start!.yMMMEd, duty.start!.Hm),
  345. const Gap(20),
  346. _titleInfo(duty.start!.yMMMEd, duty.end!.Hm)
  347. ],
  348. ),
  349. const Gap(20),
  350. if (duty.fdpStart != null)
  351. Row(
  352. children: [
  353. const Text("FDP:",
  354. style: TextStyle(fontWeight: FontWeight.w800)),
  355. const Gap(20),
  356. _titleInfo("FDP Length", duty.fdpLength.tohhmm),
  357. const Gap(20),
  358. _titleInfo("Fdp Max", duty.fdpMax!.tohhmm),
  359. const Gap(20),
  360. _titleInfo("Fdp Ext Max", duty.fdpExtMax!.tohhmm),
  361. const Gap(20),
  362. _titleInfo(margin.isNegative ? "Exceeded" : "Margin",
  363. margin.abs().tohhmm,
  364. color: margin.isNegative ? Colors.red : Colors.green,
  365. sizeinfo: 12),
  366. ],
  367. ),
  368. const Gap(20),
  369. if (duty.fdpStart != null)
  370. Row(
  371. children: [
  372. const Text("Acclim:",
  373. style: TextStyle(fontWeight: FontWeight.w800)),
  374. const Gap(20),
  375. _titleInfo("Acclim type", duty.acclim),
  376. const Gap(20),
  377. _titleInfo("Ref time", duty.reftime),
  378. ],
  379. ),
  380. const Gap(20),
  381. if (duty.fdpStart != null)
  382. Row(
  383. children: [
  384. const Text("Report:",
  385. style: TextStyle(fontWeight: FontWeight.w800)),
  386. const Gap(10),
  387. _titleInfo("Report time", duty.start?.Hm ?? "----"),
  388. const Gap(10),
  389. _titleInfo(
  390. "Delayed Report1", duty.reportdelay1?.Hm ?? "----"),
  391. const Gap(10),
  392. _titleInfo(
  393. "Delayed Report1", duty.reportdelay2?.Hm ?? "----"),
  394. ],
  395. ),
  396. const Gap(20),
  397. if (duty.fdpStart != null &&
  398. (duty.lateFinish || duty.earlyStart || duty.nightDuty))
  399. Row(
  400. children: [
  401. const Text("Disruptive:",
  402. style: TextStyle(fontWeight: FontWeight.w800)),
  403. const Gap(10),
  404. if (duty.earlyStart)
  405. const Text("early start duty",
  406. style: TextStyle(
  407. fontWeight: FontWeight.w800,
  408. fontSize: 12,
  409. color: Colors.cyan)),
  410. const Gap(10),
  411. if (duty.lateFinish)
  412. const Text("late finish duty",
  413. style: TextStyle(
  414. fontWeight: FontWeight.w800,
  415. fontSize: 12,
  416. color: Colors.cyan)),
  417. const Gap(10),
  418. if (duty.nightDuty)
  419. const Text("night duty",
  420. style: TextStyle(
  421. fontWeight: FontWeight.w800,
  422. fontSize: 12,
  423. color: Colors.cyan)),
  424. const Gap(10),
  425. ],
  426. ),
  427. const Gap(20),
  428. Row(
  429. children: [
  430. SingleChildScrollView(
  431. scrollDirection: Axis.horizontal,
  432. child: Row(
  433. mainAxisSize: MainAxisSize.max,
  434. mainAxisAlignment: MainAxisAlignment.start,
  435. children: [
  436. const Text("Detail:",
  437. style: TextStyle(fontWeight: FontWeight.w800)),
  438. ...(duty.dutiesDetailsIndex.map((e) {
  439. final detail = ftldata.dutiesDetails[e];
  440. final label = (detail.placeEnd != null)
  441. ? "${detail.placeStart}-${detail.placeEnd}"
  442. : "${detail.label}";
  443. return [
  444. const Gap(20),
  445. _titleInfo(label,
  446. "${detail.start?.Hm}-${detail.end?.Hm}",
  447. sizeinfo: 10)
  448. ];
  449. }).flattened),
  450. ]),
  451. ),
  452. ],
  453. ),
  454. const Gap(20),
  455. Row(
  456. children: [
  457. const Text("Tot duties hrs:",
  458. style: TextStyle(fontWeight: FontWeight.w800)),
  459. const Gap(10),
  460. _titleInfo("7 days",
  461. "${duty.dutyTotal?.dutylast7?.tohhmm}|${Ftl.maxdutylast7.tohhmm}",
  462. sizeinfo: 10),
  463. const Gap(5),
  464. _titleInfo("14 days",
  465. "${duty.dutyTotal?.dutylast14?.tohhmm}|${Ftl.maxdutylast14.tohhmm}",
  466. sizeinfo: 10),
  467. const Gap(5),
  468. _titleInfo("28 days",
  469. "${duty.dutyTotal?.dutylast28?.tohhmm}|${Ftl.maxdutylast28.tohhmm}",
  470. sizeinfo: 10),
  471. ],
  472. ),
  473. const Gap(20),
  474. Row(
  475. children: [
  476. const Text("Tot Flt hrs:",
  477. style: TextStyle(fontWeight: FontWeight.w800)),
  478. const Gap(10),
  479. _titleInfo("28 days",
  480. "${duty.dutyTotal?.fltlast28?.tohhmm}|${Ftl.maxfltlast28.tohhmm}",
  481. sizeinfo: 10),
  482. const Gap(5),
  483. _titleInfo("12 months",
  484. "${duty.dutyTotal?.fltlast12?.tohhmm}|${Ftl.maxfltlast12.tohhmm}",
  485. sizeinfo: 10),
  486. const Gap(5),
  487. _titleInfo("year",
  488. "${duty.dutyTotal?.fltyear?.tohhmm}|${Ftl.maxfltlastyear.tohhmm}",
  489. sizeinfo: 10),
  490. ],
  491. ),
  492. ],
  493. ),
  494. ),
  495. ),
  496. );
  497. }
  498. _titleInfo(String? title, String? info,
  499. {double sizetitle = 10,
  500. double sizeinfo = 16,
  501. Color color = Colors.blueGrey}) =>
  502. Column(
  503. children: [
  504. Text(
  505. title ?? "---",
  506. style: TextStyle(color: Colors.grey, fontSize: sizetitle),
  507. ),
  508. Text(info ?? "---",
  509. style: TextStyle(color: color, fontSize: sizeinfo)),
  510. ],
  511. );
  512. Widget _getItem(BuildContext _, int i) {
  513. final duty = dutiestodisplay.elementAt(i);
  514. final lastduty = i == 0 ? null : dutiestodisplay.elementAt(i - 1);
  515. final rest =
  516. lastduty == null ? null : DTInterval(lastduty.end!, duty.start!);
  517. Color legal;
  518. if (duty.legals.isEmpty) {
  519. legal = Colors.green[900]!;
  520. } else if (duty.legals.every((e) => (e.condition ?? "") != "")) {
  521. legal = Colors.amber;
  522. } else {
  523. legal = Colors.red;
  524. }
  525. return Column(
  526. children: [
  527. if (lastduty != null)
  528. WShadowbox(
  529. child: Column(children: [
  530. Container(
  531. decoration: const BoxDecoration(
  532. color: Color.fromARGB(255, 8, 9, 9),
  533. border: Border(top: BorderSide(width: 5)),
  534. borderRadius: BorderRadius.all(Radius.circular(10.0)),
  535. ),
  536. padding: const EdgeInsets.all(5),
  537. // width: double.infinity,
  538. child: Row(
  539. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  540. children: [
  541. Column(
  542. children: [
  543. _titleInfo("Rest",
  544. "${(rest?.duration.inDays ?? 0)} days, ${(rest?.duration.inHours ?? 0) % 24} hours, ${(rest?.duration.inMinutes ?? 0) % 60} minutes",
  545. sizeinfo: 10),
  546. ],
  547. ),
  548. Column(children: [
  549. if (_is48h(rest, lastduty.placeEnd ?? "UTC"))
  550. Container(
  551. color: Colors.teal[900],
  552. padding: const EdgeInsets.all(5),
  553. child: Text(
  554. "48h with 2 local days",
  555. style: TextStyle(
  556. color: Colors.yellow[600]!,
  557. fontSize: 11,
  558. fontWeight: FontWeight.w600),
  559. )),
  560. if (_is36h(rest, lastduty.placeEnd ?? "UTC"))
  561. Container(
  562. color: Colors.teal[800],
  563. padding: const EdgeInsets.all(5),
  564. child: Text(
  565. "36h with 2 local nights",
  566. style: TextStyle(
  567. color: Colors.yellow[500]!,
  568. fontSize: 11,
  569. fontWeight: FontWeight.w600),
  570. ))
  571. ]),
  572. ],
  573. ),
  574. )
  575. ])),
  576. InkWell(
  577. onTap: () => _showDutyInfo(i),
  578. child: WShadowbox(
  579. child: Column(
  580. children: [
  581. Container(
  582. decoration: BoxDecoration(
  583. color: Color.fromARGB(
  584. duty.start!.isAfter(Jiffy.now()) ? 255 : 70, 1, 45, 47),
  585. border: Border(top: BorderSide(width: 5, color: legal)),
  586. borderRadius:
  587. const BorderRadius.vertical(top: Radius.circular(10.0)),
  588. ),
  589. padding: const EdgeInsets.symmetric(horizontal: 20),
  590. // width: double.infinity,
  591. child: Row(
  592. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  593. children: [
  594. Text(
  595. "${duty.start?.format(pattern: "dd MMM'yy")}",
  596. style: const TextStyle(color: Colors.white54),
  597. ),
  598. if (duty.start != null) WHour(jiffy: duty.start!),
  599. ],
  600. ),
  601. ),
  602. Container(
  603. width: double.infinity,
  604. padding: const EdgeInsets.only(left: 20, right: 10),
  605. decoration: const BoxDecoration(
  606. color: Colors.black,
  607. ),
  608. child: Column(
  609. crossAxisAlignment: CrossAxisAlignment.start,
  610. children: duty.legals
  611. .map((e) => Text(
  612. "+ ${e.condition ?? ""} ${e.legalCauseMsg}",
  613. style: const TextStyle(fontSize: 10),
  614. ))
  615. .toList()),
  616. ),
  617. //details
  618. ...duty.dutiesDetailsIndex.map((index) {
  619. final FtlDutyDetails dutydetail =
  620. ftldata.dutiesDetails[index];
  621. return Row(
  622. children: [
  623. const Gap(40),
  624. SizedBox(
  625. width: 50,
  626. child: Row(children: [
  627. Text(
  628. dutydetail.typeString,
  629. style: TextStyle(
  630. backgroundColor: Colors.blueGrey[900],
  631. color: Colors.yellow,
  632. fontSize: 10,
  633. fontWeight: FontWeight.w600,
  634. ),
  635. )
  636. ]),
  637. ),
  638. SizedBox(
  639. width: 60,
  640. child: Row(
  641. children: [
  642. WHour(
  643. jiffy: dutydetail.start!,
  644. size: 12,
  645. color: Colors.white38,
  646. ),
  647. const Gap(5),
  648. WHour(
  649. jiffy: dutydetail.end!,
  650. size: 12,
  651. color: Colors.white38,
  652. ),
  653. ],
  654. ),
  655. ),
  656. SizedBox(
  657. // width: 80,
  658. child: Row(children: [
  659. const Gap(10),
  660. if (dutydetail.placeStart != null)
  661. WAirport(iata: dutydetail.placeStart!, size: 12),
  662. if (dutydetail.placeEnd != null) ...[
  663. const Icon(Icons.arrow_right_alt, size: 9),
  664. WAirport(iata: dutydetail.placeEnd!, size: 12)
  665. ] else if ((dutydetail.label ?? "") != "")
  666. Text(" (${dutydetail.label})",
  667. style: const TextStyle(
  668. color: Colors.white54,
  669. fontSize: 10,
  670. letterSpacing: 2),
  671. overflow: TextOverflow.ellipsis),
  672. ]),
  673. ),
  674. ],
  675. );
  676. }),
  677. Container(
  678. decoration: BoxDecoration(
  679. color: Color.fromARGB(
  680. duty.start!.isAfter(Jiffy.now()) ? 255 : 70, 1, 45, 47),
  681. borderRadius:
  682. BorderRadius.vertical(bottom: Radius.circular(10.0)),
  683. ),
  684. padding: const EdgeInsets.symmetric(horizontal: 20),
  685. // width: double.infinity,
  686. child: Row(
  687. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  688. children: [
  689. if (duty.end != null && duty.start != null)
  690. _titleInfo(
  691. "Duty length",
  692. duty.end!.dateTime
  693. .difference(duty.start!.dateTime)
  694. .tohhmm),
  695. const Row(children: []),
  696. Column(
  697. mainAxisSize: MainAxisSize.min,
  698. children: [
  699. Text(
  700. (duty.end?.yMMMd != duty.start?.yMMMd)
  701. ? "${duty.end?.format(pattern: "dd MMM")}"
  702. : "",
  703. style: const TextStyle(
  704. color: Colors.white54, fontSize: 12),
  705. ),
  706. if (duty.end != null) WHour(jiffy: duty.end!),
  707. ],
  708. ),
  709. ],
  710. ),
  711. ),
  712. ],
  713. ),
  714. ),
  715. ),
  716. ],
  717. );
  718. }
  719. }
  720. class FtlPageParams {
  721. const FtlPageParams({
  722. this.dateend,
  723. this.datestart,
  724. this.crewlinkuser,
  725. this.crewlinkpass,
  726. });
  727. final String? dateend;
  728. final String? datestart;
  729. final String? crewlinkuser;
  730. final String? crewlinkpass;
  731. }