updated packages
This commit is contained in:
parent
03a3c1e06c
commit
1279b0e00f
@ -39,7 +39,7 @@ android {
|
||||
// Specify your unique Application ID. This identifies your app on Google Play.
|
||||
applicationId = "com.marcoonfieldwork.aiot"
|
||||
// Set minimum and target SDK versions based on Flutter's configuration
|
||||
minSdk = 23
|
||||
minSdkVersion = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
// Set version code and name based on Flutter's configuration (from pubspec.yaml)
|
||||
versionCode = flutter.versionCode
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:label="On Field Work"
|
||||
android:label="OnFieldWork.com"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
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("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# App info
|
||||
APP_NAME="On Field Work"
|
||||
APP_NAME="OnFieldWork.com"
|
||||
BUILD_DIR="build/app/outputs"
|
||||
|
||||
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>On Field Work</string>
|
||||
<string>OnFieldWork.com</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@ -8,5 +8,5 @@ class AppConstant {
|
||||
static int iOSAppVersion = 1;
|
||||
static String version = "1.0.0";
|
||||
|
||||
static String get appName => 'On Field Work';
|
||||
static String get appName => 'OnFieldWork.com';
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
|
||||
class TenantSelectionController extends GetxController {
|
||||
|
||||
// Tenant list
|
||||
final tenants = <Tenant>[].obs;
|
||||
|
||||
@ -35,6 +34,7 @@ class TenantSelectionController extends GetxController {
|
||||
if (data == null || data.isEmpty) {
|
||||
tenants.clear();
|
||||
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
|
||||
await LocalStorage.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -124,7 +124,7 @@ class AuthService {
|
||||
method: _HttpMethod.post,
|
||||
body: body,
|
||||
);
|
||||
|
||||
|
||||
if (data != null && data['success'] == true && data['data'] != null) {
|
||||
await LocalStorage.setJwtToken(data['data']['token']);
|
||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
||||
@ -181,9 +181,10 @@ class AuthService {
|
||||
_wrapErrorHandling(
|
||||
() async {
|
||||
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 responseData = await _networkRequest(
|
||||
path: "/auth/login-mpin",
|
||||
method: _HttpMethod.post,
|
||||
@ -196,7 +197,7 @@ class AuthService {
|
||||
authToken: token,
|
||||
);
|
||||
|
||||
// Handle token updates from MPIN login success if necessary,
|
||||
// Handle token updates from MPIN login success if necessary,
|
||||
// though typically refresh or a separate login handles this.
|
||||
if (responseData?['data'] != null) {
|
||||
await _handleLoginSuccess(responseData!['data']);
|
||||
@ -299,11 +300,10 @@ class AuthService {
|
||||
final refreshed = await refreshToken();
|
||||
if (refreshed) return getTenants(hasRetried: true);
|
||||
}
|
||||
|
||||
|
||||
// Fallback on all other failures
|
||||
if (data != null && data['statusCode'] != 401) {
|
||||
_handleApiError(
|
||||
data['statusCode'], data, "Fetching tenants");
|
||||
_handleApiError(data['statusCode'], data, "Fetching tenants");
|
||||
} else if (data?['statusCode'] == 401 && hasRetried) {
|
||||
await _handleUnauthorized();
|
||||
}
|
||||
@ -354,7 +354,7 @@ class AuthService {
|
||||
if (refreshed) return selectTenant(tenantId, hasRetried: true);
|
||||
await _handleUnauthorized();
|
||||
}
|
||||
|
||||
|
||||
// Fallback on all other failures
|
||||
if (data != null) {
|
||||
_handleApiError(data['statusCode'], data, "Selecting tenant");
|
||||
@ -385,33 +385,37 @@ class AuthService {
|
||||
authToken: token,
|
||||
);
|
||||
|
||||
if (data != null && data['success'] == true && data['data'] is Map<String, dynamic>) {
|
||||
final responseData = data['data'] as Map<String, dynamic>;
|
||||
|
||||
final result = {
|
||||
'permissions': _parsePermissions(responseData['featurePermissions']),
|
||||
'employeeInfo': await _parseEmployeeInfo(responseData['employeeInfo']),
|
||||
'projects': _parseProjectsInfo(responseData['projects']),
|
||||
};
|
||||
if (data != null &&
|
||||
data['success'] == true &&
|
||||
data['data'] is Map<String, dynamic>) {
|
||||
final responseData = data['data'] as Map<String, dynamic>;
|
||||
|
||||
_userDataCache[token] = result;
|
||||
logSafe("User data fetched and decrypted successfully.");
|
||||
return result;
|
||||
final result = {
|
||||
'permissions': _parsePermissions(responseData['featurePermissions']),
|
||||
'employeeInfo': await _parseEmployeeInfo(responseData['employeeInfo']),
|
||||
'projects': _parseProjectsInfo(responseData['projects']),
|
||||
};
|
||||
|
||||
_userDataCache[token] = result;
|
||||
logSafe("User data fetched and decrypted successfully.");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Handle 401 Unauthorized via refreshToken/retry logic
|
||||
if (data?['statusCode'] == 401 && !hasRetried) {
|
||||
final refreshed = await refreshToken();
|
||||
final newToken = await LocalStorage.getJwtToken();
|
||||
if (refreshed && newToken != null) {
|
||||
return fetchAllUserData(newToken, hasRetried: true);
|
||||
}
|
||||
final refreshed = await refreshToken();
|
||||
final newToken = await LocalStorage.getJwtToken();
|
||||
if (refreshed && newToken != null) {
|
||||
return fetchAllUserData(newToken, hasRetried: true);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle failure and unauthorized
|
||||
if (data?['statusCode'] == 401 || data?['statusCode'] == 403 || data == null) {
|
||||
await _handleUnauthorized();
|
||||
throw Exception('Unauthorized or Network Error. Token refresh failed.');
|
||||
if (data?['statusCode'] == 401 ||
|
||||
data?['statusCode'] == 403 ||
|
||||
data == null) {
|
||||
await _handleUnauthorized();
|
||||
throw Exception('Unauthorized or Network Error. Token refresh failed.');
|
||||
}
|
||||
|
||||
final errorMsg = data['message'] ?? 'Unknown error';
|
||||
@ -469,7 +473,6 @@ class AuthService {
|
||||
logSafe("❌ $context failed: $message [Status: $statusCode]", level: level);
|
||||
}
|
||||
|
||||
|
||||
/// General network request handler for both GET and POST.
|
||||
static Future<Map<String, dynamic>?> _networkRequest({
|
||||
required String path,
|
||||
@ -490,8 +493,10 @@ class AuthService {
|
||||
level: LogLevel.info);
|
||||
|
||||
if (method == _HttpMethod.post) {
|
||||
response = await http.post(uri, headers: headers, body: jsonEncode(body));
|
||||
} else { // GET
|
||||
response =
|
||||
await http.post(uri, headers: headers, body: jsonEncode(body));
|
||||
} else {
|
||||
// GET
|
||||
response = await http.get(uri, headers: headers);
|
||||
}
|
||||
|
||||
@ -501,26 +506,34 @@ class AuthService {
|
||||
if (response.statusCode == 401) {
|
||||
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);
|
||||
|
||||
if (decrypted == null) {
|
||||
logSafe("❌ Response decryption failed for $path", level: LogLevel.error);
|
||||
return {"statusCode": response.statusCode, "success": false, "message": "Failed to decrypt response"};
|
||||
logSafe("❌ Response decryption failed for $path",
|
||||
level: LogLevel.error);
|
||||
return {
|
||||
"statusCode": response.statusCode,
|
||||
"success": false,
|
||||
"message": "Failed to decrypt response"
|
||||
};
|
||||
}
|
||||
|
||||
final Map<String, dynamic> result = decrypted is Map<String, dynamic>
|
||||
? decrypted
|
||||
final Map<String, dynamic> result = decrypted is Map<String, dynamic>
|
||||
? decrypted
|
||||
: {"data": decrypted}; // Wrap non-map responses
|
||||
|
||||
logSafe(
|
||||
"⬅️ Response: ${jsonEncode(result)} [Status: ${response.statusCode}]",
|
||||
level: LogLevel.info);
|
||||
|
||||
return {"statusCode": response.statusCode, ...result};
|
||||
|
||||
return {"statusCode": response.statusCode, ...result};
|
||||
} catch (e, st) {
|
||||
_handleError("$path ${method.name.toUpperCase()} error", e, st);
|
||||
return null;
|
||||
@ -561,4 +574,4 @@ class AuthService {
|
||||
}
|
||||
isLoggedIn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,8 @@ int get flexColumns => MyScreenMedia.flexColumns;
|
||||
class MaterialRadius {
|
||||
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 {
|
||||
@ -41,10 +42,12 @@ class AppTheme {
|
||||
static Color primaryColor = Color(0xff663399);
|
||||
|
||||
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(
|
||||
/// Brightness
|
||||
@ -60,14 +63,18 @@ class AppTheme {
|
||||
|
||||
/// AppBar Theme
|
||||
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
|
||||
cardTheme: CardTheme(color: Color(0xffffffff)),
|
||||
// FIX: Use CardThemeData
|
||||
cardTheme: CardThemeData(color: Color(0xffffffff)),
|
||||
cardColor: Color(0xffffffff),
|
||||
|
||||
/// Colorscheme
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Color(0xff663399), brightness: Brightness.light),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Color(0xff663399), brightness: Brightness.light),
|
||||
|
||||
snackBarTheme: SnackBarThemeData(actionTextColor: Colors.white),
|
||||
|
||||
@ -86,10 +93,12 @@ class AppTheme {
|
||||
dividerColor: Color(0xffdddddd),
|
||||
|
||||
/// Bottom AppBar Theme
|
||||
bottomAppBarTheme: BottomAppBarTheme(color: Color(0xffeeeeee), elevation: 2),
|
||||
// FIX: Use BottomAppBarThemeData
|
||||
bottomAppBarTheme:
|
||||
BottomAppBarThemeData(color: Color(0xffeeeeee), elevation: 2),
|
||||
|
||||
/// Tab bar Theme
|
||||
tabBarTheme: TabBarTheme(
|
||||
tabBarTheme: TabBarThemeData(
|
||||
unselectedLabelColor: Color(0xff495057),
|
||||
labelColor: AppTheme.primaryColor,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
@ -123,8 +132,11 @@ class AppTheme {
|
||||
checkColor: WidgetStateProperty.all(Color(0xffffffff)),
|
||||
fillColor: WidgetStateProperty.all(AppTheme.primaryColor),
|
||||
),
|
||||
switchTheme:
|
||||
SwitchThemeData(thumbColor: WidgetStateProperty.resolveWith((states) => states.contains(WidgetState.selected) ? AppTheme.primaryColor : Colors.white)),
|
||||
switchTheme: SwitchThemeData(
|
||||
thumbColor: WidgetStateProperty.resolveWith((states) =>
|
||||
states.contains(WidgetState.selected)
|
||||
? AppTheme.primaryColor
|
||||
: Colors.white)),
|
||||
|
||||
/// Other Colors
|
||||
splashColor: Colors.white.withAlpha(100),
|
||||
@ -132,8 +144,9 @@ class AppTheme {
|
||||
highlightColor: Color(0xffeeeeee),
|
||||
);
|
||||
|
||||
/// -------------------------- Dark Theme -------------------------------------------- ///
|
||||
static final ThemeData darkTheme = ThemeData.dark(useMaterial3: false).copyWith(
|
||||
/// -------------------------- Dark Theme -------------------------------------------- ///
|
||||
static final ThemeData darkTheme =
|
||||
ThemeData.dark(useMaterial3: false).copyWith(
|
||||
/// Brightness
|
||||
|
||||
/// Scaffold and Background color
|
||||
@ -146,7 +159,8 @@ class AppTheme {
|
||||
appBarTheme: AppBarTheme(backgroundColor: Color(0xff262729)),
|
||||
|
||||
/// Card Theme
|
||||
cardTheme: CardTheme(color: Color(0xff1b1b1c)),
|
||||
// FIX: Use CardThemeData
|
||||
cardTheme: CardThemeData(color: Color(0xff1b1b1c)),
|
||||
cardColor: Color(0xff1b1b1c),
|
||||
|
||||
/// Colorscheme
|
||||
@ -175,10 +189,13 @@ class AppTheme {
|
||||
foregroundColor: Colors.white),
|
||||
|
||||
/// Bottom AppBar Theme
|
||||
bottomAppBarTheme: BottomAppBarTheme(color: Color(0xff464c52), elevation: 2),
|
||||
// FIX: Use BottomAppBarThemeData
|
||||
bottomAppBarTheme:
|
||||
BottomAppBarThemeData(color: Color(0xff464c52), elevation: 2),
|
||||
|
||||
/// Tab bar Theme
|
||||
tabBarTheme: TabBarTheme(
|
||||
// FIX: Use TabBarThemeData
|
||||
tabBarTheme: TabBarThemeData(
|
||||
unselectedLabelColor: Color(0xff495057),
|
||||
labelColor: AppTheme.primaryColor,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
@ -230,7 +247,8 @@ class AppStyle {
|
||||
containerRadius: AppStyle.containerRadius.medium,
|
||||
cardRadius: AppStyle.cardRadius.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;
|
||||
try {
|
||||
@ -241,12 +259,16 @@ class AppStyle {
|
||||
My.setFlexSpacing(isMobile ? 16 : 24);
|
||||
}
|
||||
|
||||
/// -------------------------- Styles -------------------------------------------- ///
|
||||
/// -------------------------- Styles -------------------------------------------- ///
|
||||
|
||||
static MaterialRadius buttonRadius = MaterialRadius(small: 2, medium: 4, large: 8);
|
||||
static MaterialRadius cardRadius = 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);
|
||||
static MaterialRadius buttonRadius =
|
||||
MaterialRadius(small: 2, medium: 4, large: 8);
|
||||
static MaterialRadius cardRadius =
|
||||
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 {
|
||||
@ -262,13 +284,16 @@ class AppColors {
|
||||
static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
|
||||
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));
|
||||
static ColorGroup lavender = ColorGroup(Color(0xffEAE2F3), Color(0xff7748AD));
|
||||
static ColorGroup queenPink = ColorGroup(Color(0xffE8D9DC), Color(0xff804D57));
|
||||
static ColorGroup blueViolet = ColorGroup(Color(0xffC5C6E7), Color(0xff3B3E91));
|
||||
static ColorGroup queenPink =
|
||||
ColorGroup(Color(0xffE8D9DC), Color(0xff804D57));
|
||||
static ColorGroup blueViolet =
|
||||
ColorGroup(Color(0xffC5C6E7), Color(0xff3B3E91));
|
||||
static ColorGroup rosePink = ColorGroup(Color(0xffFCB1E0), Color(0xffEC0999));
|
||||
|
||||
static ColorGroup rubinRed = ColorGroup(Color(0x98f6a8bd), Color(0xffd03760));
|
||||
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 notificationSuccessTextColor = Color(0xffffffff);
|
||||
@ -278,7 +303,16 @@ class AppColors {
|
||||
static Color notificationErrorTextColor = Color(0xffFF3B0A);
|
||||
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)];
|
||||
|
||||
@ -287,7 +321,13 @@ class AppColors {
|
||||
}
|
||||
|
||||
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]!;
|
||||
}
|
||||
|
||||
@ -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/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/collection_dashboard_card.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/purchase_invoice_dashboard.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'; // Unused
|
||||
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_spacing.dart';
|
||||
@ -80,7 +80,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
Widget _sectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||
// OPTIMIZATION: Use MyText for consistent styling
|
||||
child: MyText.titleMedium(
|
||||
title,
|
||||
fontWeight: 700,
|
||||
@ -127,12 +126,13 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 30, color: Colors.white),
|
||||
const Icon(Icons.info_outline,
|
||||
size: 30, color: Colors.white),
|
||||
MySpacing.width(10),
|
||||
Expanded(
|
||||
const Expanded(
|
||||
child: Text(
|
||||
"No attendance data available yet.",
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
@ -142,9 +142,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Text(
|
||||
const Text(
|
||||
"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(
|
||||
infoText,
|
||||
style: const TextStyle(
|
||||
@ -223,7 +223,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
MySpacing.height(12), // OPTIMIZED
|
||||
MySpacing.height(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -262,7 +262,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
|
||||
final bool projectSelected = projectController.selectedProject != null;
|
||||
|
||||
// These are String constants from permission_constants.dart (kept outside of Obx)
|
||||
const List<String> cardOrder = [
|
||||
MenuItems.attendance,
|
||||
MenuItems.employees,
|
||||
@ -273,7 +272,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
MenuItems.infraProjects,
|
||||
];
|
||||
|
||||
// OPTIMIZATION: Using a static map for meta data
|
||||
final Map<String, _DashboardCardMeta> meta = {
|
||||
MenuItems.attendance:
|
||||
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
||||
@ -291,7 +289,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
_DashboardCardMeta(LucideIcons.building_2, contentTheme.primary),
|
||||
};
|
||||
|
||||
// OPTIMIZATION: Use map for faster lookup, then filter the preferred order
|
||||
final Map<String, dynamic> allowed = {
|
||||
for (final m in menuController.menuItems)
|
||||
if (m.available && meta.containsKey(m.id)) m.id: m,
|
||||
@ -308,8 +305,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_sectionTitle(
|
||||
'Modules'), // OPTIMIZATION: Reused section title helper
|
||||
_sectionTitle('Modules'),
|
||||
if (!projectSelected)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -334,7 +330,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
physics: const NeverScrollableScrollPhysics(), // Important!
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
@ -394,7 +390,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
color:
|
||||
isEnabled ? cardMeta.color : Colors.grey.shade300,
|
||||
),
|
||||
MySpacing.height(6), // OPTIMIZED
|
||||
MySpacing.height(6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Text(
|
||||
@ -521,7 +517,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
child: Column(
|
||||
children: [
|
||||
const TextField(
|
||||
// OPTIMIZED: Added const
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search project...',
|
||||
isDense: true,
|
||||
@ -557,8 +552,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build
|
||||
// Build (MODIFIED FOR FIXED HEADER)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@override
|
||||
@ -566,34 +562,39 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xfff5f6fa),
|
||||
body: Layout(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_projectSelector(),
|
||||
MySpacing.height(20),
|
||||
_quickActions(),
|
||||
MySpacing.height(20),
|
||||
_dashboardModules(),
|
||||
MySpacing.height(20),
|
||||
_sectionTitle('Reports & Analytics'),
|
||||
const CompactPurchaseInvoiceDashboard(),
|
||||
MySpacing.height(20),
|
||||
CollectionsHealthWidget(),
|
||||
MySpacing.height(20),
|
||||
_cardWrapper(
|
||||
child: ExpenseTypeReportChart(),
|
||||
),
|
||||
_cardWrapper(
|
||||
child: ExpenseByStatusWidget(
|
||||
controller: dashboardController,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_quickActions(),
|
||||
MySpacing.height(20),
|
||||
_dashboardModules(),
|
||||
MySpacing.height(20),
|
||||
_sectionTitle('Reports & Analytics'),
|
||||
_cardWrapper(
|
||||
child: ExpenseTypeReportChart(),
|
||||
),
|
||||
_cardWrapper(
|
||||
child: ExpenseByStatusWidget(
|
||||
controller: dashboardController,
|
||||
),
|
||||
),
|
||||
_cardWrapper(
|
||||
child: MonthlyExpenseDashboardChart(),
|
||||
),
|
||||
MySpacing.height(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_cardWrapper(
|
||||
child: MonthlyExpenseDashboardChart(),
|
||||
),
|
||||
MySpacing.height(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -24,7 +24,6 @@ class Layout extends StatefulWidget {
|
||||
class _LayoutState extends State<Layout> with UIMixin {
|
||||
final LayoutController controller = LayoutController();
|
||||
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
|
||||
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
||||
|
||||
bool hasMpin = true;
|
||||
|
||||
@ -46,72 +45,69 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyResponsive(builder: (context, _, screenMT) {
|
||||
return GetBuilder(
|
||||
return GetBuilder<LayoutController>(
|
||||
init: controller,
|
||||
builder: (_) {
|
||||
return (screenMT.isMobile || screenMT.isTablet)
|
||||
? _buildScaffold(context, isMobile: true)
|
||||
: _buildScaffold(context);
|
||||
return _buildScaffold(context);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScaffold(BuildContext context, {bool isMobile = false}) {
|
||||
Widget _buildScaffold(BuildContext context) {
|
||||
final primaryColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
key: controller.scaffoldKey,
|
||||
endDrawer: const UserProfileBar(),
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
body: Column(
|
||||
children: [
|
||||
// Solid primary background area
|
||||
Container(
|
||||
key: controller.scaffoldKey,
|
||||
endDrawer: const UserProfileBar(),
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: primaryColor,
|
||||
child: _buildHeaderContent(),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: primaryColor,
|
||||
child: _buildHeaderContent(isMobile),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
primaryColor,
|
||||
primaryColor.withOpacity(0.7),
|
||||
primaryColor.withOpacity(0.0),
|
||||
],
|
||||
stops: const [0.0, 0.1, 0.3],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
primaryColor,
|
||||
primaryColor.withOpacity(0.7),
|
||||
primaryColor.withOpacity(0.0),
|
||||
],
|
||||
stops: const [0.0, 0.1, 0.3],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {},
|
||||
child: SingleChildScrollView(
|
||||
key: controller.scrollKey,
|
||||
padding: EdgeInsets.zero,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () =>
|
||||
FocusScope.of(context).unfocus(),
|
||||
child: widget.child ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderContent(bool isMobile) {
|
||||
Widget _buildHeaderContent() {
|
||||
final selectedTenant = AuthService.currentTenant;
|
||||
final bool isBeta = ApiEndpoints.baseUrl.contains("stage");
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 45, 10, 0),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@ -139,7 +135,7 @@ class _LayoutState extends State<Layout> with UIMixin {
|
||||
),
|
||||
|
||||
// Beta badge
|
||||
if (ApiEndpoints.baseUrl.contains("stage"))
|
||||
if (isBeta)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
||||
23
pubspec.yaml
23
pubspec.yaml
@ -46,15 +46,15 @@ dependencies:
|
||||
carousel_slider: ^5.0.0
|
||||
reorderable_grid: ^1.0.10
|
||||
loading_animation_widget: ^1.3.0
|
||||
intl: ^0.19.0
|
||||
syncfusion_flutter_core: ^29.1.40
|
||||
syncfusion_flutter_sliders: ^29.1.40
|
||||
intl: ^0.20.2
|
||||
syncfusion_flutter_core: ^31.2.18
|
||||
syncfusion_flutter_sliders: ^31.2.18
|
||||
file_picker: ^10.3.2
|
||||
timelines_plus: ^1.0.4
|
||||
syncfusion_flutter_charts: ^29.1.40
|
||||
syncfusion_flutter_charts: ^31.2.18
|
||||
appflowy_board: ^0.1.2
|
||||
syncfusion_flutter_calendar: ^29.1.40
|
||||
syncfusion_flutter_maps: ^29.1.40
|
||||
syncfusion_flutter_calendar: ^31.2.18
|
||||
syncfusion_flutter_maps: ^31.2.18
|
||||
http: ^1.6.0
|
||||
geolocator: ^14.0.2
|
||||
permission_handler: ^12.0.1
|
||||
@ -71,13 +71,13 @@ dependencies:
|
||||
font_awesome_flutter: ^10.8.0
|
||||
flutter_html: ^3.0.0
|
||||
tab_indicator_styler: ^2.0.0
|
||||
connectivity_plus: ^6.1.4
|
||||
connectivity_plus: ^7.0.0
|
||||
geocoding: ^4.0.0
|
||||
firebase_core: ^4.0.0
|
||||
firebase_messaging: ^16.0.0
|
||||
googleapis_auth: ^2.0.0
|
||||
device_info_plus: ^11.3.0
|
||||
flutter_local_notifications: 19.4.0
|
||||
device_info_plus: ^12.3.0
|
||||
flutter_local_notifications: ^19.5.0
|
||||
equatable: ^2.0.7
|
||||
mime: ^2.0.0
|
||||
timeago: ^3.7.1
|
||||
@ -97,7 +97,7 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# 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
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@ -150,6 +150,3 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.6.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user