diff --git a/assets/logo/loading_logo.png b/assets/logo/loading_logo.png new file mode 100644 index 0000000..81871d3 Binary files /dev/null and b/assets/logo/loading_logo.png differ diff --git a/lib/helpers/widgets/my_loading_component.dart b/lib/helpers/widgets/my_loading_component.dart new file mode 100644 index 0000000..4ddaca7 --- /dev/null +++ b/lib/helpers/widgets/my_loading_component.dart @@ -0,0 +1,105 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:marco/images.dart'; + +class LoadingComponent extends StatelessWidget { + final bool isLoading; + final Widget child; + final Color overlayColor; + final double overlayOpacity; + final double imageSize; + final bool showDots; + final String loadingText; + + const LoadingComponent({ + Key? key, + required this.isLoading, + required this.child, + this.overlayColor = Colors.black, + this.overlayOpacity = 0.2, + this.imageSize = 100, + this.showDots = true, + this.loadingText = 'Loading...', + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + if (isLoading) _buildLoadingOverlay(), + ], + ); + } + + Widget _buildLoadingOverlay() { + return Positioned.fill( + child: Semantics( + label: 'Loading...', + child: Stack( + children: [ + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + color: overlayColor.withOpacity(overlayOpacity), + ), + ), + Center( + child: _LoadingAnimation( + imageSize: imageSize, + showDots: showDots, + loadingText: loadingText, + ), + ), + ], + ), + ), + ); + } +} + +class _LoadingAnimation extends StatelessWidget { + final double imageSize; + final bool showDots; + final String loadingText; + + const _LoadingAnimation({ + Key? key, + required this.imageSize, + required this.showDots, + required this.loadingText, + }) : super(key: key); + + static const _textStyle = TextStyle( + fontSize: 12, + color: Colors.white, + ); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Images.loadingLogo, + height: imageSize, + width: imageSize, + fit: BoxFit.contain, + ), + const SizedBox(height: 8), + Text( + loadingText, + style: _textStyle, + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + if (showDots) + LoadingAnimationWidget.waveDots( + color: const Color(0xFFEF0000), + size: 50, + ), + ], + ); + } +} diff --git a/lib/images.dart b/lib/images.dart index 9c39401..4606cb6 100644 --- a/lib/images.dart +++ b/lib/images.dart @@ -10,7 +10,7 @@ class Images { static String logoDark = 'assets/logo/logo_dark.png'; static String logoDarkSmall = 'assets/logo/logo_dark_small.png'; static String authBackground = 'assets/auth_background.jpg'; - + static String loadingLogo = 'assets/logo/loading_logo.png'; static String randomImage(List images) { return images[Random().nextInt(images.length)]; } diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 0e995c6..7dae8b2 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -21,6 +21,7 @@ import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/helpers/widgets/my_refresh_wrapper.dart'; import 'package:marco/model/my_paginated_table.dart'; +import 'package:marco/helpers/widgets/my_loading_component.dart'; class AttendanceScreen extends StatefulWidget { const AttendanceScreen({super.key}); @@ -51,149 +52,153 @@ class _AttendanceScreenState extends State with UIMixin { init: attendanceController, tag: 'attendance_dashboard_controller', builder: (controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.titleMedium("Attendance", - fontSize: 18, fontWeight: 600), - MyBreadcrumb( - children: [ - MyBreadcrumbItem(name: 'Dashboard'), - MyBreadcrumbItem(name: 'Attendance', active: true), - ], - ), - ], - ), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing / 2), - child: MyFlex( - children: [ - // Popup Menu for Project Selection - MyFlexItem( - sizes: 'lg-12', - child: MyContainer.bordered( - padding: MySpacing.xy(8, 8), - child: PopupMenuButton( - onSelected: (value) { - setState(() { - attendanceController.selectedProjectId = value; - attendanceController - .fetchEmployeesByProject(value); - attendanceController.fetchAttendanceLogs(value); - }); - }, - itemBuilder: (BuildContext context) { - if (attendanceController.projects.isEmpty) { - return [ - PopupMenuItem( - value: '', - child: MyText.bodySmall('No Data', - fontWeight: 600), - ) - ]; - } - return attendanceController.projects - .map((project) { - return PopupMenuItem( - value: project.id.toString(), - height: 32, - child: MyText.bodySmall( - project.name, - color: theme.colorScheme.onSurface, - fontWeight: 600, - ), - ); - }).toList(); - }, - color: theme.cardTheme.color, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.labelSmall( - attendanceController.selectedProjectId != null - ? attendanceController.projects - .firstWhereOrNull((proj) => - proj.id.toString() == - attendanceController - .selectedProjectId) - ?.name ?? - 'Select a Project' - : 'Select a Project', - color: theme.colorScheme.onSurface, - ), - Icon(LucideIcons.chevron_down, - size: 20, - color: theme.colorScheme.onSurface), - ], - ), - ), + return LoadingComponent( + isLoading: controller.isLoading.value, + loadingText: 'Loading Attendance...', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium("Attendance", + fontSize: 18, fontWeight: 600), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Dashboard'), + MyBreadcrumbItem(name: 'Attendance', active: true), + ], ), - ), - - // Tabs for Employee List, Logs, and Regularization - MyFlexItem( - sizes: 'lg-12', - child: Obx(() { - bool hasRegularizationPermission = - permissionController.hasPermission( - Permissions.regularizeAttendance); - - final tabs = [ - const Tab(text: 'Employee List'), - const Tab(text: 'Logs'), - if (hasRegularizationPermission) - const Tab(text: 'Regularization'), - ]; - - final views = [ - employeeListTab(), - reportsTab(context), - if (hasRegularizationPermission) - regularizationTab(context), - ]; - - return DefaultTabController( - length: tabs.length, - child: MyCard.bordered( - borderRadiusAll: 4, - border: - Border.all(color: Colors.grey.withAlpha(50)), - shadow: MyShadow( - elevation: 1, - position: MyShadowPosition.bottom), - paddingAll: 10, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing / 2), + child: MyFlex( + children: [ + // Popup Menu for Project Selection + MyFlexItem( + sizes: 'lg-12', + child: MyContainer.bordered( + padding: MySpacing.xy(8, 8), + child: PopupMenuButton( + onSelected: (value) { + setState(() { + attendanceController.selectedProjectId = value; + attendanceController + .fetchEmployeesByProject(value); + attendanceController.fetchAttendanceLogs(value); + }); + }, + itemBuilder: (BuildContext context) { + if (attendanceController.projects.isEmpty) { + return [ + PopupMenuItem( + value: '', + child: MyText.bodySmall('No Data', + fontWeight: 600), + ) + ]; + } + return attendanceController.projects + .map((project) { + return PopupMenuItem( + value: project.id.toString(), + height: 32, + child: MyText.bodySmall( + project.name, + color: theme.colorScheme.onSurface, + fontWeight: 600, + ), + ); + }).toList(); + }, + color: theme.cardTheme.color, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TabBar( - labelColor: theme.colorScheme.primary, - unselectedLabelColor: theme - .colorScheme.onSurface - .withAlpha(150), - tabs: tabs, - ), - MySpacing.height(16), - SizedBox( - height: 550, - child: TabBarView(children: views), + MyText.labelSmall( + attendanceController.selectedProjectId != null + ? attendanceController.projects + .firstWhereOrNull((proj) => + proj.id.toString() == + attendanceController + .selectedProjectId) + ?.name ?? + 'Select a Project' + : 'Select a Project', + color: theme.colorScheme.onSurface, ), + Icon(LucideIcons.chevron_down, + size: 20, + color: theme.colorScheme.onSurface), ], ), ), - ); - }), - ), - ], + ), + ), + + // Tabs for Employee List, Logs, and Regularization + MyFlexItem( + sizes: 'lg-12', + child: Obx(() { + bool hasRegularizationPermission = + permissionController.hasPermission( + Permissions.regularizeAttendance); + + final tabs = [ + const Tab(text: 'Employee List'), + const Tab(text: 'Logs'), + if (hasRegularizationPermission) + const Tab(text: 'Regularization'), + ]; + + final views = [ + employeeListTab(), + reportsTab(context), + if (hasRegularizationPermission) + regularizationTab(context), + ]; + + return DefaultTabController( + length: tabs.length, + child: MyCard.bordered( + borderRadiusAll: 4, + border: + Border.all(color: Colors.grey.withAlpha(50)), + shadow: MyShadow( + elevation: 1, + position: MyShadowPosition.bottom), + paddingAll: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + labelColor: theme.colorScheme.primary, + unselectedLabelColor: theme + .colorScheme.onSurface + .withAlpha(150), + tabs: tabs, + ), + MySpacing.height(16), + SizedBox( + height: 550, + child: TabBarView(children: views), + ), + ], + ), + ), + ); + }), + ), + ], + ), ), - ), - ], + ], + ), ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index d3badc8..663e05d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,6 +99,7 @@ flutter: - assets/coin/ - assets/dummy/ecommerce/ - assets/dummy/single_product/ + - assets/logo/loading_logo.png # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg