| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 | // ignore_for_file: use_build_context_synchronouslyimport 'package:collection/collection.dart';import 'package:flutter/material.dart';import 'package:flutter_riverpod/flutter_riverpod.dart';import 'package:gap/gap.dart';import 'package:go_router/go_router.dart';import 'package:hive_flutter/hive_flutter.dart';import 'package:jiffy/jiffy.dart';import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';import 'package:super_sliver_list/super_sliver_list.dart';import 'package:tp5/core/basic_page.dart';import 'package:tp5/core/utils.dart';import 'package:tp5/ftl/provider/ftl.dart';import 'package:tp5/ftl/widget/w_shadowbox.dart';import 'package:tp5/roster/api/crewlink_api.dart';import 'package:tp5/roster/models/duty.dart';import 'package:tp5/roster/widgets/w_airport.dart';import 'package:tp5/roster/widgets/w_hour.dart';class FtlPage extends ConsumerStatefulWidget {  const FtlPage({super.key, required this.params});  final FtlPageParams params;  @override  ConsumerState<ConsumerStatefulWidget> createState() => _FtlPageState();}class _FtlPageState extends ConsumerState<FtlPage> {  late String crewlinkUser;  late String crewlinkPass;  late String startdate;  late String enddate;  Map? roster;  List<Duty> duties = [];  String _rosterKey({String? clUser, String? start, String? end}) =>      "roster_${clUser ?? crewlinkUser}_${start ?? startdate}_${end ?? enddate}";  String fileidroster({String? clUser, String? start, String? end}) =>      PathTo().crewlinkFile(          "${_rosterKey(clUser: clUser, start: start, end: end)}.pdf");  // String get _rosterKey =>  //     "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}";  // String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf");  final ScrollController _scrollCtrl = ScrollController();  final ListController _listCtrl = ListController();  late Jiffy now;  @override  void initState() {    crewlinkUser = widget.params.crewlinkuser ??        Hive.box("profile").get("crewlink_user") ??        "";    crewlinkPass = widget.params.crewlinkpass ??        Hive.box("profile").get("crewlink_pass") ??        "";    startdate = widget.params.datestart ??        Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy");    enddate = widget.params.dateend ??        Jiffy.now()            .toUtc()            //.add(months: 1)            .endOf(Unit.month)            .format(pattern: "ddMMMyy");    Future.delayed(Duration.zero, () async {      await _loadMinMax();      //await _loadRoster();      await _loadOldRoster();      await _ftlCalc(base: 'DJE');    }).then(        (value) => Future.delayed(const Duration(milliseconds: 200), () async {              _scrollToDate();            }));    super.initState();  }  bool _isLoading = false;  Future<Map?> _loadRosterOnline(      {String? clUser, String? start, String? end}) async {    //print("FtlPage: Requesting ONline roster $start -> $end");    if (!ref.read(crewlinkapiProvider).logged) {      final login = await ref          .read(crewlinkapiProvider)          .login(username: crewlinkUser, password: crewlinkPass);      if (login["data"]?["logged"] != true) {        context.showError(login["error"] ?? "Unknown error");        return null;      }    }    final roster = await ref.read(crewlinkapiProvider).roster(        start: start ?? startdate        //??Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy")        ,        end: end ?? enddate        //??Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy")        ,        fileid: fileidroster(clUser: clUser, start: start, end: end));    if (roster?["data"]?["msg"] != null) {      context.showError(roster?["data"]?["msg"] ?? "Unknown error");    }    if (roster["error"] == null && roster["data"] != null) {      if (roster["msg"] != null) context.showAlert(roster["msg"]);      Hive.box("crewlink")          .put(_rosterKey(clUser: clUser, start: start, end: end), roster);      return roster;    }    return null;  }  Future<Map?> _loadRosterOffline(      {String? clUser, String? start, String? end}) async {    //print("FtlPage: Requesting OFFline roster $start -> $end");    return Hive.box("crewlink")        .get(_rosterKey(clUser: clUser, start: start, end: end));  }  _loadRoster(      {String? clUser,      String? start,      String? end,      bool cachefirst = false}) async {    try {      ref.read(isLoadingProvider.notifier).state = true;      final rosterOffline =          await _loadRosterOffline(clUser: clUser, start: start, end: end);      if (rosterOffline?["data"]?["decoded"] is Map) {        print(            "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}");        _convertRoster(rosterOffline);        setState(() {});      }      if (rosterOffline?["data"]?["decoded"] is! Map || !cachefirst) {        final rosterOnline =            await _loadRosterOnline(clUser: clUser, start: start, end: end);        if (rosterOnline?["data"]?["decoded"] is Map) {          print(              "FTL Page: found online: $start ${rosterOnline?["data"]?["decoded"] is Map}");          _convertRoster(rosterOnline);          setState(() {});        } else if (rosterOffline?["data"]?["decoded"] is Map) {          print(              "FTL Page: found offline: $start ${rosterOffline?["data"]?["decoded"] is Map}");          _convertRoster(rosterOnline);          setState(() {});        }      }    } finally {      ref.read(isLoadingProvider.notifier).state = false;    }  }  _loadOldRoster() async {    bool exitnow = false;    // _rosterMax = Jiffy.now().toUtc().endOf(Unit.month).subtract(hours: 10);    Jiffy date1 = Jiffy.parse(startdate, pattern: "ddMMMyy")        .subtract(months: 11)        .startOf(Unit.day)        .startOf(Unit.month)        .max(_rosterMin ?? Jiffy.now().toUtc().startOf(Unit.month));    while (!exitnow) {      Jiffy date2 = date1          .endOf(Unit.month)          .min(_rosterMax?.endOf(Unit.day) ?? Jiffy.now().toUtc().add(days: 3));      if (date2.isAfter(date1) &&          date2.endOf(Unit.month) == date1.endOf(Unit.month)) {        //print("ftlpage: loadoldroster: loading ${DTInterval(date1, date2)}");        await _loadRoster(            start: date1.format(pattern: "ddMMMyy"),            end: date2.format(pattern: "ddMMMyy"),            cachefirst: date1                .endOf(Unit.month)                .isBefore(Jiffy.now().endOf(Unit.month)));      } else {        exitnow = true;      }      date1 = date1.add(months: 1).startOf(Unit.month);    }  }  String get _rosterMinMaxKey => "minmax_$crewlinkUser";  _loadMinMax() async {    final resoff = Hive.box("crewlink").get(_rosterMinMaxKey);    //print("ftlpage: resoff: $resoff");    final reson = await ref.read(crewlinkapiProvider).rosterMinMax();    //print("ftlpage: reson: $reson");    ref.read(isLoadingProvider.notifier).state = false;    dynamic res;    if (reson != null && reson["error"] == null && reson?["data"] != null) {      Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]);      res = reson["data"];    } else if (resoff == null) {      //print(reson["error"] ?? "Unknown error");    }    res = res ?? resoff;    _rosterMin =        Jiffy.parse(res["mindate"], pattern: "yyyy-MM-dd", isUtc: true);    _rosterMax =        Jiffy.parse(res["maxdate"], pattern: "yyyy-MM-dd", isUtc: true);  }  Jiffy? _rosterMin;  Jiffy? _rosterMax;  _scrollToDate({Jiffy? date}) async {    final jdate = date ?? Jiffy.now().toUtc();    bool found = false;    int id = 0;    for (FtlDuty duty in dutiestodisplay) {      // if (duty.jdate.yMd == jdate.yMd) {      if (duty.start!.isSameOrAfter(jdate)) {        found = true;        break;      }      id++;    }    if (found && mounted && _scrollCtrl.hasClients) {      // await _scrollCtrl.scrollToIndex(duties.length - 30,      //     duration: const Duration(milliseconds: 500),      //     preferPosition: AutoScrollPosition.end);      _listCtrl.animateToItem(        duration: (d) => const Duration(milliseconds: 700),        index: id,        scrollController: _scrollCtrl,        alignment: 0.75,        curve: (double estimatedDistance) => Curves.decelerate,      );    }  }  int i = 0;  _convertRoster(Map? input) {    roster = input;    final decoded = (input?["data"]?["decoded"]?["roster"] ?? {});    for (var date in decoded.keys) {      duties = duties.where((e) => e.date != date).toList();      var dutylist = (decoded[date] as List);      for (var duty in dutylist) {        i++;        duties.add(            Duty(date: date, type: duty["type"], data: duty["data"], order: i));        //print("${(duties.last).jdate.yMd} ${(duties.last).type} ${(duties.last).start?.Hm} ${(duties.last).end?.Hm}");      }    }  }  Ftl ftldata = Ftl.fromCrewlink(clDuties: [], base: 'DJE');  Jiffy ftlcalcdate = Jiffy.now().subtract(days: 30).startOf(Unit.month);  _ftlCalc({required String base}) async {    ref.read(isLoadingProvider.notifier).state = true;    ftldata = Ftl.fromCrewlink(clDuties: duties, base: base);    //await compute(ftlCalc, {"clDuties": duties, "base": base});    // await ftldata.calcduties(date: null);    await ftldata.calcduties(date: ftlcalcdate);    ref.read(isLoadingProvider.notifier).state = false;  }  List<FtlDuty> get dutiestodisplay => ftldata.duties      .whereIndexed((i, e) => e.start!.isSameOrAfter(ftlcalcdate))      .toList();  @override  Widget build(BuildContext context) {    return BasicPage(      actions: [        ElevatedButton(            onPressed: _isLoading                ? null                : () async {                    setState(() {                      _isLoading = true;                    });                    await _ftlCalc(base: "DJE");                    setState(() {                      _isLoading = false;                    });                  },            child: const Text("calc"))      ],      title: "FTL",      body: ftldata.duties.isEmpty          ? const Text("Please wait, calculations in progress...")          : SuperListView.builder(              itemCount: dutiestodisplay.length,              itemBuilder: (contexte, index) => _getItem(contexte, index),              listController: _listCtrl,              shrinkWrap: false,              controller: _scrollCtrl,            ),    );  }  bool _is36h(DTInterval? e, String station) {    if (e == null) return false;    final firstnight = DTInterval.fromHm(        apartir: Ftl.changeTz(e.start, station).toUtc(),        h: 23,        m: 0,        duration: const Duration(hours: 7, minutes: 59),        ap: station);    final secondnight =        DTInterval(firstnight.start.add(days: 1), firstnight.end.add(days: 1));    if (e.duration.inHours >= 36 &&        e.contains(firstnight) &&        e.contains(secondnight)) {      return true;    } else {      return false;    }  }  bool _is48h(DTInterval? e, String station) {    if (e == null) return false;    final localdays = DTInterval.fromHm(        apartir: Ftl.changeTz(e.start, station).toUtc(),        h: 0,        m: 0,        duration: const Duration(hours: 47, minutes: 59),        ap: station);    if (e.contains(localdays)) {      return true;    } else {      return false;    }  }  _showDutyInfo(int i) {    final duty = dutiestodisplay[i];    //print("ftlpage: ${duty.interval}  ${duty.type}");    final margin =        ((duty.fdpExt ? duty.fdpExtMax : duty.fdpMax) ?? Duration.zero)            .subtract(duty.fdpLength);    showMaterialModalBottomSheet(      bounce: true,      context: context,      builder: (context) => Container(        padding: const EdgeInsets.all(10),        decoration: const BoxDecoration(            border: Border(top: BorderSide(width: 3, color: Colors.white70))),        //height: 250,        child: SingleChildScrollView(          child: Column(            children: [              const Gap(5),              ElevatedButton(                  onPressed: () {                    context.pop();                  },                  child: const Text(                    "Close",                    style: TextStyle(                        color: Colors.red, fontWeight: FontWeight.w700),                  )),              const Gap(5),              const Text(                "Duty FTL Details",                style: TextStyle(fontSize: 22),              ),              const Gap(10),              Row(                children: [                  const Text("Duty:",                      style: TextStyle(fontWeight: FontWeight.w800)),                  const Gap(20),                  _titleInfo(duty.start!.yMMMEd, duty.start!.Hm),                  const Gap(20),                  _titleInfo(duty.start!.yMMMEd, duty.end!.Hm)                ],              ),              const Gap(20),              if (duty.fdpStart != null)                Row(                  children: [                    const Text("FDP:",                        style: TextStyle(fontWeight: FontWeight.w800)),                    const Gap(20),                    _titleInfo("FDP Length", duty.fdpLength.tohhmm),                    const Gap(20),                    _titleInfo("Fdp Max", duty.fdpMax!.tohhmm),                    const Gap(20),                    _titleInfo("Fdp Ext Max", duty.fdpExtMax!.tohhmm),                    const Gap(20),                    _titleInfo(margin.isNegative ? "Exceeded" : "Margin",                        margin.abs().tohhmm,                        color: margin.isNegative ? Colors.red : Colors.green,                        sizeinfo: 12),                  ],                ),              const Gap(20),              if (duty.fdpStart != null)                Row(                  children: [                    const Text("Acclim:",                        style: TextStyle(fontWeight: FontWeight.w800)),                    const Gap(20),                    _titleInfo("Acclim type", duty.acclim),                    const Gap(20),                    _titleInfo("Ref time", duty.reftime),                  ],                ),              const Gap(20),              if (duty.fdpStart != null)                Row(                  children: [                    const Text("Report:",                        style: TextStyle(fontWeight: FontWeight.w800)),                    const Gap(10),                    _titleInfo("Report time", duty.start?.Hm ?? "----"),                    const Gap(10),                    _titleInfo(                        "Delayed Report1", duty.reportdelay1?.Hm ?? "----"),                    const Gap(10),                    _titleInfo(                        "Delayed Report1", duty.reportdelay2?.Hm ?? "----"),                  ],                ),              const Gap(20),              if (duty.fdpStart != null &&                  (duty.lateFinish || duty.earlyStart || duty.nightDuty))                Row(                  children: [                    const Text("Disruptive:",                        style: TextStyle(fontWeight: FontWeight.w800)),                    const Gap(10),                    if (duty.earlyStart)                      const Text("early start duty",                          style: TextStyle(                              fontWeight: FontWeight.w800,                              fontSize: 12,                              color: Colors.cyan)),                    const Gap(10),                    if (duty.lateFinish)                      const Text("late finish duty",                          style: TextStyle(                              fontWeight: FontWeight.w800,                              fontSize: 12,                              color: Colors.cyan)),                    const Gap(10),                    if (duty.nightDuty)                      const Text("night duty",                          style: TextStyle(                              fontWeight: FontWeight.w800,                              fontSize: 12,                              color: Colors.cyan)),                    const Gap(10),                  ],                ),              const Gap(20),              Row(                children: [                  SingleChildScrollView(                    scrollDirection: Axis.horizontal,                    child: Row(                        mainAxisSize: MainAxisSize.max,                        mainAxisAlignment: MainAxisAlignment.start,                        children: [                          const Text("Detail:",                              style: TextStyle(fontWeight: FontWeight.w800)),                          ...(duty.dutiesDetailsIndex.map((e) {                            final detail = ftldata.dutiesDetails[e];                            final label = (detail.placeEnd != null)                                ? "${detail.placeStart}-${detail.placeEnd}"                                : "${detail.label}";                            return [                              const Gap(20),                              _titleInfo(label,                                  "${detail.start?.Hm}-${detail.end?.Hm}",                                  sizeinfo: 10)                            ];                          }).flattened),                        ]),                  ),                ],              ),              const Gap(20),              Row(                children: [                  const Text("Tot duties hrs:",                      style: TextStyle(fontWeight: FontWeight.w800)),                  const Gap(10),                  _titleInfo("7 days",                      "${duty.dutyTotal?.dutylast7?.tohhmm}|${Ftl.maxdutylast7.tohhmm}",                      sizeinfo: 10),                  const Gap(5),                  _titleInfo("14 days",                      "${duty.dutyTotal?.dutylast14?.tohhmm}|${Ftl.maxdutylast14.tohhmm}",                      sizeinfo: 10),                  const Gap(5),                  _titleInfo("28 days",                      "${duty.dutyTotal?.dutylast28?.tohhmm}|${Ftl.maxdutylast28.tohhmm}",                      sizeinfo: 10),                ],              ),              const Gap(20),              Row(                children: [                  const Text("Tot Flt hrs:",                      style: TextStyle(fontWeight: FontWeight.w800)),                  const Gap(10),                  _titleInfo("28 days",                      "${duty.dutyTotal?.fltlast28?.tohhmm}|${Ftl.maxfltlast28.tohhmm}",                      sizeinfo: 10),                  const Gap(5),                  _titleInfo("12 months",                      "${duty.dutyTotal?.fltlast12?.tohhmm}|${Ftl.maxfltlast12.tohhmm}",                      sizeinfo: 10),                  const Gap(5),                  _titleInfo("year",                      "${duty.dutyTotal?.fltyear?.tohhmm}|${Ftl.maxfltlastyear.tohhmm}",                      sizeinfo: 10),                ],              ),            ],          ),        ),      ),    );  }  _titleInfo(String? title, String? info,          {double sizetitle = 10,          double sizeinfo = 16,          Color color = Colors.blueGrey}) =>      Column(        children: [          Text(            title ?? "---",            style: TextStyle(color: Colors.grey, fontSize: sizetitle),          ),          Text(info ?? "---",              style: TextStyle(color: color, fontSize: sizeinfo)),        ],      );  Widget _getItem(BuildContext _, int i) {    final duty = dutiestodisplay.elementAt(i);    final lastduty = i == 0 ? null : dutiestodisplay.elementAt(i - 1);    final rest =        lastduty == null ? null : DTInterval(lastduty.end!, duty.start!);    Color legal;    if (duty.legals.isEmpty) {      legal = Colors.green[900]!;    } else if (duty.legals.every((e) => (e.condition ?? "") != "")) {      legal = Colors.amber;    } else {      legal = Colors.red;    }    return Column(      children: [        if (lastduty != null)          WShadowbox(              child: Column(children: [            Container(              decoration: const BoxDecoration(                color: Color.fromARGB(255, 8, 9, 9),                border: Border(top: BorderSide(width: 5)),                borderRadius: BorderRadius.all(Radius.circular(10.0)),              ),              padding: const EdgeInsets.all(5),              // width: double.infinity,              child: Row(                mainAxisAlignment: MainAxisAlignment.spaceBetween,                children: [                  Column(                    children: [                      _titleInfo("Rest",                          "${(rest?.duration.inDays ?? 0)} days, ${(rest?.duration.inHours ?? 0) % 24} hours, ${(rest?.duration.inMinutes ?? 0) % 60} minutes",                          sizeinfo: 10),                    ],                  ),                  Column(children: [                    if (_is48h(rest, lastduty.placeEnd ?? "UTC"))                      Container(                          color: Colors.teal[900],                          padding: const EdgeInsets.all(5),                          child: Text(                            "48h with 2 local days",                            style: TextStyle(                                color: Colors.yellow[600]!,                                fontSize: 11,                                fontWeight: FontWeight.w600),                          )),                    if (_is36h(rest, lastduty.placeEnd ?? "UTC"))                      Container(                          color: Colors.teal[800],                          padding: const EdgeInsets.all(5),                          child: Text(                            "36h with 2 local nights",                            style: TextStyle(                                color: Colors.yellow[500]!,                                fontSize: 11,                                fontWeight: FontWeight.w600),                          ))                  ]),                ],              ),            )          ])),        InkWell(          onTap: () => _showDutyInfo(i),          child: WShadowbox(            child: Column(              children: [                Container(                  decoration: BoxDecoration(                    color: Color.fromARGB(                        duty.start!.isAfter(Jiffy.now()) ? 255 : 70, 1, 45, 47),                    border: Border(top: BorderSide(width: 5, color: legal)),                    borderRadius:                        const BorderRadius.vertical(top: Radius.circular(10.0)),                  ),                  padding: const EdgeInsets.symmetric(horizontal: 20),                  // width: double.infinity,                  child: Row(                    mainAxisAlignment: MainAxisAlignment.spaceBetween,                    children: [                      Text(                        "${duty.start?.format(pattern: "dd MMM'yy")}",                        style: const TextStyle(color: Colors.white54),                      ),                      if (duty.start != null) WHour(jiffy: duty.start!),                    ],                  ),                ),                Container(                  width: double.infinity,                  padding: const EdgeInsets.only(left: 20, right: 10),                  decoration: const BoxDecoration(                    color: Colors.black,                  ),                  child: Column(                      crossAxisAlignment: CrossAxisAlignment.start,                      children: duty.legals                          .map((e) => Text(                                "+ ${e.condition ?? ""} ${e.legalCauseMsg}",                                style: const TextStyle(fontSize: 10),                              ))                          .toList()),                ),                //details                ...duty.dutiesDetailsIndex.map((index) {                  final FtlDutyDetails dutydetail =                      ftldata.dutiesDetails[index];                  return Row(                    children: [                      const Gap(40),                      SizedBox(                        width: 50,                        child: Row(children: [                          Text(                            dutydetail.typeString,                            style: TextStyle(                              backgroundColor: Colors.blueGrey[900],                              color: Colors.yellow,                              fontSize: 10,                              fontWeight: FontWeight.w600,                            ),                          )                        ]),                      ),                      SizedBox(                        width: 60,                        child: Row(                          children: [                            WHour(                              jiffy: dutydetail.start!,                              size: 12,                              color: Colors.white38,                            ),                            const Gap(5),                            WHour(                              jiffy: dutydetail.end!,                              size: 12,                              color: Colors.white38,                            ),                          ],                        ),                      ),                      SizedBox(                        // width: 80,                        child: Row(children: [                          const Gap(10),                          if (dutydetail.placeStart != null)                            WAirport(iata: dutydetail.placeStart!, size: 12),                          if (dutydetail.placeEnd != null) ...[                            const Icon(Icons.arrow_right_alt, size: 9),                            WAirport(iata: dutydetail.placeEnd!, size: 12)                          ] else if ((dutydetail.label ?? "") != "")                            Text(" (${dutydetail.label})",                                style: const TextStyle(                                    color: Colors.white54,                                    fontSize: 10,                                    letterSpacing: 2),                                overflow: TextOverflow.ellipsis),                        ]),                      ),                    ],                  );                }),                Container(                  decoration: BoxDecoration(                    color: Color.fromARGB(                        duty.start!.isAfter(Jiffy.now()) ? 255 : 70, 1, 45, 47),                    borderRadius:                        BorderRadius.vertical(bottom: Radius.circular(10.0)),                  ),                  padding: const EdgeInsets.symmetric(horizontal: 20),                  // width: double.infinity,                  child: Row(                    mainAxisAlignment: MainAxisAlignment.spaceBetween,                    children: [                      if (duty.end != null && duty.start != null)                        _titleInfo(                            "Duty length",                            duty.end!.dateTime                                .difference(duty.start!.dateTime)                                .tohhmm),                      const Row(children: []),                      Column(                        mainAxisSize: MainAxisSize.min,                        children: [                          Text(                            (duty.end?.yMMMd != duty.start?.yMMMd)                                ? "${duty.end?.format(pattern: "dd MMM")}"                                : "",                            style: const TextStyle(                                color: Colors.white54, fontSize: 12),                          ),                          if (duty.end != null) WHour(jiffy: duty.end!),                        ],                      ),                    ],                  ),                ),              ],            ),          ),        ),      ],    );  }}class FtlPageParams {  const FtlPageParams({    this.dateend,    this.datestart,    this.crewlinkuser,    this.crewlinkpass,  });  final String? dateend;  final String? datestart;  final String? crewlinkuser;  final String? crewlinkpass;}
 |