1
0

ftl_page.dart 28 KB

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