From 6d84f7bdc351e38d630481634a4128e45a3ba44e Mon Sep 17 00:00:00 2001 From: Mads Heimdal Thy Date: Tue, 31 Mar 2026 23:03:08 +0200 Subject: [PATCH] chore: Update translations, improve error handling, and bump version to 1.2.12 --- assets/translations/da.json | 51 ++- assets/translations/en.json | 51 ++- ios/Runner.xcodeproj/project.pbxproj | 12 +- lib/controllers/app.controller.dart | 32 +- lib/core/constants/app_constants.dart | 68 ---- lib/main.dart | 2 +- lib/screens/info.screen.dart | 439 +++++++++++++------------- lib/screens/login.screen.dart | 2 + lib/screens/more.screen.dart | 26 -- lib/screens/notifications.screen.dart | 13 +- lib/screens/profile.screen.dart | 2 + lib/services/config.service.dart | 61 +++- pubspec.yaml | 2 +- 13 files changed, 399 insertions(+), 362 deletions(-) diff --git a/assets/translations/da.json b/assets/translations/da.json index 83e89bf..3aa0003 100644 --- a/assets/translations/da.json +++ b/assets/translations/da.json @@ -22,6 +22,10 @@ "show": "Vis kort" } }, + "areas": { + "commonArea": "Fællesområdet", + "niche": "Nichen" + }, "boardgames": { "fastavalGame": "Er et Fastaval spil", "gameAvailable": { @@ -135,28 +139,23 @@ "store1": { "day1": "Onsdag", "day2": "Tor-Lør", - "day3": "Søndag", - "title": "Epic Panda @ B45" + "day3": "Søndag" }, "store2": { "day1": "Fredag", "day2": "Lørdag", - "day3": "Søndag", - "title": "Allroleplaying @ A07" + "day3": "Søndag" }, "store3": { "day1": "Onsdag", "day2": "Tor-Lør", - "day3": "Søndag", - "title": "Grandmas Gaming Goodies @ A07" + "day3": "Søndag" }, "store4": { - "day1": "Tor-Lør", - "title": "Bunkern Esbjerg @ Fællesområdet" + "day1": "Tor-Lør" }, "store5": { - "day1": "Lørdag", - "title": "Artist Alley popup shops @ Fællesområdet" + "day1": "Lørdag" }, "title": "Butikker" }, @@ -272,6 +271,36 @@ "short": "Ons" } }, + "days": { + "wed": { + "long": "Onsdag", + "short": "Ons" + }, + "thu": { + "long": "Torsdag", + "short": "Tor" + }, + "fri": { + "long": "Fredag", + "short": "Fre" + }, + "sat": { + "long": "Lørdag", + "short": "Lør" + }, + "sun": { + "long": "Søndag", + "short": "Søn" + }, + "mon": { + "long": "Mandag", + "short": "Man" + }, + "tue": { + "long": "Tirsdag", + "short": "Tir" + } + }, "screenTitle": { "boardgames": "Brætspil", "favorites": "Favoritter", @@ -286,4 +315,4 @@ "settings": { "title": "Indstillinger" } -} +} \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index dbba327..f74d58c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -22,6 +22,10 @@ "show": "Show map" } }, + "areas": { + "commonArea": "Common Area", + "niche": "The Niche" + }, "boardgames": { "fastavalGame": "is a Fastaval game", "gameAvailable": { @@ -135,28 +139,23 @@ "store1": { "day1": "Wednesday", "day2": "Thu-Sat", - "day3": "Sunday", - "title": "Epic Panda @ B45" + "day3": "Sunday" }, "store2": { "day1": "Friday", "day2": "Saturday", - "day3": "Sunday", - "title": "Allroleplaying @ A07" + "day3": "Sunday" }, "store3": { "day1": "Wednesday", "day2": "Thu-Sat", - "day3": "Sunday", - "title": "Grandmas Gaming Goodies @ A07" + "day3": "Sunday" }, "store4": { - "day1": "Thu-Sat", - "title": "Bunkern Esbjerg @ Common Area" + "day1": "Thu-Sat" }, "store5": { - "day1": "Saturday", - "title": "Artist Alley popup shops @ Common Area" + "day1": "Saturday" }, "title": "Shops" }, @@ -272,6 +271,36 @@ "short": "Wed" } }, + "days": { + "wed": { + "long": "Wednesday", + "short": "Wed" + }, + "thu": { + "long": "Thursday", + "short": "Thu" + }, + "fri": { + "long": "Friday", + "short": "Fri" + }, + "sat": { + "long": "Saturday", + "short": "Sat" + }, + "sun": { + "long": "Sunday", + "short": "Sun" + }, + "mon": { + "long": "Monday", + "short": "Mon" + }, + "tue": { + "long": "Tuesday", + "short": "Tue" + } + }, "screenTitle": { "boardgames": "Board Games", "favorites": "Favorites", @@ -286,4 +315,4 @@ "settings": { "title": "Settings" } -} +} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2709936..a3e1227 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -400,7 +400,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.11; + MARKETING_VERSION = 1.2.12; PRODUCT_BUNDLE_IDENTIFIER = dk.fastaval.fastavappen; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -541,7 +541,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -553,7 +553,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.11; + MARKETING_VERSION = 1.2.12; PRODUCT_BUNDLE_IDENTIFIER = dk.fastaval.fastavappen; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -576,7 +576,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -588,7 +588,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.11; + MARKETING_VERSION = 1.2.12; PRODUCT_BUNDLE_IDENTIFIER = dk.fastaval.fastavappen; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/lib/controllers/app.controller.dart b/lib/controllers/app.controller.dart index 1c23c01..f3839d5 100644 --- a/lib/controllers/app.controller.dart +++ b/lib/controllers/app.controller.dart @@ -51,12 +51,20 @@ class AppController extends GetxController { if (user.id == 0) return; fetchingUser(true); - User newUser = await fetchUser(user.id.toString(), user.password); - newUser.password = user.password; + try { + User newUser = await fetchUser(user.id.toString(), user.password); + newUser.password = user.password; - await UserService().setUser(newUser); - updateUser(newUser); - fetchingUser(false); + await UserService().setUser(newUser); + updateUser(newUser); + } catch (e) { + // If fetching the user fails (e.g. invalid stored credentials), logout + // and prevent the exception from bubbling up during app init. + logout(); + return; + } finally { + fetchingUser(false); + } } Future login(String id, String password) async { @@ -75,7 +83,12 @@ class AppController extends GetxController { } void logout() { - var boardCtrl = Get.find(); + // Only attempt to access BoardGameController if it's been registered + BoardGameController? boardCtrl; + if (Get.isRegistered()) { + boardCtrl = Get.find(); + } + UserService().removeUser(); user.id = 0; user.password = ''; @@ -83,7 +96,10 @@ class AppController extends GetxController { updateNavIndex(0); Fluttertoast.showToast(msg: tr('logout.message')); - boardCtrl.availableBoardgames.clear(); - boardCtrl.chosenBoardgames.clear(); + // If the controller exists, clear its selections. Otherwise skip. + if (boardCtrl != null) { + boardCtrl.availableBoardgames.clear(); + boardCtrl.chosenBoardgames.clear(); + } } } diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index c2a3d0e..48d7589 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -29,72 +29,4 @@ class AppConstants { "value": "+4598512300", "name": "98 51 23 00", }; - - static const Map heroForcePhoneNumber = { - "value": "+4593903568", - "name": "93 90 35 68", - }; - - static const Map dutyGeneralPhoneNumber = { - "value": "+4593903569", - "name": "93 90 35 69", - }; - - static const Map safetyHostPhoneNumber = { - "value": "+4593903567", - "name": "93 90 35 67", - }; - - // Service Opening Hours - static const Map> serviceOpeningHours = { - "bar": { - "day1": "17:00 - 02:00", - }, - "boardGames": { - "day1": "09:00 - 02:00", - "day2": "09:00 - 15:00", - }, - "coffeeCafe": { - "day1": "11:00 - 01:00", - "day2": "09:00 - 01:00", - "day3": "09:00 - 15:00", - }, - "information": { - "day1": "15:00 - 20:30", - "day2": "09:30 - 20:30", - "day3": "09:30 - 17:00", - }, - "kiosk": { - "day1": "08:00 - 20:00", - "day2": "08:00 - 16:00", - }, - "ottosLivingRoom": { - "day1": "15:00 - 01:00", - }, - }; - - // Store Opening Hours - static const Map> storeOpeningHours = { - "store1": { - "day1": "15:00 - 23:00", - "day2": "10:00 - 23:00", - "day3": "10:00 - 15:00", - }, - "store2": { - "day1": "11:00 - 18:00", - "day2": "13:00 - 17:00", - "day3": "14:00 - 17:00", - }, - "store3": { - "day1": "15:00 - 20:00", - "day2": "09:30 - 20:00", - "day3": "09:30 - 18:00", - }, - "store4": { - "day1": "10:00 - 22:00", - }, - "store5": { - "day1": "12:00 - 18:00", - }, - }; } diff --git a/lib/main.dart b/lib/main.dart index 3466cfa..288a079 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,8 +31,8 @@ void main() async { await Get.put(SettingsController()).init(); await Get.put(AppController()).init(); - await Get.put(ProgramController()).init(); await Get.put(BoardGameController()).init(); + await Get.put(ProgramController()).init(); await Get.put(NotificationController()).init(); runApp( diff --git a/lib/screens/info.screen.dart b/lib/screens/info.screen.dart index 1653470..8b362f0 100644 --- a/lib/screens/info.screen.dart +++ b/lib/screens/info.screen.dart @@ -1,9 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fastaval_app/controllers/notification.controller.dart'; -import 'package:fastaval_app/core/constants/app_constants.dart'; import 'package:fastaval_app/core/theme/app_colors.dart'; import 'package:fastaval_app/core/theme/app_decorations.dart'; import 'package:fastaval_app/core/theme/app_text_styles.dart'; +import 'package:fastaval_app/services/config.service.dart'; import 'package:fastaval_app/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -43,7 +43,7 @@ class InfoScreen extends StatelessWidget { SizedBox(height: 12), _buildSafeFastavalCard(), _buildOpenHoursCard(), - _buildWifiCard(), + //_buildWifiCard(), _buildLostFoundCard(), _buildTransportCard(), _buildFastaWearCard(), @@ -96,230 +96,233 @@ Widget _buildOpenHoursCard() => textAndIconCard( padding: EdgeInsets.fromLTRB(16, 8, 24, 16), child: Column( children: [ - textRowHeader(tr('info.openHours.bar.title')), - twoTextRow( - tr('info.openHours.bar.day1'), - AppConstants.serviceOpeningHours["bar"]!["day1"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.openHours.boardGames.title')), - twoTextRow( - tr('info.openHours.boardGames.day1'), - AppConstants.serviceOpeningHours["boardGames"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.boardGames.day2'), - AppConstants.serviceOpeningHours["boardGames"]!["day2"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.openHours.information.title')), - twoTextRow( - tr('info.openHours.information.day1'), - AppConstants.serviceOpeningHours["information"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.information.day2'), - AppConstants.serviceOpeningHours["information"]!["day2"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.information.day3'), - AppConstants.serviceOpeningHours["information"]!["day3"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.openHours.kiosk.title')), - twoTextRow( - tr('info.openHours.kiosk.day1'), - AppConstants.serviceOpeningHours["kiosk"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.kiosk.day2'), - AppConstants.serviceOpeningHours["kiosk"]!["day2"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.openHours.ottosLivingRoom.title')), - twoTextRow( - tr('info.openHours.ottosLivingRoom.day1'), - AppConstants.serviceOpeningHours["ottosLivingRoom"]!["day1"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.openHours.coffeeCafe.title')), - twoTextRow( - tr('info.openHours.coffeeCafe.day1'), - AppConstants.serviceOpeningHours["coffeeCafe"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.coffeeCafe.day2'), - AppConstants.serviceOpeningHours["coffeeCafe"]!["day2"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.openHours.coffeeCafe.day3'), - AppConstants.serviceOpeningHours["coffeeCafe"]!["day3"]!, - sidePadding: true, - ), - SizedBox(height: 20), - SizedBox(height: 1, child: Divider(color: AppColors.textPrimary)), - SizedBox(height: 20), - textRowHeader(tr('info.stores.store1.title')), - twoTextRow( - tr('info.stores.store1.day1'), - AppConstants.storeOpeningHours["store1"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store1.day2'), - AppConstants.storeOpeningHours["store1"]!["day2"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store1.day3'), - AppConstants.storeOpeningHours["store1"]!["day3"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.stores.store2.title')), - twoTextRow( - tr('info.stores.store2.day1'), - AppConstants.storeOpeningHours["store2"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store2.day2'), - AppConstants.storeOpeningHours["store2"]!["day2"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store2.day3'), - AppConstants.storeOpeningHours["store2"]!["day3"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.stores.store3.title')), - twoTextRow( - tr('info.stores.store3.day1'), - AppConstants.storeOpeningHours["store3"]!["day1"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store3.day2'), - AppConstants.storeOpeningHours["store3"]!["day2"]!, - sidePadding: true, - ), - twoTextRow( - tr('info.stores.store3.day3'), - AppConstants.storeOpeningHours["store3"]!["day3"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.stores.store4.title')), - twoTextRow( - tr('info.stores.store4.day1'), - AppConstants.storeOpeningHours["store4"]!["day1"]!, - sidePadding: true, - ), - SizedBox(height: 10), - textRowHeader(tr('info.stores.store5.title')), - twoTextRow( - tr('info.stores.store5.day1'), - AppConstants.storeOpeningHours["store5"]!["day1"]!, - sidePadding: true, - ), - SizedBox(height: 10), - ], - ), - ), - ); + Builder(builder: (_) { + final svc = ConfigService.instance.getServiceOpeningHours(); + final stores = ConfigService.instance.getStoreOpeningHours(); + final children = []; -Widget _buildSafeFastavalCard() => textAndIconCard( - tr('info.safe.title'), - Icons.phone, - Padding( - padding: EdgeInsets.fromLTRB(16, 8, 24, 16), - child: Column( - children: [ - twoTextRowWithTapAction( - tr('info.safe.dutyGeneral'), - AppConstants.dutyGeneralPhoneNumber["name"]!, - Uri( - scheme: 'tel', - path: AppConstants.dutyGeneralPhoneNumber["value"]), - ), - SizedBox(height: 10), - twoTextRowWithTapAction( - tr('info.safe.heroForce'), - AppConstants.heroForcePhoneNumber["name"]!, - Uri( - scheme: 'tel', - path: AppConstants.heroForcePhoneNumber["value"]), - ), - SizedBox(height: 10), - twoTextRowWithTapAction( - tr('info.safe.safetyHost'), - AppConstants.safetyHostPhoneNumber["name"]!, - Uri( - scheme: 'tel', - path: AppConstants.safetyHostPhoneNumber["value"]), - ), + // Render service opening hours only for entries present in remote config + svc.entries.toList() + ..sort((a, b) => a.key.compareTo(b.key)) + ..forEach((entry) { + final serviceKey = entry.key; + final days = Map.from(entry.value); + if (days.isEmpty) return; + + children.add( + textRowHeader(tr('info.openHours.$serviceKey.title'))); + + final dayEntries = days.entries.toList() + ..sort((a, b) => a.key.compareTo(b.key)); + for (var d in dayEntries) { + children.add(twoTextRow( + tr('info.openHours.$serviceKey.${d.key}'), + d.value, + sidePadding: true, + )); + } + + children.add(SizedBox(height: 20)); + }); + + // Divider between services and stores when both present + if (svc.isNotEmpty && stores.isNotEmpty) { + children.add(SizedBox( + height: 1, child: Divider(color: AppColors.textPrimary))); + children.add(SizedBox(height: 20)); + } + + // Render store opening hours only for entries present in remote config + stores.entries.toList() + ..sort((a, b) => a.key.compareTo(b.key)) + ..forEach((entry) { + final storeKey = entry.key; + final storeMap = Map.from(entry.value); + if (storeMap.isEmpty) return; + + // Title and location come from Remote Config now. + final rawTitle = storeMap['title']?.toString() ?? storeKey; + final rawLocation = storeMap['location']?.toString() ?? ''; + + final displayTitle = + rawTitle.contains('.') ? tr(rawTitle) : rawTitle; + final displayLocation = + rawLocation.contains('.') ? tr(rawLocation) : rawLocation; + final header = displayLocation.isNotEmpty + ? '$displayTitle @ $displayLocation' + : displayTitle; + + // Build day rows either from the new `days` array format + // or fall back to legacy `day1`, `day2`, ... keys. + final dayRows = []; + + final daysArray = (storeMap['days'] is List) + ? List.from(storeMap['days'] as List) + : []; + + if (daysArray.isNotEmpty) { + // helper: translate parts separated by '-' (e.g. "days.thu.short-days.sat.short") + String translateComposite(String raw) { + if (raw.isEmpty) return ''; + return raw + .split('-') + .map((part) => part.contains('.') ? tr(part) : part) + .join('-'); + } + + for (var item in daysArray) { + if (item is Map) { + final rawLabel = item['title']?.toString() ?? ''; + final label = translateComposite(rawLabel); + final time = item['time']?.toString() ?? ''; + if (label.isEmpty && time.isEmpty) continue; + dayRows.add(twoTextRow(label, time, sidePadding: true)); + } + } + } else { + // legacy fallback: day1/day2/... keys + final dayEntries = storeMap.entries + .where((e) => e.key.startsWith('day')) + .toList() + ..sort((a, b) => a.key.compareTo(b.key)); + + for (var d in dayEntries) { + dayRows.add(twoTextRow( + tr('info.stores.$storeKey.${d.key}'), + d.value.toString(), + sidePadding: true, + )); + } + } + + // If there are no day rows, skip rendering this store entirely. + if (dayRows.isEmpty) return; + + children.add(textRowHeader(header)); + children.addAll(dayRows); + children.add(SizedBox(height: 10)); + }); + + return Column(children: children); + }), SizedBox(height: 10), - twoTextRowWithTapAction( - tr('info.safe.safetyMail'), - AppConstants.safeFastavalEmail, - Uri(scheme: 'mailto', path: AppConstants.safeFastavalEmail), - ), ], ), ), ); -Widget _buildTransportCard() => textAndIconCard( - tr('info.transportAndParking.title'), - Icons.local_parking, - Padding( +Widget _buildSafeFastavalCard() { + final boxes = ConfigService.instance.getInfoScreenBoxes(); + final safeBox = boxes.firstWhere( + (b) => + b is Map && (b['title'] == 'info.safe.title' || b['icon'] == 'phone'), + orElse: () => null, + ); + + if (safeBox == null) { + return SizedBox.shrink(); + } + + final items = (safeBox['items'] as List?) ?? []; + final children = []; + for (var i = 0; i < items.length; i++) { + final item = items[i] as Map; + final textKey = item['text']?.toString() ?? ''; + final display = + item['valueDisplay']?.toString() ?? item['value']?.toString() ?? ''; + final value = item['value']?.toString() ?? ''; + final type = item['type']?.toString() ?? ''; + + if (type == 'phone') { + children.add(twoTextRowWithTapAction( + tr(textKey), display, Uri(scheme: 'tel', path: value))); + } else if (type == 'email') { + children.add(twoTextRowWithTapAction( + tr(textKey), display, Uri(scheme: 'mailto', path: value))); + } else { + children.add(twoTextRow(tr(textKey), display, sidePadding: true)); + } + + if (i < items.length - 1) children.add(SizedBox(height: 10)); + } + + return textAndIconCard( + tr('info.safe.title'), + Icons.phone, + Padding( + padding: EdgeInsets.fromLTRB(16, 8, 24, 16), + child: Column(children: children), + ), + ); +} + +Widget _buildTransportCard() { + final boxes = ConfigService.instance.getInfoScreenBoxes(); + final transportBox = boxes.firstWhere( + (b) => + b is Map && + (b['title'] == 'info.transportAndParking.title' || + b['icon'] == 'local_parking'), + orElse: () => null, + ); + + if (transportBox == null) return SizedBox.shrink(); + + final items = (transportBox['items'] as List?) ?? []; + final children = []; + for (var i = 0; i < items.length; i++) { + final item = items[i] as Map; + final textKey = item['text']?.toString() ?? ''; + final display = + item['valueDisplay']?.toString() ?? item['value']?.toString() ?? ''; + final value = item['value']?.toString() ?? ''; + final type = item['type']?.toString() ?? ''; + + if (type == 'phone') { + children.add(twoTextRowWithTapAction( + tr(textKey), display, Uri(scheme: 'tel', path: value))); + } else { + children.add(twoTextRow(tr(textKey), display, sidePadding: true)); + } + + if (i < items.length - 1) children.add(SizedBox(height: 20)); + } + + return textAndIconCard( + tr('info.transportAndParking.title'), + Icons.local_parking, + Padding( padding: EdgeInsets.fromLTRB(16, 8, 24, 16), - child: Column( - children: [ - Text(tr('info.transportAndParking.text'), - style: AppTextStyles.normal), - SizedBox(height: 20), - twoTextRowWithTapAction( - tr('info.transportAndParking.taxi1'), - AppConstants.taxi1PhoneNumber['name']!, - Uri(scheme: 'tel', path: AppConstants.taxi1PhoneNumber['value']), - ), - SizedBox(height: 20), - twoTextRowWithTapAction( - tr('info.transportAndParking.taxi2'), - AppConstants.taxi2PhoneNumber['name']!, - Uri(scheme: 'tel', path: AppConstants.taxi2PhoneNumber['value']), - ), - ], - ), - ), - ); + child: Column(children: children)), + ); +} -Widget _buildWifiCard() => textAndIconCard( - tr('info.wifi.title'), - Icons.wifi, - Padding( +Widget _buildWifiCard() { + final boxes = ConfigService.instance.getInfoScreenBoxes(); + final wifiBox = boxes.firstWhere( + (b) => b is Map && (b['title'] == 'info.wifi.title' || b['icon'] == 'wifi'), + orElse: () => null, + ); + + if (wifiBox == null) return SizedBox.shrink(); + + final items = (wifiBox['items'] as List?) ?? []; + final children = []; + for (var i = 0; i < items.length; i++) { + final item = items[i] as Map; + final textKey = item['text']?.toString() ?? ''; + final display = + item['valueDisplay']?.toString() ?? item['value']?.toString() ?? ''; + children.add(twoTextRow(tr(textKey), display, sidePadding: true)); + if (i < items.length - 1) children.add(SizedBox(height: 10)); + } + + return textAndIconCard( + tr('info.wifi.title'), + Icons.wifi, + Padding( padding: EdgeInsets.fromLTRB(16, 8, 24, 16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - twoTextRow( - tr('info.wifi.networkName'), AppConstants.wifiNetworkName), - SizedBox(height: 10), - Text(tr('info.wifi.explainer'), style: AppTextStyles.normal), - ], - ), - ), - ); + crossAxisAlignment: CrossAxisAlignment.start, children: children)), + ); +} diff --git a/lib/screens/login.screen.dart b/lib/screens/login.screen.dart index 856f68a..1857542 100644 --- a/lib/screens/login.screen.dart +++ b/lib/screens/login.screen.dart @@ -55,6 +55,8 @@ class LoginScreen extends StatelessWidget { Widget _buildLoginButton() => SizedBox( width: double.infinity, child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, elevation: 2), onPressed: () async => { HapticFeedback.heavyImpact(), await appCtrl.login(userIdInput.text, passwordInput.text), diff --git a/lib/screens/more.screen.dart b/lib/screens/more.screen.dart index 24dd1c3..9ce9d25 100644 --- a/lib/screens/more.screen.dart +++ b/lib/screens/more.screen.dart @@ -25,9 +25,6 @@ class MoreScreen extends StatelessWidget { @override Widget build(context) { - var newsletter = Uri.parse( - 'https://fastaval.us21.list-manage.com/subscribe?u=0929f085b293ddfa9eb2bc60a&id=13f4440b66', - ); var x = Uri.parse('https://twitter.com/fastaval'); var facebook = Uri.parse('https://www.facebook.com/Fastaval'); var instagram = Uri.parse('https://www.instagram.com/fastaval/'); @@ -186,31 +183,8 @@ class MoreScreen extends StatelessWidget { color: AppColors.textPrimary, ), ), - SizedBox(width: 24), - IconButton( - onPressed: () => canLaunchUrl(newsletter).then( - (allowed) => { - if (allowed) - launchUrl( - newsletter, - mode: LaunchMode.externalApplication, - ), - }, - ), - icon: FaIcon( - FontAwesomeIcons.envelopeOpenText, - size: 40, - color: AppColors.textPrimary, - ), - ), ], ), - SizedBox(height: 8), - Text( - tr('more.socialMedia'), - style: AppTextStyles.normal, - textAlign: TextAlign.center, - ), SizedBox(height: 24), Align( alignment: Alignment.bottomRight, diff --git a/lib/screens/notifications.screen.dart b/lib/screens/notifications.screen.dart index eb1783b..d27d0e4 100644 --- a/lib/screens/notifications.screen.dart +++ b/lib/screens/notifications.screen.dart @@ -8,7 +8,6 @@ import 'package:fastaval_app/widgets/widgets.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:timezone/standalone.dart' as tz; class NotificationsScreen extends StatelessWidget { final notificationCtrl = Get.find(); @@ -76,14 +75,6 @@ class NotificationsScreen extends StatelessWidget { } Widget notificationItem(InfosysNotification notification) { - var tzOffset = (tz - .getLocation('Europe/Copenhagen') - .currentTimeZone - .offset - .inMilliseconds / - 1000) - .round(); - return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,11 +83,11 @@ class NotificationsScreen extends StatelessWidget { child: Column( children: [ Text( - formatTime(notification.sendTime + tzOffset), + formatTime(notification.sendTime), style: AppTextStyles.normalBold, ), Text( - formatDay(notification.sendTime + tzOffset), + formatDay(notification.sendTime), style: AppTextStyles.normalBold, ), ], diff --git a/lib/screens/profile.screen.dart b/lib/screens/profile.screen.dart index 6094722..36ca716 100644 --- a/lib/screens/profile.screen.dart +++ b/lib/screens/profile.screen.dart @@ -132,6 +132,8 @@ class ProfileScreen extends StatelessWidget { child: SizedBox( width: double.infinity, child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, elevation: 2), onPressed: () => {HapticFeedback.heavyImpact(), appCtrl.logout()}, child: Text( tr('login.signOut'), diff --git a/lib/services/config.service.dart b/lib/services/config.service.dart index 31ec76e..fa6d9d0 100644 --- a/lib/services/config.service.dart +++ b/lib/services/config.service.dart @@ -15,7 +15,9 @@ class ConfigService { 'API': 'https://infosys.fastaval.dk/api', 'APItest': 'https://infosys-test.fastaval.dk/api', 'dates': "[]", - 'info_screen_boxes': "[]" + 'info_screen_boxes': "[]", + 'service_opening_hours': "[]", + 'info_opening_hours': "[]" }); await _remoteConfig.setConfigSettings( RemoteConfigSettings( @@ -35,4 +37,61 @@ class ConfigService { List datesList = jsonDecode(datesJson); return datesList.map((date) => date.toString()).toList(); } + + /// Returns the parsed `info_screen_boxes` JSON from Remote Config. + /// Falls back to an empty list if parsing fails. + List getInfoScreenBoxes() { + try { + String json = _remoteConfig.getString('info_screen_boxes'); + if (json.isEmpty) return []; + return jsonDecode(json) as List; + } catch (e) { + return []; + } + } + + /// The remote config now contains a key `info_opening_hours` which is + /// an array with objects for `serviceHours` and `storeHours`. + /// These helpers parse and return maps suitable for UI use, with + /// fallbacks to the constants in `AppConstants`. + Map> getServiceOpeningHours() { + try { + String json = _remoteConfig.getString('info_opening_hours'); + if (json.isEmpty) return >{}; + List arr = jsonDecode(json) as List; + final serviceObj = arr.firstWhere( + (e) => e is Map && e.containsKey('serviceHours'), + orElse: () => null, + ); + if (serviceObj == null) return >{}; + final Map raw = + Map.from(serviceObj['serviceHours']); + return raw.map((k, v) => MapEntry(k, Map.from(v))); + } catch (e) { + return >{}; + } + } + + /// Returns store opening hours as a map where each value may contain + /// nested structures (e.g. `title`, `location`, and a `days` array). + /// Use callers should treat the value as `Map`. + Map getStoreOpeningHours() { + try { + String json = _remoteConfig.getString('info_opening_hours'); + if (json.isEmpty) return {}; + List arr = jsonDecode(json) as List; + final storeObj = arr.firstWhere( + (e) => e is Map && e.containsKey('storeHours'), + orElse: () => null, + ); + if (storeObj == null) return {}; + final Map raw = + Map.from(storeObj['storeHours']); + // Ensure each store value is a Map when possible. + return raw.map( + (k, v) => MapEntry(k, v is Map ? Map.from(v) : v)); + } catch (e) { + return {}; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 8ff8eaa..d6eea1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.11+33 +version: 1.2.12+34 environment: sdk: ">=3.5.0 <4.0.0"