updated packages

This commit is contained in:
Vaibhav Surve 2025-12-10 14:54:27 +05:30
parent 03a3c1e06c
commit 1279b0e00f
14 changed files with 219 additions and 172 deletions

View File

@ -1,4 +1,4 @@
# On Field Work # OnFieldWork.com
A new Flutter project. A new Flutter project.

View File

@ -39,7 +39,7 @@ android {
// Specify your unique Application ID. This identifies your app on Google Play. // Specify your unique Application ID. This identifies your app on Google Play.
applicationId = "com.marcoonfieldwork.aiot" applicationId = "com.marcoonfieldwork.aiot"
// Set minimum and target SDK versions based on Flutter's configuration // Set minimum and target SDK versions based on Flutter's configuration
minSdk = 23 minSdkVersion = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
// Set version code and name based on Flutter's configuration (from pubspec.yaml) // Set version code and name based on Flutter's configuration (from pubspec.yaml)
versionCode = flutter.versionCode versionCode = flutter.versionCode

View File

@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:label="On Field Work" android:label="OnFieldWork.com"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

View File

@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.0" apply false id "com.android.application" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.21" apply false id "org.jetbrains.kotlin.android" version "2.2.21" apply false
id("com.google.gms.google-services") version "4.4.2" apply false id("com.google.gms.google-services") version "4.4.2" apply false
} }

View File

@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# App info # App info
APP_NAME="On Field Work" APP_NAME="OnFieldWork.com"
BUILD_DIR="build/app/outputs" BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}" echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>On Field Work</string> <string>OnFieldWork.com</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

View File

@ -8,5 +8,5 @@ class AppConstant {
static int iOSAppVersion = 1; static int iOSAppVersion = 1;
static String version = "1.0.0"; static String version = "1.0.0";
static String get appName => 'On Field Work'; static String get appName => 'OnFieldWork.com';
} }

View File

@ -7,7 +7,6 @@ import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
class TenantSelectionController extends GetxController { class TenantSelectionController extends GetxController {
// Tenant list // Tenant list
final tenants = <Tenant>[].obs; final tenants = <Tenant>[].obs;
@ -35,6 +34,7 @@ class TenantSelectionController extends GetxController {
if (data == null || data.isEmpty) { if (data == null || data.isEmpty) {
tenants.clear(); tenants.clear();
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning); logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
await LocalStorage.logout();
return; return;
} }

View File

@ -181,7 +181,8 @@ class AuthService {
_wrapErrorHandling( _wrapErrorHandling(
() async { () async {
final employeeInfo = await LocalStorage.getEmployeeInfo(); final employeeInfo = await LocalStorage.getEmployeeInfo();
if (employeeInfo == null) return null; // Fails immediately if info is missing if (employeeInfo == null)
return null; // Fails immediately if info is missing
final token = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
final responseData = await _networkRequest( final responseData = await _networkRequest(
@ -302,8 +303,7 @@ class AuthService {
// Fallback on all other failures // Fallback on all other failures
if (data != null && data['statusCode'] != 401) { if (data != null && data['statusCode'] != 401) {
_handleApiError( _handleApiError(data['statusCode'], data, "Fetching tenants");
data['statusCode'], data, "Fetching tenants");
} else if (data?['statusCode'] == 401 && hasRetried) { } else if (data?['statusCode'] == 401 && hasRetried) {
await _handleUnauthorized(); await _handleUnauthorized();
} }
@ -385,33 +385,37 @@ class AuthService {
authToken: token, authToken: token,
); );
if (data != null && data['success'] == true && data['data'] is Map<String, dynamic>) { if (data != null &&
final responseData = data['data'] as Map<String, dynamic>; data['success'] == true &&
data['data'] is Map<String, dynamic>) {
final responseData = data['data'] as Map<String, dynamic>;
final result = { final result = {
'permissions': _parsePermissions(responseData['featurePermissions']), 'permissions': _parsePermissions(responseData['featurePermissions']),
'employeeInfo': await _parseEmployeeInfo(responseData['employeeInfo']), 'employeeInfo': await _parseEmployeeInfo(responseData['employeeInfo']),
'projects': _parseProjectsInfo(responseData['projects']), 'projects': _parseProjectsInfo(responseData['projects']),
}; };
_userDataCache[token] = result; _userDataCache[token] = result;
logSafe("User data fetched and decrypted successfully."); logSafe("User data fetched and decrypted successfully.");
return result; return result;
} }
// Handle 401 Unauthorized via refreshToken/retry logic // Handle 401 Unauthorized via refreshToken/retry logic
if (data?['statusCode'] == 401 && !hasRetried) { if (data?['statusCode'] == 401 && !hasRetried) {
final refreshed = await refreshToken(); final refreshed = await refreshToken();
final newToken = await LocalStorage.getJwtToken(); final newToken = await LocalStorage.getJwtToken();
if (refreshed && newToken != null) { if (refreshed && newToken != null) {
return fetchAllUserData(newToken, hasRetried: true); return fetchAllUserData(newToken, hasRetried: true);
} }
} }
// Handle failure and unauthorized // Handle failure and unauthorized
if (data?['statusCode'] == 401 || data?['statusCode'] == 403 || data == null) { if (data?['statusCode'] == 401 ||
await _handleUnauthorized(); data?['statusCode'] == 403 ||
throw Exception('Unauthorized or Network Error. Token refresh failed.'); data == null) {
await _handleUnauthorized();
throw Exception('Unauthorized or Network Error. Token refresh failed.');
} }
final errorMsg = data['message'] ?? 'Unknown error'; final errorMsg = data['message'] ?? 'Unknown error';
@ -469,7 +473,6 @@ class AuthService {
logSafe("$context failed: $message [Status: $statusCode]", level: level); logSafe("$context failed: $message [Status: $statusCode]", level: level);
} }
/// General network request handler for both GET and POST. /// General network request handler for both GET and POST.
static Future<Map<String, dynamic>?> _networkRequest({ static Future<Map<String, dynamic>?> _networkRequest({
required String path, required String path,
@ -490,8 +493,10 @@ class AuthService {
level: LogLevel.info); level: LogLevel.info);
if (method == _HttpMethod.post) { if (method == _HttpMethod.post) {
response = await http.post(uri, headers: headers, body: jsonEncode(body)); response =
} else { // GET await http.post(uri, headers: headers, body: jsonEncode(body));
} else {
// GET
response = await http.get(uri, headers: headers); response = await http.get(uri, headers: headers);
} }
@ -501,14 +506,23 @@ class AuthService {
if (response.statusCode == 401) { if (response.statusCode == 401) {
await _handleUnauthorized(); await _handleUnauthorized();
} }
return {"statusCode": response.statusCode, "success": false, "message": "Empty response body"}; return {
"statusCode": response.statusCode,
"success": false,
"message": "Empty response body"
};
} }
final decrypted = decryptResponse(response.body); final decrypted = decryptResponse(response.body);
if (decrypted == null) { if (decrypted == null) {
logSafe("❌ Response decryption failed for $path", level: LogLevel.error); logSafe("❌ Response decryption failed for $path",
return {"statusCode": response.statusCode, "success": false, "message": "Failed to decrypt response"}; level: LogLevel.error);
return {
"statusCode": response.statusCode,
"success": false,
"message": "Failed to decrypt response"
};
} }
final Map<String, dynamic> result = decrypted is Map<String, dynamic> final Map<String, dynamic> result = decrypted is Map<String, dynamic>
@ -520,7 +534,6 @@ class AuthService {
level: LogLevel.info); level: LogLevel.info);
return {"statusCode": response.statusCode, ...result}; return {"statusCode": response.statusCode, ...result};
} catch (e, st) { } catch (e, st) {
_handleError("$path ${method.name.toUpperCase()} error", e, st); _handleError("$path ${method.name.toUpperCase()} error", e, st);
return null; return null;

View File

@ -25,7 +25,8 @@ int get flexColumns => MyScreenMedia.flexColumns;
class MaterialRadius { class MaterialRadius {
double xs, small, medium, large; double xs, small, medium, large;
MaterialRadius({this.xs = 2, this.small = 4, this.medium = 6, this.large = 8}); MaterialRadius(
{this.xs = 2, this.small = 4, this.medium = 6, this.large = 8});
} }
class ColorGroup { class ColorGroup {
@ -41,10 +42,12 @@ class AppTheme {
static Color primaryColor = Color(0xff663399); static Color primaryColor = Color(0xff663399);
static ThemeData getThemeFromThemeMode() { static ThemeData getThemeFromThemeMode() {
return ThemeCustomizer.instance.theme == ThemeMode.light ? lightTheme : darkTheme; return ThemeCustomizer.instance.theme == ThemeMode.light
? lightTheme
: darkTheme;
} }
/// -------------------------- Light Theme -------------------------------------------- /// /// -------------------------- Light Theme  -------------------------------------------- ///
static final ThemeData lightTheme = ThemeData( static final ThemeData lightTheme = ThemeData(
/// Brightness /// Brightness
@ -60,14 +63,18 @@ class AppTheme {
/// AppBar Theme /// AppBar Theme
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: Color(0xffF5F5F5), iconTheme: IconThemeData(color: Color(0xff495057)), actionsIconTheme: IconThemeData(color: Color(0xff495057))), backgroundColor: Color(0xffF5F5F5),
iconTheme: IconThemeData(color: Color(0xff495057)),
actionsIconTheme: IconThemeData(color: Color(0xff495057))),
/// Card Theme /// Card Theme
cardTheme: CardTheme(color: Color(0xffffffff)), // FIX: Use CardThemeData
cardTheme: CardThemeData(color: Color(0xffffffff)),
cardColor: Color(0xffffffff), cardColor: Color(0xffffffff),
/// Colorscheme /// Colorscheme
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xff663399), brightness: Brightness.light), colorScheme: ColorScheme.fromSeed(
seedColor: Color(0xff663399), brightness: Brightness.light),
snackBarTheme: SnackBarThemeData(actionTextColor: Colors.white), snackBarTheme: SnackBarThemeData(actionTextColor: Colors.white),
@ -86,10 +93,12 @@ class AppTheme {
dividerColor: Color(0xffdddddd), dividerColor: Color(0xffdddddd),
/// Bottom AppBar Theme /// Bottom AppBar Theme
bottomAppBarTheme: BottomAppBarTheme(color: Color(0xffeeeeee), elevation: 2), // FIX: Use BottomAppBarThemeData
bottomAppBarTheme:
BottomAppBarThemeData(color: Color(0xffeeeeee), elevation: 2),
/// Tab bar Theme /// Tab bar Theme
tabBarTheme: TabBarTheme( tabBarTheme: TabBarThemeData(
unselectedLabelColor: Color(0xff495057), unselectedLabelColor: Color(0xff495057),
labelColor: AppTheme.primaryColor, labelColor: AppTheme.primaryColor,
indicatorSize: TabBarIndicatorSize.label, indicatorSize: TabBarIndicatorSize.label,
@ -123,8 +132,11 @@ class AppTheme {
checkColor: WidgetStateProperty.all(Color(0xffffffff)), checkColor: WidgetStateProperty.all(Color(0xffffffff)),
fillColor: WidgetStateProperty.all(AppTheme.primaryColor), fillColor: WidgetStateProperty.all(AppTheme.primaryColor),
), ),
switchTheme: switchTheme: SwitchThemeData(
SwitchThemeData(thumbColor: WidgetStateProperty.resolveWith((states) => states.contains(WidgetState.selected) ? AppTheme.primaryColor : Colors.white)), thumbColor: WidgetStateProperty.resolveWith((states) =>
states.contains(WidgetState.selected)
? AppTheme.primaryColor
: Colors.white)),
/// Other Colors /// Other Colors
splashColor: Colors.white.withAlpha(100), splashColor: Colors.white.withAlpha(100),
@ -132,8 +144,9 @@ class AppTheme {
highlightColor: Color(0xffeeeeee), highlightColor: Color(0xffeeeeee),
); );
/// -------------------------- Dark Theme -------------------------------------------- /// /// -------------------------- Dark Theme  -------------------------------------------- ///
static final ThemeData darkTheme = ThemeData.dark(useMaterial3: false).copyWith( static final ThemeData darkTheme =
ThemeData.dark(useMaterial3: false).copyWith(
/// Brightness /// Brightness
/// Scaffold and Background color /// Scaffold and Background color
@ -146,7 +159,8 @@ class AppTheme {
appBarTheme: AppBarTheme(backgroundColor: Color(0xff262729)), appBarTheme: AppBarTheme(backgroundColor: Color(0xff262729)),
/// Card Theme /// Card Theme
cardTheme: CardTheme(color: Color(0xff1b1b1c)), // FIX: Use CardThemeData
cardTheme: CardThemeData(color: Color(0xff1b1b1c)),
cardColor: Color(0xff1b1b1c), cardColor: Color(0xff1b1b1c),
/// Colorscheme /// Colorscheme
@ -175,10 +189,13 @@ class AppTheme {
foregroundColor: Colors.white), foregroundColor: Colors.white),
/// Bottom AppBar Theme /// Bottom AppBar Theme
bottomAppBarTheme: BottomAppBarTheme(color: Color(0xff464c52), elevation: 2), // FIX: Use BottomAppBarThemeData
bottomAppBarTheme:
BottomAppBarThemeData(color: Color(0xff464c52), elevation: 2),
/// Tab bar Theme /// Tab bar Theme
tabBarTheme: TabBarTheme( // FIX: Use TabBarThemeData
tabBarTheme: TabBarThemeData(
unselectedLabelColor: Color(0xff495057), unselectedLabelColor: Color(0xff495057),
labelColor: AppTheme.primaryColor, labelColor: AppTheme.primaryColor,
indicatorSize: TabBarIndicatorSize.label, indicatorSize: TabBarIndicatorSize.label,
@ -230,7 +247,8 @@ class AppStyle {
containerRadius: AppStyle.containerRadius.medium, containerRadius: AppStyle.containerRadius.medium,
cardRadius: AppStyle.cardRadius.medium, cardRadius: AppStyle.cardRadius.medium,
buttonRadius: AppStyle.buttonRadius.medium, buttonRadius: AppStyle.buttonRadius.medium,
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'), defaultBreadCrumbItem:
MyBreadcrumbItem(name: 'OnFieldWork.com', route: '/client/dashboard'),
)); ));
bool isMobile = true; bool isMobile = true;
try { try {
@ -241,12 +259,16 @@ class AppStyle {
My.setFlexSpacing(isMobile ? 16 : 24); My.setFlexSpacing(isMobile ? 16 : 24);
} }
/// -------------------------- Styles -------------------------------------------- /// /// -------------------------- Styles  -------------------------------------------- ///
static MaterialRadius buttonRadius = MaterialRadius(small: 2, medium: 4, large: 8); static MaterialRadius buttonRadius =
static MaterialRadius cardRadius = MaterialRadius(xs: 2, small: 4, medium: 4, large: 8); MaterialRadius(small: 2, medium: 4, large: 8);
static MaterialRadius containerRadius = MaterialRadius(xs: 2, small: 4, medium: 4, large: 8); static MaterialRadius cardRadius =
static MaterialRadius imageRadius = MaterialRadius(xs: 2, small: 4, medium: 4, large: 8); MaterialRadius(xs: 2, small: 4, medium: 4, large: 8);
static MaterialRadius containerRadius =
MaterialRadius(xs: 2, small: 4, medium: 4, large: 8);
static MaterialRadius imageRadius =
MaterialRadius(xs: 2, small: 4, medium: 4, large: 8);
} }
class AppColors { class AppColors {
@ -262,13 +284,16 @@ class AppColors {
static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A)); static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC)); static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));
static ColorGroup lavender = ColorGroup(Color(0xffEAE2F3), Color(0xff7748AD)); static ColorGroup lavender = ColorGroup(Color(0xffEAE2F3), Color(0xff7748AD));
static ColorGroup queenPink = ColorGroup(Color(0xffE8D9DC), Color(0xff804D57)); static ColorGroup queenPink =
static ColorGroup blueViolet = ColorGroup(Color(0xffC5C6E7), Color(0xff3B3E91)); ColorGroup(Color(0xffE8D9DC), Color(0xff804D57));
static ColorGroup blueViolet =
ColorGroup(Color(0xffC5C6E7), Color(0xff3B3E91));
static ColorGroup rosePink = ColorGroup(Color(0xffFCB1E0), Color(0xffEC0999)); static ColorGroup rosePink = ColorGroup(Color(0xffFCB1E0), Color(0xffEC0999));
static ColorGroup rubinRed = ColorGroup(Color(0x98f6a8bd), Color(0xffd03760)); static ColorGroup rubinRed = ColorGroup(Color(0x98f6a8bd), Color(0xffd03760));
static ColorGroup favorite = rubinRed; static ColorGroup favorite = rubinRed;
static ColorGroup redOrange = ColorGroup(Color(0xffFFAD99), Color(0xffF53100)); static ColorGroup redOrange =
ColorGroup(Color(0xffFFAD99), Color(0xffF53100));
static Color notificationSuccessBGColor = Color(0xff117E68); static Color notificationSuccessBGColor = Color(0xff117E68);
static Color notificationSuccessTextColor = Color(0xffffffff); static Color notificationSuccessTextColor = Color(0xffffffff);
@ -278,7 +303,16 @@ class AppColors {
static Color notificationErrorTextColor = Color(0xffFF3B0A); static Color notificationErrorTextColor = Color(0xffFF3B0A);
static Color notificationErrorActionColor = Color(0xff006784); static Color notificationErrorActionColor = Color(0xff006784);
static List<ColorGroup> list = [redOrange, violet, blue, green, orange, skyBlue, lavender, blueViolet]; static List<ColorGroup> list = [
redOrange,
violet,
blue,
green,
orange,
skyBlue,
lavender,
blueViolet
];
static ColorGroup get random => list[Random().nextInt(list.length)]; static ColorGroup get random => list[Random().nextInt(list.length)];
@ -287,7 +321,13 @@ class AppColors {
} }
static Color getColorByRating(int rating) { static Color getColorByRating(int rating) {
var colors = {1: Color(0xfff0323c), 2: Color(0xcdf0323c), 3: star, 4: Color(0xcd3cd278), 5: Color(0xff3cd278)}; var colors = {
1: Color(0xfff0323c),
2: Color(0xcdf0323c),
3: star,
4: Color(0xcd3cd278),
5: Color(0xff3cd278)
};
return colors[rating] ?? colors[1]!; return colors[rating] ?? colors[1]!;
} }

View File

@ -11,8 +11,8 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/collection_dashboard_card.dart'; // import 'package:on_field_work/helpers/widgets/dashbaord/collection_dashboard_card.dart'; // Unused
import 'package:on_field_work/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart'; // import 'package:on_field_work/helpers/widgets/dashbaord/purchase_invoice_dashboard.dart'; // Unused
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
@ -80,7 +80,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Widget _sectionTitle(String title) { Widget _sectionTitle(String title) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8), padding: const EdgeInsets.only(left: 4, bottom: 8),
// OPTIMIZATION: Use MyText for consistent styling
child: MyText.titleMedium( child: MyText.titleMedium(
title, title,
fontWeight: 700, fontWeight: 700,
@ -127,12 +126,13 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
children: [ children: [
Row( Row(
children: [ children: [
Icon(Icons.info_outline, size: 30, color: Colors.white), const Icon(Icons.info_outline,
size: 30, color: Colors.white),
MySpacing.width(10), MySpacing.width(10),
Expanded( const Expanded(
child: Text( child: Text(
"No attendance data available yet.", "No attendance data available yet.",
style: const TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -142,9 +142,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
], ],
), ),
MySpacing.height(12), MySpacing.height(12),
Text( const Text(
"You are not added to this project or attendance data is not available.", "You are not added to this project or attendance data is not available.",
style: const TextStyle(color: Colors.white70, fontSize: 13), style: TextStyle(color: Colors.white70, fontSize: 13),
), ),
], ],
), ),
@ -215,7 +215,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
), ),
], ],
), ),
MySpacing.height(12), // OPTIMIZED MySpacing.height(12),
Text( Text(
infoText, infoText,
style: const TextStyle( style: const TextStyle(
@ -223,7 +223,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
fontSize: 13, fontSize: 13,
), ),
), ),
MySpacing.height(12), // OPTIMIZED MySpacing.height(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -262,7 +262,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final bool projectSelected = projectController.selectedProject != null; final bool projectSelected = projectController.selectedProject != null;
// These are String constants from permission_constants.dart (kept outside of Obx)
const List<String> cardOrder = [ const List<String> cardOrder = [
MenuItems.attendance, MenuItems.attendance,
MenuItems.employees, MenuItems.employees,
@ -273,7 +272,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.infraProjects, MenuItems.infraProjects,
]; ];
// OPTIMIZATION: Using a static map for meta data
final Map<String, _DashboardCardMeta> meta = { final Map<String, _DashboardCardMeta> meta = {
MenuItems.attendance: MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
@ -291,7 +289,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary), _DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
}; };
// OPTIMIZATION: Use map for faster lookup, then filter the preferred order
final Map<String, dynamic> allowed = { final Map<String, dynamic> allowed = {
for (final m in menuController.menuItems) for (final m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m, if (m.available && meta.containsKey(m.id)) m.id: m,
@ -308,8 +305,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_sectionTitle( _sectionTitle('Modules'),
'Modules'), // OPTIMIZATION: Reused section title helper
if (!projectSelected) if (!projectSelected)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -334,7 +330,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
), ),
GridView.builder( GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(), // Important!
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
@ -394,7 +390,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
color: color:
isEnabled ? cardMeta.color : Colors.grey.shade300, isEnabled ? cardMeta.color : Colors.grey.shade300,
), ),
MySpacing.height(6), // OPTIMIZED MySpacing.height(6),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text( child: Text(
@ -521,7 +517,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
child: Column( child: Column(
children: [ children: [
const TextField( const TextField(
// OPTIMIZED: Added const
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search project...', hintText: 'Search project...',
isDense: true, isDense: true,
@ -557,8 +552,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
), ),
); );
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Build // Build (MODIFIED FOR FIXED HEADER)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@override @override
@ -566,34 +562,39 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xfff5f6fa), backgroundColor: const Color(0xfff5f6fa),
body: Layout( body: Layout(
child: SingleChildScrollView( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_projectSelector(), _projectSelector(),
MySpacing.height(20), MySpacing.height(20),
_quickActions(), Expanded(
MySpacing.height(20), child: SingleChildScrollView(
_dashboardModules(), child: Column(
MySpacing.height(20), crossAxisAlignment: CrossAxisAlignment.start,
_sectionTitle('Reports & Analytics'), children: [
const CompactPurchaseInvoiceDashboard(), _quickActions(),
MySpacing.height(20), MySpacing.height(20),
CollectionsHealthWidget(), _dashboardModules(),
MySpacing.height(20), MySpacing.height(20),
_cardWrapper( _sectionTitle('Reports & Analytics'),
child: ExpenseTypeReportChart(), _cardWrapper(
), child: ExpenseTypeReportChart(),
_cardWrapper( ),
child: ExpenseByStatusWidget( _cardWrapper(
controller: dashboardController, child: ExpenseByStatusWidget(
controller: dashboardController,
),
),
_cardWrapper(
child: MonthlyExpenseDashboardChart(),
),
MySpacing.height(20),
],
),
), ),
), ),
_cardWrapper(
child: MonthlyExpenseDashboardChart(),
),
MySpacing.height(20),
], ],
), ),
), ),

View File

@ -24,7 +24,6 @@ class Layout extends StatefulWidget {
class _LayoutState extends State<Layout> with UIMixin { class _LayoutState extends State<Layout> with UIMixin {
final LayoutController controller = LayoutController(); final LayoutController controller = LayoutController();
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
bool hasMpin = true; bool hasMpin = true;
@ -46,72 +45,69 @@ class _LayoutState extends State<Layout> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MyResponsive(builder: (context, _, screenMT) { return MyResponsive(builder: (context, _, screenMT) {
return GetBuilder( return GetBuilder<LayoutController>(
init: controller, init: controller,
builder: (_) { builder: (_) {
return (screenMT.isMobile || screenMT.isTablet) return _buildScaffold(context);
? _buildScaffold(context, isMobile: true)
: _buildScaffold(context);
}, },
); );
}); });
} }
Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { Widget _buildScaffold(BuildContext context) {
final primaryColor = contentTheme.primary; final primaryColor = contentTheme.primary;
return Scaffold( return Scaffold(
key: controller.scaffoldKey, key: controller.scaffoldKey,
endDrawer: const UserProfileBar(), endDrawer: const UserProfileBar(),
floatingActionButton: widget.floatingActionButton, floatingActionButton: widget.floatingActionButton,
body: Column( body: Column(
children: [ children: [
// Solid primary background area Container(
Container( width: double.infinity,
color: primaryColor,
child: _buildHeaderContent(),
),
Expanded(
child: Container(
width: double.infinity, width: double.infinity,
color: primaryColor, decoration: BoxDecoration(
child: _buildHeaderContent(isMobile), gradient: LinearGradient(
), begin: Alignment.topCenter,
Expanded( end: Alignment.bottomCenter,
child: Container( colors: [
width: double.infinity, primaryColor,
decoration: BoxDecoration( primaryColor.withOpacity(0.7),
gradient: LinearGradient( primaryColor.withOpacity(0.0),
begin: Alignment.topCenter, ],
end: Alignment.bottomCenter, stops: const [0.0, 0.1, 0.3],
colors: [
primaryColor,
primaryColor.withOpacity(0.7),
primaryColor.withOpacity(0.0),
],
stops: const [0.0, 0.1, 0.3],
),
), ),
child: SafeArea( ),
top: false, child: SafeArea(
child: GestureDetector( top: false,
behavior: HitTestBehavior.translucent, child: GestureDetector(
onTap: () {}, behavior: HitTestBehavior.translucent,
child: SingleChildScrollView( onTap: () =>
key: controller.scrollKey, FocusScope.of(context).unfocus(),
padding: EdgeInsets.zero, child: widget.child ??
child: widget.child, const SizedBox.shrink(),
),
),
), ),
), ),
), ),
], ),
)); ],
),
);
} }
Widget _buildHeaderContent(bool isMobile) { Widget _buildHeaderContent() {
final selectedTenant = AuthService.currentTenant; final selectedTenant = AuthService.currentTenant;
final bool isBeta = ApiEndpoints.baseUrl.contains("stage");
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 45, 10, 0), padding: const EdgeInsets.fromLTRB(10, 45, 10, 0),
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 18), margin: const EdgeInsets.only(bottom: 10),
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -139,7 +135,7 @@ class _LayoutState extends State<Layout> with UIMixin {
), ),
// Beta badge // Beta badge
if (ApiEndpoints.baseUrl.contains("stage")) if (isBeta)
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,

View File

@ -46,15 +46,15 @@ dependencies:
carousel_slider: ^5.0.0 carousel_slider: ^5.0.0
reorderable_grid: ^1.0.10 reorderable_grid: ^1.0.10
loading_animation_widget: ^1.3.0 loading_animation_widget: ^1.3.0
intl: ^0.19.0 intl: ^0.20.2
syncfusion_flutter_core: ^29.1.40 syncfusion_flutter_core: ^31.2.18
syncfusion_flutter_sliders: ^29.1.40 syncfusion_flutter_sliders: ^31.2.18
file_picker: ^10.3.2 file_picker: ^10.3.2
timelines_plus: ^1.0.4 timelines_plus: ^1.0.4
syncfusion_flutter_charts: ^29.1.40 syncfusion_flutter_charts: ^31.2.18
appflowy_board: ^0.1.2 appflowy_board: ^0.1.2
syncfusion_flutter_calendar: ^29.1.40 syncfusion_flutter_calendar: ^31.2.18
syncfusion_flutter_maps: ^29.1.40 syncfusion_flutter_maps: ^31.2.18
http: ^1.6.0 http: ^1.6.0
geolocator: ^14.0.2 geolocator: ^14.0.2
permission_handler: ^12.0.1 permission_handler: ^12.0.1
@ -71,13 +71,13 @@ dependencies:
font_awesome_flutter: ^10.8.0 font_awesome_flutter: ^10.8.0
flutter_html: ^3.0.0 flutter_html: ^3.0.0
tab_indicator_styler: ^2.0.0 tab_indicator_styler: ^2.0.0
connectivity_plus: ^6.1.4 connectivity_plus: ^7.0.0
geocoding: ^4.0.0 geocoding: ^4.0.0
firebase_core: ^4.0.0 firebase_core: ^4.0.0
firebase_messaging: ^16.0.0 firebase_messaging: ^16.0.0
googleapis_auth: ^2.0.0 googleapis_auth: ^2.0.0
device_info_plus: ^11.3.0 device_info_plus: ^12.3.0
flutter_local_notifications: 19.4.0 flutter_local_notifications: ^19.5.0
equatable: ^2.0.7 equatable: ^2.0.7
mime: ^2.0.0 mime: ^2.0.0
timeago: ^3.7.1 timeago: ^3.7.1
@ -97,7 +97,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
@ -150,6 +150,3 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
dependency_overrides:
http: ^1.6.0