feat: Refactor user profile right bar for improved loading state and UI enhancements
This commit is contained in:
parent
3aab006bea
commit
a0f3475c5e
@ -30,8 +30,9 @@ class Avatar extends StatelessWidget {
|
||||
paddingAll: 0,
|
||||
color: bgColor,
|
||||
child: Center(
|
||||
child: MyText.labelSmall(
|
||||
child: MyText(
|
||||
initials,
|
||||
fontSize: size * 0.45, // 👈 scales with avatar size
|
||||
fontWeight: 600,
|
||||
color: textColor,
|
||||
),
|
||||
|
@ -11,45 +11,41 @@ class AttendanceDashboardChart extends StatelessWidget {
|
||||
|
||||
final DashboardController _controller = Get.find<DashboardController>();
|
||||
|
||||
static const List<Color> _flatColors = [
|
||||
Color(0xFFE57373), // Red 300
|
||||
Color(0xFF64B5F6), // Blue 300
|
||||
Color(0xFF81C784), // Green 300
|
||||
Color(0xFFFFB74D), // Orange 300
|
||||
Color(0xFFBA68C8), // Purple 300
|
||||
Color(0xFFFF8A65), // Deep Orange 300
|
||||
Color(0xFF4DB6AC), // Teal 300
|
||||
Color(0xFFA1887F), // Brown 400
|
||||
Color(0xFFDCE775), // Lime 300
|
||||
Color(0xFF9575CD), // Deep Purple 300
|
||||
static const List<Color> _flatColors = [
|
||||
Color(0xFFE57373), // Red 300
|
||||
Color(0xFF64B5F6), // Blue 300
|
||||
Color(0xFF81C784), // Green 300
|
||||
Color(0xFFFFB74D), // Orange 300
|
||||
Color(0xFFBA68C8), // Purple 300
|
||||
Color(0xFFFF8A65), // Deep Orange 300
|
||||
Color(0xFF4DB6AC), // Teal 300
|
||||
Color(0xFFA1887F), // Brown 400
|
||||
Color(0xFFDCE775), // Lime 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 = {};
|
||||
|
||||
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 {
|
||||
const _Header({
|
||||
Key? key,
|
||||
@ -154,7 +150,6 @@ class _Header extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title + toggle row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -162,7 +157,7 @@ class _Header extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium('Attendance Overview', fontWeight: 700),
|
||||
SizedBox(height: 2),
|
||||
const SizedBox(height: 2),
|
||||
MyText.bodySmall('Role-wise present count',
|
||||
color: Colors.grey),
|
||||
],
|
||||
@ -189,7 +184,6 @@ class _Header extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Range buttons
|
||||
Row(
|
||||
children: ["7D", "15D", "30D"]
|
||||
.map(
|
||||
@ -231,7 +225,7 @@ class _Header extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// No data message widget
|
||||
// No Data
|
||||
class _NoDataMessage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -255,7 +249,7 @@ class _NoDataMessage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Attendance Chart widget
|
||||
// Chart
|
||||
class _AttendanceChart extends StatelessWidget {
|
||||
const _AttendanceChart({
|
||||
Key? key,
|
||||
@ -278,7 +272,6 @@ class _AttendanceChart extends StatelessWidget {
|
||||
|
||||
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
|
||||
|
||||
// Check if all present values are zero
|
||||
final allZero = filteredRoles.every((role) {
|
||||
return data
|
||||
.where((entry) => entry['role'] == role)
|
||||
@ -302,7 +295,6 @@ class _AttendanceChart extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Normal chart rendering
|
||||
final formattedMap = {
|
||||
for (var e in data)
|
||||
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':
|
||||
@ -330,7 +322,7 @@ class _AttendanceChart extends StatelessWidget {
|
||||
final seriesData = filteredDates.map((date) {
|
||||
final key = '${role}_$date';
|
||||
return {'date': date, 'present': formattedMap[key] ?? 0};
|
||||
}).toList();
|
||||
}).where((d) => (d['present'] ?? 0) > 0).toList(); // ✅ remove 0 bars
|
||||
|
||||
return StackedColumnSeries<Map<String, dynamic>, String>(
|
||||
dataSource: seriesData,
|
||||
@ -338,9 +330,14 @@ class _AttendanceChart extends StatelessWidget {
|
||||
yValueMapper: (d, _) => d['present'],
|
||||
name: role,
|
||||
color: getRoleColor(role),
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
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(),
|
||||
@ -349,7 +346,7 @@ class _AttendanceChart extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Attendance Table widget
|
||||
// Table
|
||||
class _AttendanceTable extends StatelessWidget {
|
||||
const _AttendanceTable({
|
||||
Key? key,
|
||||
@ -374,7 +371,6 @@ class _AttendanceTable extends StatelessWidget {
|
||||
|
||||
final filteredRoles = data.map((e) => e['role'] as String).toSet().toList();
|
||||
|
||||
// Check if all present values are zero
|
||||
final allZero = filteredRoles.every((role) {
|
||||
return data
|
||||
.where((entry) => entry['role'] == role)
|
||||
@ -398,7 +394,6 @@ class _AttendanceTable extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Normal table rendering
|
||||
final formattedMap = {
|
||||
for (var e in data)
|
||||
'${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}':
|
||||
|
@ -210,34 +210,39 @@ class ProjectProgressChart extends StatelessWidget {
|
||||
child: SfCartesianChart(
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
primaryXAxis: DateTimeAxis(
|
||||
dateFormat: DateFormat('MMM d'),
|
||||
intervalType: DateTimeIntervalType.days,
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
// ✅ Use CategoryAxis so only nonZeroData dates show up
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelRotation: 0,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
labelFormat: '{value}',
|
||||
axisLine: AxisLine(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
),
|
||||
series: <CartesianSeries>[
|
||||
ColumnSeries<ChartTaskData, DateTime>(
|
||||
ColumnSeries<ChartTaskData, String>(
|
||||
name: 'Planned',
|
||||
dataSource: nonZeroData,
|
||||
xValueMapper: (d, _) => d.date,
|
||||
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
|
||||
yValueMapper: (d, _) => d.planned,
|
||||
color: _getTaskColor('Planned'),
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true, textStyle: TextStyle(fontSize: 11)),
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
ColumnSeries<ChartTaskData, DateTime>(
|
||||
ColumnSeries<ChartTaskData, String>(
|
||||
name: 'Completed',
|
||||
dataSource: nonZeroData,
|
||||
xValueMapper: (d, _) => d.date,
|
||||
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
|
||||
yValueMapper: (d, _) => d.completed,
|
||||
color: _getTaskColor('Completed'),
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true, textStyle: TextStyle(fontSize: 11)),
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -43,7 +43,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@ -92,8 +92,7 @@ class _ImageViewerDialogState extends State<ImageViewerDialog> {
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Center(
|
||||
child: Icon(Icons.broken_image,
|
||||
size: 48, color: Colors.grey),
|
||||
child: Icon(Icons.broken_image, size: 48, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -84,7 +84,7 @@ class _ContentView extends StatelessWidget {
|
||||
MyText.bodySmall(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
|
@ -14,8 +14,12 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class EmployeeDetailPage extends StatefulWidget {
|
||||
final String employeeId;
|
||||
|
||||
const EmployeeDetailPage({super.key, required this.employeeId});
|
||||
final bool fromProfile;
|
||||
const EmployeeDetailPage({
|
||||
super.key,
|
||||
required this.employeeId,
|
||||
this.fromProfile = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
|
||||
@ -201,7 +205,13 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard/employees'),
|
||||
onPressed: () {
|
||||
if (widget.fromProfile) {
|
||||
Get.back();
|
||||
} else {
|
||||
Get.offNamed('/dashboard/employees');
|
||||
}
|
||||
},
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,325 +1,341 @@
|
||||
import 'dart:ui';
|
||||
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/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_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:get/get.dart';
|
||||
import 'package:marco/model/employees/employee_info.dart';
|
||||
import 'package:marco/controller/auth/mpin_controller.dart';
|
||||
import 'package:marco/view/employees/employee_detail_screen.dart';
|
||||
|
||||
class UserProfileBar extends StatefulWidget {
|
||||
final bool isCondensed;
|
||||
|
||||
const UserProfileBar({super.key, this.isCondensed = false});
|
||||
const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key);
|
||||
|
||||
@override
|
||||
_UserProfileBarState createState() => _UserProfileBarState();
|
||||
State<UserProfileBar> createState() => _UserProfileBarState();
|
||||
}
|
||||
|
||||
class _UserProfileBarState extends State<UserProfileBar>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
final ThemeCustomizer customizer = ThemeCustomizer.instance;
|
||||
bool isCondensed = false;
|
||||
EmployeeInfo? employeeInfo;
|
||||
late EmployeeInfo employeeInfo;
|
||||
bool _isLoading = true;
|
||||
bool hasMpin = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEmployeeInfo();
|
||||
_checkMpinStatus();
|
||||
_initData();
|
||||
}
|
||||
|
||||
void _loadEmployeeInfo() {
|
||||
setState(() {
|
||||
employeeInfo = LocalStorage.getEmployeeInfo();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkMpinStatus() async {
|
||||
final bool mpinStatus = await LocalStorage.getIsMpin();
|
||||
setState(() {
|
||||
hasMpin = mpinStatus;
|
||||
});
|
||||
Future<void> _initData() async {
|
||||
employeeInfo = LocalStorage.getEmployeeInfo()!;
|
||||
hasMpin = await LocalStorage.getIsMpin();
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
isCondensed = widget.isCondensed;
|
||||
|
||||
return MyCard(
|
||||
borderRadiusAll: 16,
|
||||
paddingAll: 0,
|
||||
shadow: MyShadow(
|
||||
position: MyShadowPosition.centerRight,
|
||||
elevation: 6,
|
||||
blurRadius: 12,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
leftBarTheme.background.withOpacity(0.97),
|
||||
leftBarTheme.background.withOpacity(0.88),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
width: isCondensed ? 90 : 260,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
child: SafeArea(
|
||||
bottom: true,
|
||||
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,
|
||||
),
|
||||
final bool isCondensed = widget.isCondensed;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 14),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
width: isCondensed ? 84 : 260,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.95),
|
||||
Colors.white.withValues(alpha: 0.85),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
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() {
|
||||
Widget _supportAndSettingsMenu(bool condensed) {
|
||||
final spacingHeight = condensed ? 8.0 : 14.0;
|
||||
return Padding(
|
||||
padding: MySpacing.xy(16, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Column(
|
||||
children: [
|
||||
menuItem(
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.user,
|
||||
label: 'My Profile',
|
||||
onTap: _onProfileTap,
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.settings,
|
||||
label: "Settings",
|
||||
label: 'Settings',
|
||||
),
|
||||
MySpacing.height(14),
|
||||
menuItem(
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.badge_help,
|
||||
label: "Support",
|
||||
label: 'Support',
|
||||
),
|
||||
MySpacing.height(14),
|
||||
menuItem(
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.lock,
|
||||
label: hasMpin ? "Change MPIN" : "Set MPIN",
|
||||
iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent,
|
||||
labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent,
|
||||
onTap: () {
|
||||
final controller = Get.put(MPINController());
|
||||
if (hasMpin) {
|
||||
controller.setChangeMpinMode();
|
||||
}
|
||||
Navigator.pushNamed(context, "/auth/mpin-auth");
|
||||
},
|
||||
filled: true,
|
||||
label: hasMpin ? 'Change MPIN' : 'Set MPIN',
|
||||
iconColor: Colors.redAccent,
|
||||
textColor: Colors.redAccent,
|
||||
onTap: _onMpinTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget menuItem({
|
||||
Widget _menuItemRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
Color? iconColor,
|
||||
Color? labelColor,
|
||||
VoidCallback? onTap,
|
||||
bool filled = false,
|
||||
Color? iconColor,
|
||||
Color? textColor,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25),
|
||||
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35),
|
||||
child: Container(
|
||||
padding: MySpacing.xy(14, 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: filled
|
||||
? leftBarTheme.activeItemBackground.withOpacity(0.15)
|
||||
: Colors.transparent,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: filled
|
||||
? leftBarTheme.activeItemBackground.withOpacity(0.3)
|
||||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground),
|
||||
MySpacing.width(14),
|
||||
Icon(icon, size: 22, color: iconColor ?? Colors.black87),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
child: Text(
|
||||
label,
|
||||
color: labelColor ?? leftBarTheme.onBackground,
|
||||
fontWeight: 600,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, size: 20, color: Colors.black54),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Logout Button
|
||||
Widget logoutButton() {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
await _showLogoutConfirmation();
|
||||
},
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25),
|
||||
splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35),
|
||||
child: Container(
|
||||
padding: MySpacing.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: leftBarTheme.activeItemBackground,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
void _onProfileTap() {
|
||||
Get.to(() => EmployeeDetailPage(
|
||||
employeeId: employeeInfo.id,
|
||||
fromProfile: true,
|
||||
));
|
||||
}
|
||||
|
||||
void _onMpinTap() {
|
||||
final controller = Get.put(MPINController());
|
||||
if (hasMpin) controller.setChangeMpinMode();
|
||||
Navigator.pushNamed(context, "/auth/mpin-auth");
|
||||
}
|
||||
|
||||
Widget _logoutButton(bool condensed) {
|
||||
return Padding(
|
||||
padding: MySpacing.all(14),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
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 {
|
||||
bool? confirm = await showDialog<bool>(
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => _buildLogoutDialog(context),
|
||||
builder: _buildLogoutDialog,
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
await LocalStorage.logout();
|
||||
}
|
||||
if (confirm == true) await LocalStorage.logout();
|
||||
}
|
||||
|
||||
Widget _buildLogoutDialog(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
elevation: 10,
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
"Logout Confirmation",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
"Are you sure you want to logout?\nYou will need to login again to continue.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
style: TextStyle(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 30),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12)),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user