Dashboard_Charts #67

Merged
vaibhav.surve merged 4 commits from Dashboard_Charts into main 2025-09-01 09:41:21 +00:00
8 changed files with 324 additions and 530 deletions
Showing only changes of commit a0f3475c5e - Show all commits

View File

@ -30,8 +30,9 @@ class Avatar extends StatelessWidget {
paddingAll: 0, paddingAll: 0,
color: bgColor, color: bgColor,
child: Center( child: Center(
child: MyText.labelSmall( child: MyText(
initials, initials,
fontSize: size * 0.45, // 👈 scales with avatar size
fontWeight: 600, fontWeight: 600,
color: textColor, color: textColor,
), ),

View File

@ -11,45 +11,41 @@ class AttendanceDashboardChart extends StatelessWidget {
final DashboardController _controller = Get.find<DashboardController>(); final DashboardController _controller = Get.find<DashboardController>();
static const List<Color> _flatColors = [ static const List<Color> _flatColors = [
Color(0xFFE57373), // Red 300 Color(0xFFE57373), // Red 300
Color(0xFF64B5F6), // Blue 300 Color(0xFF64B5F6), // Blue 300
Color(0xFF81C784), // Green 300 Color(0xFF81C784), // Green 300
Color(0xFFFFB74D), // Orange 300 Color(0xFFFFB74D), // Orange 300
Color(0xFFBA68C8), // Purple 300 Color(0xFFBA68C8), // Purple 300
Color(0xFFFF8A65), // Deep Orange 300 Color(0xFFFF8A65), // Deep Orange 300
Color(0xFF4DB6AC), // Teal 300 Color(0xFF4DB6AC), // Teal 300
Color(0xFFA1887F), // Brown 400 Color(0xFFA1887F), // Brown 400
Color(0xFFDCE775), // Lime 300 Color(0xFFDCE775), // Lime 300
Color(0xFF9575CD), // Deep Purple 300 Color(0xFF9575CD), // Deep Purple 300
Color(0xFF7986CB), // Indigo 300
Color(0xFFAED581), // Light Green 300
Color(0xFFFF7043), // Deep Orange 400
Color(0xFF4FC3F7), // Light Blue 300
Color(0xFFFFD54F), // Amber 300
Color(0xFF90A4AE), // Blue Grey 300
Color(0xFFE573BB), // Pink 300
Color(0xFF81D4FA), // Light Blue 200
Color(0xFFBCAAA4), // Brown 300
Color(0xFFA5D6A7), // Green 300
Color(0xFFCE93D8), // Purple 200
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
Color(0xFF80CBC4), // Teal 200
Color(0xFFFFF176), // Yellow 300
Color(0xFF90CAF9), // Blue 200
Color(0xFFE0E0E0), // Grey 300
Color(0xFFF48FB1), // Pink 200
Color(0xFFA1887F), // Brown 400 (repeat)
Color(0xFFB0BEC5), // Blue Grey 200
Color(0xFF81C784), // Green 300 (repeat)
Color(0xFFFFB74D), // Orange 300 (repeat)
Color(0xFF64B5F6), // Blue 300 (repeat)
];
Color(0xFF7986CB), // Indigo 300
Color(0xFFAED581), // Light Green 300
Color(0xFFFF7043), // Deep Orange 400
Color(0xFF4FC3F7), // Light Blue 300
Color(0xFFFFD54F), // Amber 300
Color(0xFF90A4AE), // Blue Grey 300
Color(0xFFE573BB), // Pink 300
Color(0xFF81D4FA), // Light Blue 200
Color(0xFFBCAAA4), // Brown 300
Color(0xFFA5D6A7), // Green 300
Color(0xFFCE93D8), // Purple 200
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
Color(0xFF80CBC4), // Teal 200
Color(0xFFFFF176), // Yellow 300
Color(0xFF90CAF9), // Blue 200
Color(0xFFE0E0E0), // Grey 300
Color(0xFFF48FB1), // Pink 200
Color(0xFFA1887F), // Brown 400 (repeat)
Color(0xFFB0BEC5), // Blue Grey 200
Color(0xFF81C784), // Green 300 (repeat)
Color(0xFFFFB74D), // Orange 300 (repeat)
Color(0xFF64B5F6), // Blue 300 (repeat)
];
// Cache role colors as static to maintain immutability in stateless widget context
static final Map<String, Color> _roleColorMap = {}; static final Map<String, Color> _roleColorMap = {};
Color _getRoleColor(String role) { Color _getRoleColor(String role) {
@ -132,7 +128,7 @@ static const List<Color> _flatColors = [
} }
} }
// Header as a separate widget for clarity & reusability // Header
class _Header extends StatelessWidget { class _Header extends StatelessWidget {
const _Header({ const _Header({
Key? key, Key? key,
@ -154,7 +150,6 @@ class _Header extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Title + toggle row
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -162,7 +157,7 @@ class _Header extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodyMedium('Attendance Overview', fontWeight: 700), MyText.bodyMedium('Attendance Overview', fontWeight: 700),
SizedBox(height: 2), const SizedBox(height: 2),
MyText.bodySmall('Role-wise present count', MyText.bodySmall('Role-wise present count',
color: Colors.grey), color: Colors.grey),
], ],
@ -189,7 +184,6 @@ class _Header extends StatelessWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Range buttons
Row( Row(
children: ["7D", "15D", "30D"] children: ["7D", "15D", "30D"]
.map( .map(
@ -231,7 +225,7 @@ class _Header extends StatelessWidget {
} }
} }
// No data message widget // No Data
class _NoDataMessage extends StatelessWidget { class _NoDataMessage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -255,7 +249,7 @@ class _NoDataMessage extends StatelessWidget {
} }
} }
// Attendance Chart widget // Chart
class _AttendanceChart extends StatelessWidget { class _AttendanceChart extends StatelessWidget {
const _AttendanceChart({ const _AttendanceChart({
Key? key, Key? key,
@ -278,7 +272,6 @@ class _AttendanceChart extends StatelessWidget {
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
// Check if all present values are zero
final allZero = filteredRoles.every((role) { final allZero = filteredRoles.every((role) {
return data return data
.where((entry) => entry['role'] == role) .where((entry) => entry['role'] == role)
@ -302,7 +295,6 @@ class _AttendanceChart extends StatelessWidget {
); );
} }
// Normal chart rendering
final formattedMap = { final formattedMap = {
for (var e in data) for (var e in data)
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':
@ -330,7 +322,7 @@ class _AttendanceChart extends StatelessWidget {
final seriesData = filteredDates.map((date) { final seriesData = filteredDates.map((date) {
final key = '${role}_$date'; final key = '${role}_$date';
return {'date': date, 'present': formattedMap[key] ?? 0}; return {'date': date, 'present': formattedMap[key] ?? 0};
}).toList(); }).where((d) => (d['present'] ?? 0) > 0).toList(); // remove 0 bars
return StackedColumnSeries<Map<String, dynamic>, String>( return StackedColumnSeries<Map<String, dynamic>, String>(
dataSource: seriesData, dataSource: seriesData,
@ -338,9 +330,14 @@ class _AttendanceChart extends StatelessWidget {
yValueMapper: (d, _) => d['present'], yValueMapper: (d, _) => d['present'],
name: role, name: role,
color: getRoleColor(role), color: getRoleColor(role),
dataLabelSettings: const DataLabelSettings( dataLabelSettings: DataLabelSettings(
isVisible: true, isVisible: true,
textStyle: TextStyle(fontSize: 11), builder: (dynamic data, _, __, ___, ____) {
return (data['present'] ?? 0) > 0
? Text('${data['present']}',
style: const TextStyle(fontSize: 11))
: const SizedBox.shrink();
},
), ),
); );
}).toList(), }).toList(),
@ -349,7 +346,7 @@ class _AttendanceChart extends StatelessWidget {
} }
} }
// Attendance Table widget // Table
class _AttendanceTable extends StatelessWidget { class _AttendanceTable extends StatelessWidget {
const _AttendanceTable({ const _AttendanceTable({
Key? key, Key? key,
@ -374,7 +371,6 @@ class _AttendanceTable extends StatelessWidget {
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
// Check if all present values are zero
final allZero = filteredRoles.every((role) { final allZero = filteredRoles.every((role) {
return data return data
.where((entry) => entry['role'] == role) .where((entry) => entry['role'] == role)
@ -398,7 +394,6 @@ class _AttendanceTable extends StatelessWidget {
); );
} }
// Normal table rendering
final formattedMap = { final formattedMap = {
for (var e in data) for (var e in data)
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':

View File

@ -210,34 +210,39 @@ class ProjectProgressChart extends StatelessWidget {
child: SfCartesianChart( child: SfCartesianChart(
tooltipBehavior: TooltipBehavior(enable: true), tooltipBehavior: TooltipBehavior(enable: true),
legend: Legend(isVisible: true, position: LegendPosition.bottom), legend: Legend(isVisible: true, position: LegendPosition.bottom),
primaryXAxis: DateTimeAxis( // Use CategoryAxis so only nonZeroData dates show up
dateFormat: DateFormat('MMM d'), primaryXAxis: CategoryAxis(
intervalType: DateTimeIntervalType.days, majorGridLines: const MajorGridLines(width: 0),
majorGridLines: MajorGridLines(width: 0), axisLine: const AxisLine(width: 0),
labelRotation: 0,
), ),
primaryYAxis: NumericAxis( primaryYAxis: NumericAxis(
labelFormat: '{value}', labelFormat: '{value}',
axisLine: AxisLine(width: 0), axisLine: const AxisLine(width: 0),
majorTickLines: MajorTickLines(size: 0), majorTickLines: const MajorTickLines(size: 0),
), ),
series: <CartesianSeries>[ series: <CartesianSeries>[
ColumnSeries<ChartTaskData, DateTime>( ColumnSeries<ChartTaskData, String>(
name: 'Planned', name: 'Planned',
dataSource: nonZeroData, dataSource: nonZeroData,
xValueMapper: (d, _) => d.date, xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
yValueMapper: (d, _) => d.planned, yValueMapper: (d, _) => d.planned,
color: _getTaskColor('Planned'), color: _getTaskColor('Planned'),
dataLabelSettings: DataLabelSettings( dataLabelSettings: const DataLabelSettings(
isVisible: true, textStyle: TextStyle(fontSize: 11)), isVisible: true,
textStyle: TextStyle(fontSize: 11),
),
), ),
ColumnSeries<ChartTaskData, DateTime>( ColumnSeries<ChartTaskData, String>(
name: 'Completed', name: 'Completed',
dataSource: nonZeroData, dataSource: nonZeroData,
xValueMapper: (d, _) => d.date, xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
yValueMapper: (d, _) => d.completed, yValueMapper: (d, _) => d.completed,
color: _getTaskColor('Completed'), color: _getTaskColor('Completed'),
dataLabelSettings: DataLabelSettings( dataLabelSettings: const DataLabelSettings(
isVisible: true, textStyle: TextStyle(fontSize: 11)), isVisible: true,
textStyle: TextStyle(fontSize: 11),
),
), ),
], ],
), ),

View File

@ -43,7 +43,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
color: Colors.white, color: Colors.white,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withValues(alpha: 0.1),
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
@ -92,8 +92,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
}, },
errorBuilder: (context, error, stackTrace) => errorBuilder: (context, error, stackTrace) =>
const Center( const Center(
child: Icon(Icons.broken_image, child: Icon(Icons.broken_image, size: 48, color: Colors.grey),
size: 48, color: Colors.grey),
), ),
), ),
); );

View File

@ -84,7 +84,7 @@ class _ContentView extends StatelessWidget {
MyText.bodySmall( MyText.bodySmall(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(

View File

@ -14,8 +14,12 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
class EmployeeDetailPage extends StatefulWidget { class EmployeeDetailPage extends StatefulWidget {
final String employeeId; final String employeeId;
final bool fromProfile;
const EmployeeDetailPage({super.key, required this.employeeId}); const EmployeeDetailPage({
super.key,
required this.employeeId,
this.fromProfile = false,
});
@override @override
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState(); State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
@ -201,7 +205,13 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
IconButton( IconButton(
icon: const Icon(Icons.arrow_back_ios_new, icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20), color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard/employees'), onPressed: () {
if (widget.fromProfile) {
Get.back();
} else {
Get.offNamed('/dashboard/employees');
}
},
), ),
MySpacing.width(8), MySpacing.width(8),
Expanded( Expanded(

View File

@ -1,244 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_flex.dart';
import 'package:marco/helpers/widgets/my_flex_item.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/my_loading_component.dart';
import 'package:marco/helpers/widgets/my_refresh_wrapper.dart';
import 'package:marco/model/my_paginated_table.dart';
class EmployeeScreen extends StatefulWidget {
const EmployeeScreen({super.key});
@override
State<EmployeeScreen> createState() => _EmployeeScreenState();
}
class _EmployeeScreenState extends State<EmployeeScreen> with UIMixin {
final EmployeesScreenController employeesScreenController =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
employeesScreenController.selectedProjectId = null;
await employeesScreenController.fetchAllEmployees();
employeesScreenController.update();
});
}
@override
Widget build(BuildContext context) {
return Layout(
child: Obx(() {
return LoadingComponent(
isLoading: employeesScreenController.isLoading.value,
loadingText: 'Loading Employees...',
child: GetBuilder<EmployeesScreenController>(
init: employeesScreenController,
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: MyText.titleMedium("Employee",
fontSize: 18, fontWeight: 600),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Employee', active: true),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
width: 1.5,
),
borderRadius: BorderRadius.circular(4),
),
child: PopupMenuButton<String>(
onSelected: (String value) async {
if (value.isEmpty) {
employeesScreenController.selectedProjectId =
null;
await employeesScreenController
.fetchAllEmployees();
} else {
employeesScreenController.selectedProjectId =
value;
await employeesScreenController
.fetchEmployeesByProject(value);
}
employeesScreenController.update();
},
itemBuilder: (BuildContext context) {
List<PopupMenuItem<String>> items = [
PopupMenuItem<String>(
value: '',
child: MyText.bodySmall('All Employees',
fontWeight: 600),
),
];
items.addAll(
employeesScreenController.projects
.map<PopupMenuItem<String>>((project) {
return PopupMenuItem<String>(
value: project.id,
child: MyText.bodySmall(project.name),
);
}).toList(),
);
return items;
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
employeesScreenController
.selectedProjectId ==
null
? 'All Employees'
: employeesScreenController.projects
.firstWhere((project) =>
project.id ==
employeesScreenController
.selectedProjectId)
.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 4),
const Icon(
Icons.arrow_drop_down,
size: 20,
color: Colors.black,
),
],
),
),
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
Get.toNamed('/employees/addEmployee');
},
child: Text('Add New Employee'),
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(
children: [
MyFlexItem(sizes: 'lg-6 ', child: employeeListTab()),
],
),
),
],
);
},
),
);
}),
);
}
Widget employeeListTab() {
if (employeesScreenController.employees.isEmpty) {
return Center(
child: MyText.bodySmall("No Employees Assigned to This Project",
fontWeight: 600),
);
}
final columns = <DataColumn>[
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Contact', color: contentTheme.primary)),
];
final rows =
employeesScreenController.employees.asMap().entries.map((entry) {
var employee = entry.value;
return DataRow(
cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.name, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.jobRole, color: Colors.grey),
],
),
),
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.email, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(employee.phoneNumber, color: Colors.grey),
],
),
),
],
);
}).toList();
return MyRefreshableContent(
onRefresh: () async {
if (employeesScreenController.selectedProjectId == null) {
await employeesScreenController.fetchAllEmployees();
} else {
await employeesScreenController.fetchEmployeesByProject(
employeesScreenController.selectedProjectId!,
);
}
},
child: MyPaginatedTable(
columns: columns,
rows: rows,
),
);
}
}

View File

@ -1,325 +1,341 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:get/get.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/controller/auth/mpin_controller.dart'; import 'package:marco/controller/auth/mpin_controller.dart';
import 'package:marco/view/employees/employee_detail_screen.dart';
class UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
final bool isCondensed; final bool isCondensed;
const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key);
const UserProfileBar({super.key, this.isCondensed = false});
@override @override
_UserProfileBarState createState() => _UserProfileBarState(); State<UserProfileBar> createState() => _UserProfileBarState();
} }
class _UserProfileBarState extends State<UserProfileBar> class _UserProfileBarState extends State<UserProfileBar>
with SingleTickerProviderStateMixin, UIMixin { with SingleTickerProviderStateMixin, UIMixin {
final ThemeCustomizer customizer = ThemeCustomizer.instance; late EmployeeInfo employeeInfo;
bool isCondensed = false; bool _isLoading = true;
EmployeeInfo? employeeInfo;
bool hasMpin = true; bool hasMpin = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadEmployeeInfo(); _initData();
_checkMpinStatus();
} }
void _loadEmployeeInfo() { Future<void> _initData() async {
setState(() { employeeInfo = LocalStorage.getEmployeeInfo()!;
employeeInfo = LocalStorage.getEmployeeInfo(); hasMpin = await LocalStorage.getIsMpin();
}); setState(() => _isLoading = false);
}
Future<void> _checkMpinStatus() async {
final bool mpinStatus = await LocalStorage.getIsMpin();
setState(() {
hasMpin = mpinStatus;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
isCondensed = widget.isCondensed; final bool isCondensed = widget.isCondensed;
return Padding(
return MyCard( padding: const EdgeInsets.only(left: 14),
borderRadiusAll: 16, child: ClipRRect(
paddingAll: 0, borderRadius: BorderRadius.circular(22),
shadow: MyShadow( child: BackdropFilter(
position: MyShadowPosition.centerRight, filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
elevation: 6, child: AnimatedContainer(
blurRadius: 12, duration: const Duration(milliseconds: 350),
), curve: Curves.easeInOut,
child: AnimatedContainer( width: isCondensed ? 84 : 260,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
leftBarTheme.background.withOpacity(0.97), Colors.white.withValues(alpha: 0.95),
leftBarTheme.background.withOpacity(0.88), Colors.white.withValues(alpha: 0.85),
], ],
begin: Alignment.topCenter, begin: Alignment.topLeft,
end: Alignment.bottomCenter, end: Alignment.bottomRight,
), ),
), borderRadius: BorderRadius.circular(22),
width: isCondensed ? 90 : 260, boxShadow: [
duration: const Duration(milliseconds: 300), BoxShadow(
curve: Curves.easeInOut, color: Colors.black.withValues(alpha: 0.06),
child: SafeArea( blurRadius: 18,
bottom: true, offset: const Offset(0, 8),
top: false, )
left: false,
right: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
userProfileSection(),
MySpacing.height(16),
supportAndSettingsMenu(),
const Spacer(),
logoutButton(),
],
),
),
),
);
}
/// User Profile Section - Avatar + Name
Widget userProfileSection() {
if (employeeInfo == null) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: CircularProgressIndicator()),
);
}
return Container(
width: double.infinity,
padding: MySpacing.fromLTRB(20, 50, 30, 50),
decoration: BoxDecoration(
color: leftBarTheme.activeItemBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
Avatar(
firstName: employeeInfo?.firstName ?? 'F',
lastName: employeeInfo?.lastName ?? 'N',
size: 50,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(
"${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}",
fontWeight: 700,
color: leftBarTheme.activeItemColor,
),
], ],
border: Border.all(
color: Colors.grey.withValues(alpha: 0.25),
width: 1,
),
),
child: SafeArea(
bottom: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_isLoading
? const _LoadingSection()
: _userProfileSection(isCondensed),
MySpacing.height(12),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.7,
color: Colors.grey.withValues(alpha: 0.25),
),
MySpacing.height(12),
_supportAndSettingsMenu(isCondensed),
const Spacer(),
Divider(
indent: 18,
endIndent: 18,
thickness: 0.35,
color: Colors.grey.withValues(alpha: 0.18),
),
_logoutButton(isCondensed),
],
),
), ),
), ),
),
),
);
}
Widget _userProfileSection(bool condensed) {
final padding = MySpacing.fromLTRB(
condensed ? 16 : 26,
condensed ? 20 : 28,
condensed ? 14 : 28,
condensed ? 10 : 18,
);
final avatarSize = condensed ? 48.0 : 64.0;
return Padding(
padding: padding,
child: Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor.withValues(alpha: 0.15),
blurRadius: 10,
spreadRadius: 1,
),
],
border: Border.all(
color: Theme.of(context).primaryColor,
width: 2,
),
),
child: Avatar(
firstName: employeeInfo.firstName,
lastName: employeeInfo.lastName,
size: avatarSize,
),
),
if (!condensed) ...[
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyLarge(
'${employeeInfo.firstName} ${employeeInfo.lastName}',
fontWeight: 700,
color: Colors.indigo,
),
MySpacing.height(4),
MyText.bodySmall(
"You're on track this month!",
color: Colors.black54,
),
],
),
),
],
], ],
), ),
); );
} }
/// Menu Section with Settings, Support & MPIN Widget _supportAndSettingsMenu(bool condensed) {
Widget supportAndSettingsMenu() { final spacingHeight = condensed ? 8.0 : 14.0;
return Padding( return Padding(
padding: MySpacing.xy(16, 16), padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column( child: Column(
children: [ children: [
menuItem( _menuItemRow(
icon: LucideIcons.user,
label: 'My Profile',
onTap: _onProfileTap,
),
SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.settings, icon: LucideIcons.settings,
label: "Settings", label: 'Settings',
), ),
MySpacing.height(14), SizedBox(height: spacingHeight),
menuItem( _menuItemRow(
icon: LucideIcons.badge_help, icon: LucideIcons.badge_help,
label: "Support", label: 'Support',
), ),
MySpacing.height(14), SizedBox(height: spacingHeight),
menuItem( _menuItemRow(
icon: LucideIcons.lock, icon: LucideIcons.lock,
label: hasMpin ? "Change MPIN" : "Set MPIN", label: hasMpin ? 'Change MPIN' : 'Set MPIN',
iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, iconColor: Colors.redAccent,
labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, textColor: Colors.redAccent,
onTap: () { onTap: _onMpinTap,
final controller = Get.put(MPINController());
if (hasMpin) {
controller.setChangeMpinMode();
}
Navigator.pushNamed(context, "/auth/mpin-auth");
},
filled: true,
), ),
], ],
), ),
); );
} }
Widget menuItem({ Widget _menuItemRow({
required IconData icon, required IconData icon,
required String label, required String label,
Color? iconColor,
Color? labelColor,
VoidCallback? onTap, VoidCallback? onTap,
bool filled = false, Color? iconColor,
Color? textColor,
}) { }) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25),
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35),
child: Container( child: Container(
padding: MySpacing.xy(14, 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: filled color: Colors.white.withOpacity(0.9),
? leftBarTheme.activeItemBackground.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
color: filled
? leftBarTheme.activeItemBackground.withOpacity(0.3)
: Colors.transparent,
width: 1,
),
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground), Icon(icon, size: 22, color: iconColor ?? Colors.black87),
MySpacing.width(14), const SizedBox(width: 16),
Expanded( Expanded(
child: MyText.bodyMedium( child: Text(
label, label,
color: labelColor ?? leftBarTheme.onBackground, style: TextStyle(
fontWeight: 600, fontSize: 15,
fontWeight: FontWeight.w600,
color: textColor ?? Colors.black87,
),
), ),
), ),
const Icon(Icons.chevron_right, size: 20, color: Colors.black54),
], ],
), ),
), ),
); );
} }
/// Logout Button void _onProfileTap() {
Widget logoutButton() { Get.to(() => EmployeeDetailPage(
return InkWell( employeeId: employeeInfo.id,
onTap: () async { fromProfile: true,
await _showLogoutConfirmation(); ));
}, }
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16), void _onMpinTap() {
bottomRight: Radius.circular(16), final controller = Get.put(MPINController());
), if (hasMpin) controller.setChangeMpinMode();
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), Navigator.pushNamed(context, "/auth/mpin-auth");
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), }
child: Container(
padding: MySpacing.all(16), Widget _logoutButton(bool condensed) {
decoration: BoxDecoration( return Padding(
color: leftBarTheme.activeItemBackground, padding: MySpacing.all(14),
borderRadius: const BorderRadius.only( child: SizedBox(
bottomLeft: Radius.circular(16), width: double.infinity,
bottomRight: Radius.circular(16), child: ElevatedButton.icon(
onPressed: _showLogoutConfirmation,
icon: const Icon(LucideIcons.log_out, size: 22, color: Colors.white),
label: condensed
? const SizedBox.shrink()
: MyText.bodyMedium(
"Logout",
color: Colors.white,
fontWeight: 700,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
shadowColor: Colors.red.shade200,
padding: EdgeInsets.symmetric(
vertical: condensed ? 14 : 18,
horizontal: condensed ? 14 : 22,
),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), ),
), ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(
"Logout",
color: leftBarTheme.activeItemColor,
fontWeight: 600,
),
MySpacing.width(8),
Icon(
LucideIcons.log_out,
size: 20,
color: leftBarTheme.activeItemColor,
),
],
),
), ),
); );
} }
Future<void> _showLogoutConfirmation() async { Future<void> _showLogoutConfirmation() async {
bool? confirm = await showDialog<bool>( final bool? confirm = await showDialog<bool>(
context: context, context: context,
builder: (context) => _buildLogoutDialog(context), builder: _buildLogoutDialog,
); );
if (confirm == true) await LocalStorage.logout();
if (confirm == true) {
await LocalStorage.logout();
}
} }
Widget _buildLogoutDialog(BuildContext context) { Widget _buildLogoutDialog(BuildContext context) {
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 10,
backgroundColor: Colors.white,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent), Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700),
const SizedBox(height: 16), const SizedBox(height: 18),
Text( const Text(
"Logout Confirmation", "Logout Confirmation",
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 22,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onBackground, color: Colors.black87),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 14),
Text( const Text(
"Are you sure you want to logout?\nYou will need to login again to continue.", "Are you sure you want to logout?\nYou will need to login again to continue.",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 16, color: Colors.black54),
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 30),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade700, foregroundColor: Colors.grey.shade700,
), padding: const EdgeInsets.symmetric(vertical: 12)),
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 18),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(14)),
),
), ),
child: const Text("Logout"), child: const Text("Logout"),
), ),
@ -332,3 +348,15 @@ class _UserProfileBarState extends State<UserProfileBar>
); );
} }
} }
class _LoadingSection extends StatelessWidget {
const _LoadingSection();
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 44, horizontal: 8),
child: Center(child: CircularProgressIndicator()),
);
}
}