roster_page copy.dart.bak 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. // ignore_for_file: use_build_context_synchronously
  2. import 'dart:developer';
  3. import 'dart:io';
  4. import 'package:awesome_dialog/awesome_dialog.dart';
  5. import 'package:collection/collection.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter_riverpod/flutter_riverpod.dart';
  8. import 'package:gap/gap.dart';
  9. import 'package:go_router/go_router.dart';
  10. import 'package:hive_flutter/hive_flutter.dart';
  11. import 'package:jiffy/jiffy.dart';
  12. import 'package:scroll_to_index/scroll_to_index.dart';
  13. import 'package:tp5/core/basic_page.dart';
  14. import 'package:tp5/core/core.dart';
  15. import 'package:tp5/pdf/pdf_page.dart';
  16. import 'package:tp5/roster/api/crewlink_api.dart';
  17. import 'package:tp5/roster/models/duty.dart';
  18. import 'package:tp5/roster/view/crewlist_page.dart';
  19. import 'package:tp5/roster/widgets/w_day.dart';
  20. import 'package:tp5/roster/widgets/w_duty.dart';
  21. import 'package:tp5/roster/widgets/w_horizontal_month.dart';
  22. class RosterPage extends ConsumerStatefulWidget {
  23. const RosterPage({required this.params, super.key});
  24. final RosterPageParams params;
  25. @override
  26. ConsumerState<ConsumerStatefulWidget> createState() => _RosterPageState();
  27. }
  28. class _RosterPageState extends ConsumerState<RosterPage> {
  29. late String crewlinkUser;
  30. late String crewlinkPass;
  31. Map? roster;
  32. List<Duty> duties = [];
  33. List<Jiffy> get rostermonths => duties
  34. .fold(
  35. <Jiffy>[],
  36. (List<Jiffy> p, Duty e) => [
  37. ...p,
  38. if (!p
  39. .map((Jiffy j) => j.format(pattern: "MMMyy"))
  40. .contains(e.jdate.format(pattern: "MMMyy")))
  41. e.jdate
  42. ])
  43. .where((Jiffy e) => (duties
  44. .map((Duty d) => d.jdate.yMd)
  45. .contains(e.startOf(Unit.month).yMd) &&
  46. duties
  47. .map((Duty d) => d.jdate.yMd)
  48. .contains(e.endOf(Unit.month).yMd)))
  49. .toList();
  50. String get _rosterKey =>
  51. "roster_${crewlinkUser}_${widget.params.datestart}_${widget.params.dateend}";
  52. String get _rosterMinMaxKey => "minmax_$crewlinkUser";
  53. String get fileidroster => PathTo().crewlinkFile("$_rosterKey.pdf");
  54. String get fileidnotif => PathTo().crewlinkFile("notif_$crewlinkUser.pdf");
  55. final AutoScrollController _scrollCtrl = AutoScrollController();
  56. late Jiffy now;
  57. @override
  58. void initState() {
  59. // getusername & pass
  60. // crewlinkPass = Hive.box("profile").get("crewlink_pass");
  61. crewlinkUser = widget.params.crewlinkuser ??
  62. Hive.box("profile").get("crewlink_user") ??
  63. "";
  64. crewlinkPass = widget.params.crewlinkpass ??
  65. Hive.box("profile").get("crewlink_pass") ??
  66. "";
  67. Future.delayed(Duration.zero, () => _loadRoster());
  68. super.initState();
  69. }
  70. Future<Map?> _loadRosterOnline() async {
  71. if (!ref.read(crewlinkapiProvider).logged) {
  72. final login = await ref
  73. .read(crewlinkapiProvider)
  74. .login(username: crewlinkUser, password: crewlinkPass);
  75. if (login["data"]?["logged"] != true) {
  76. context.showError(login["error"] ?? "Unknown error");
  77. return null;
  78. }
  79. }
  80. final roster = await ref.read(crewlinkapiProvider).roster(
  81. start: widget.params.datestart ??
  82. Jiffy.now().toUtc().startOf(Unit.month).format(pattern: "ddMMMyy"),
  83. end: widget.params.dateend ??
  84. Jiffy.now().toUtc().endOf(Unit.month).format(pattern: "ddMMMyy"),
  85. fileid: fileidroster);
  86. if (roster?["data"]?["msg"] != null) {
  87. context.showError(roster?["data"]?["msg"] ?? "Unknown error");
  88. }
  89. if (roster["error"] == null && roster["data"] != null) {
  90. if (roster["msg"] != null) context.showAlert(roster["msg"]);
  91. Hive.box("crewlink").put(_rosterKey, roster);
  92. return roster;
  93. }
  94. return null;
  95. }
  96. Future<Map?> _loadRosterOffline() async {
  97. return Hive.box("crewlink").get(_rosterKey);
  98. }
  99. _loadRoster() async {
  100. try {
  101. ref.read(isLoadingProvider.notifier).state = true;
  102. final rosterOffline = await _loadRosterOffline();
  103. if (rosterOffline != null && mounted) {
  104. _convertRoster(rosterOffline);
  105. setState(() {});
  106. _scrollToDate();
  107. }
  108. final rosterOnline = await _loadRosterOnline();
  109. if (rosterOnline != null && mounted) {
  110. _convertRoster(rosterOnline);
  111. setState(() {});
  112. _scrollToDate();
  113. }
  114. } finally {
  115. ref.read(isLoadingProvider.notifier).state = false;
  116. }
  117. }
  118. _scrollToDate({Jiffy? date}) {
  119. final jdate = date ?? Jiffy.now().toUtc();
  120. bool found = false;
  121. int id = 0;
  122. for (Duty duty in duties) {
  123. if (duty.jdate.yMd == jdate.yMd) {
  124. found = true;
  125. break;
  126. }
  127. id++;
  128. }
  129. Future.delayed(const Duration(milliseconds: 100)).then((value) {
  130. if (found && mounted && _scrollCtrl.hasClients) {
  131. _scrollCtrl.scrollToIndex(id,
  132. duration: const Duration(milliseconds: 1300),
  133. preferPosition: AutoScrollPosition.begin);
  134. }
  135. });
  136. }
  137. _convertRoster(Map? input) {
  138. roster = input;
  139. int i = 0;
  140. duties.clear();
  141. final decoded = (input?["data"]?["decoded"]?["roster"] ?? {});
  142. for (var date in decoded.keys) {
  143. var dutylist = (decoded[date] as List);
  144. for (var duty in dutylist) {
  145. i++;
  146. duties.add(
  147. Duty(date: date, type: duty["type"], data: duty["data"], order: i));
  148. // print(
  149. // "${(duties.last).jdate.yMd} ${(duties.last).type} ${(duties.last).start?.Hm} ${(duties.last).end?.Hm}");
  150. }
  151. }
  152. }
  153. final bottomnavstyle = ElevatedButton.styleFrom(
  154. shape: RoundedRectangleBorder(
  155. borderRadius: BorderRadius.circular(5.0),
  156. ),
  157. backgroundColor:
  158. const Color.fromARGB(255, 0, 36, 53) //elevated btton background color
  159. );
  160. @override
  161. Widget build(BuildContext context) {
  162. now = ref.watch(clockProvider);
  163. return BasicPage(
  164. actions: [
  165. IconButton(
  166. onPressed: () => context.push("/crewlink/settings"),
  167. icon: const Icon(Icons.settings)),
  168. const Gap(10)
  169. ],
  170. bottomNavigationBar: Container(
  171. padding: const EdgeInsets.all(8),
  172. // color: Colors.black,
  173. decoration: BoxDecoration(
  174. gradient: LinearGradient(
  175. begin: Alignment.topCenter,
  176. end: Alignment.bottomCenter,
  177. colors: [
  178. Colors.grey[700]!,
  179. Colors.black,
  180. ],
  181. )),
  182. child: Row(
  183. mainAxisAlignment: MainAxisAlignment.spaceAround,
  184. children: [
  185. ElevatedButton.icon(
  186. onPressed: () {
  187. _showMonths(context);
  188. },
  189. icon: const Icon(
  190. Icons.calendar_month), //icon data for elevated button
  191. label: const Text("Change\nMonth"), //label text
  192. style: bottomnavstyle,
  193. ),
  194. const Gap(10),
  195. ElevatedButton.icon(
  196. onPressed: () {
  197. _showPdf(context);
  198. },
  199. icon: Image.asset(
  200. 'assets/pdficon.png',
  201. width: 28,
  202. ), //icon data for elevated button
  203. label: const Text("Roster\n PDF"), //label text
  204. style: bottomnavstyle,
  205. ),
  206. ],
  207. ),
  208. ),
  209. title: "CrewLink / Roster",
  210. body: ListView.builder(
  211. itemCount: duties.length,
  212. itemBuilder: (_, index) => AutoScrollTag(
  213. key: ValueKey(index),
  214. controller: _scrollCtrl,
  215. index: index,
  216. child: _getItem(_, index)),
  217. shrinkWrap: false,
  218. controller: _scrollCtrl,
  219. ),
  220. );
  221. }
  222. Widget _getItem(BuildContext ctx, int i) {
  223. final duty = duties.elementAt(i);
  224. Duty? firstitem =
  225. duties.firstWhereOrNull((Duty el) => el.date == duty.date);
  226. bool isfirstitem = false;
  227. if (firstitem != null &&
  228. firstitem.type == duty.type &&
  229. firstitem.data == duty.data) isfirstitem = true;
  230. final String lastday =
  231. duty.jdate.subtract(days: 1).format(pattern: "yyyy-MM-dd");
  232. final Duty? dutyendingtoday = (!isfirstitem)
  233. ? null
  234. : duties.firstWhereOrNull((Duty el) =>
  235. (el.date == lastday) &&
  236. (el.end != null) &&
  237. (el.end!.format(pattern: "yyyy-MM-dd") == duty.date));
  238. return Column(children: [
  239. if (isfirstitem) ...[
  240. const Gap(10),
  241. const Divider(),
  242. WDay(
  243. date: duty.jdate,
  244. highlight: now.yMd == duty.jdate.yMd,
  245. onTap: () => context.push("/crewlink/crewlist",
  246. extra: CrewlistPageParams(
  247. datestart: duty.jdate.format(pattern: "ddMMMyy"))),
  248. ),
  249. if (dutyendingtoday != null)
  250. WDuty(duty: dutyendingtoday, date: duty.date),
  251. ],
  252. InkWell(
  253. highlightColor: Colors.yellow[900],
  254. onTap: () {
  255. log("Tap: ${duty.type} ${duty.data}", name: "RosterPage");
  256. switch (duty.type) {
  257. case "changed":
  258. AwesomeDialog(
  259. context: context,
  260. dialogType: DialogType.question,
  261. animType: AnimType.topSlide,
  262. title: 'Notification',
  263. desc: 'Load and show pending notification?',
  264. btnOkOnPress: () async {
  265. ref.read(isLoadingProvider.notifier).state = true;
  266. final res = await ref
  267. .read(crewlinkapiProvider)
  268. .notif(download: true, fileid: fileidnotif);
  269. ref.read(isLoadingProvider.notifier).state = false;
  270. if (res is Map && res["data"]?["id"] != null) {
  271. _showNotif();
  272. } else {
  273. AwesomeDialog(
  274. context: context,
  275. dialogType: DialogType.warning,
  276. animType: AnimType.topSlide,
  277. title: 'Notification',
  278. desc: res?["error"] ??
  279. res?["data"]?["msg"] ??
  280. 'No pending notification found !!!',
  281. btnOkOnPress: () async {})
  282. .show();
  283. }
  284. },
  285. btnCancelOnPress: () {},
  286. btnOkText: "Load notification",
  287. showCloseIcon: true)
  288. .show();
  289. break;
  290. default:
  291. }
  292. },
  293. child: WDuty(duty: duty),
  294. )
  295. ]);
  296. }
  297. void _showNotif() {
  298. {
  299. context
  300. .push("/pdf",
  301. extra: PdfPageParams(
  302. file: fileidnotif,
  303. title: "Roster change",
  304. bottom: ElevatedButton(
  305. style: ElevatedButton.styleFrom(
  306. backgroundColor: Colors.red[900],
  307. ),
  308. onPressed: () async {
  309. ref.read(isLoadingProvider.notifier).state = true;
  310. final ack =
  311. await ref.read(crewlinkapiProvider).confirmNotif();
  312. // final ack = {
  313. // "error": null,
  314. // "data": {
  315. // "msg": null,
  316. // "error": null,
  317. // "notif": true
  318. // }
  319. // };
  320. ref.read(isLoadingProvider.notifier).state = false;
  321. final ackok = (ack is Map &&
  322. ack["error"] != null &&
  323. ack["data"]?["error"] != null);
  324. AwesomeDialog(
  325. context: context,
  326. dialogType:
  327. ackok ? DialogType.success : DialogType.warning,
  328. animType: AnimType.topSlide,
  329. title: 'Notification',
  330. desc:
  331. // ack["data"]?["msg"] ??
  332. 'Notification for change was acknowledged.',
  333. btnOkOnPress: () {
  334. context.pop({"notif": true});
  335. }).show();
  336. },
  337. child: const Padding(
  338. padding: EdgeInsets.all(8.0),
  339. child: Text(
  340. 'Acknowledge & Confirm changes',
  341. style: TextStyle(fontSize: 20, color: Colors.yellow),
  342. ),
  343. ),
  344. )))
  345. .then((res) {
  346. if (res is Map && res["notif"]) _loadRoster();
  347. });
  348. }
  349. }
  350. void _showPdf(xcontext) {
  351. if (File(fileidroster).existsSync()) {
  352. context.push("/pdf",
  353. extra: PdfPageParams(
  354. file: fileidroster,
  355. title:
  356. "Roster: ${Jiffy.parse(widget.params.datestart ?? "", pattern: "ddMMMyy", isUtc: true).format(pattern: "MMMM yyyy")}"));
  357. } else {
  358. context.showError(
  359. "Can't find the roster for this month.\n Try to connect to internet and retrieve it from CrewLink?");
  360. }
  361. }
  362. void _showMonths(xcontext) async {
  363. ref.read(isLoadingProvider.notifier).state = true;
  364. final resoff = Hive.box("crewlink").get(_rosterMinMaxKey);
  365. final reson = await ref.read(crewlinkapiProvider).rosterMinMax();
  366. ref.read(isLoadingProvider.notifier).state = false;
  367. dynamic res;
  368. if (reson["error"] == null && reson?["data"] != null) {
  369. Hive.box("crewlink").put(_rosterMinMaxKey, reson["data"]);
  370. res = reson["data"];
  371. } else if (resoff == null) {
  372. context.showError(reson["error"] ?? "Unknown error");
  373. return;
  374. }
  375. res = res ?? resoff;
  376. showModalBottomSheet(
  377. useSafeArea: true,
  378. backgroundColor: Colors.blueGrey[900],
  379. context: context,
  380. builder: (BuildContext bc) {
  381. return HorizontalMonth(
  382. start: Jiffy.parse(res["mindate"],
  383. pattern: "yyyy-MM-dd", isUtc: true)
  384. // .add(days: 1),
  385. .add(months: 1)
  386. .startOf(Unit.month),
  387. end: Jiffy.parse(res["maxdate"],
  388. pattern: "yyyy-MM-dd", isUtc: true)
  389. .subtract(days: 1),
  390. selectedmonth: rostermonths,
  391. onmonthclick: (date) {
  392. bc.push("/crewlink/roster",
  393. extra: RosterPageParams(
  394. dateend:
  395. date.endOf(Unit.month).format(pattern: "ddMMMyy"),
  396. datestart:
  397. date.startOf(Unit.month).format(pattern: "ddMMMyy"),
  398. crewlinkuser: crewlinkUser,
  399. crewlinkpass: crewlinkPass));
  400. });
  401. });
  402. }
  403. }
  404. class RosterPageParams {
  405. const RosterPageParams({
  406. this.dateend,
  407. this.datestart,
  408. this.crewlinkuser,
  409. this.crewlinkpass,
  410. });
  411. final String? dateend;
  412. final String? datestart;
  413. final String? crewlinkuser;
  414. final String? crewlinkpass;
  415. }