implemented peginated table

This commit is contained in:
Vaibhav Surve 2025-04-30 17:55:09 +05:30
parent 1007e081b9
commit daf40ab2ce
13 changed files with 1221 additions and 658 deletions

View File

@ -9,6 +9,7 @@ class LoginController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false, isChecked = false;
RxBool isLoading = false.obs; // Add reactive loading state
final String _dummyEmail = "admin@marcobms.com";
final String _dummyPassword = "User@123";
@ -16,10 +17,7 @@ class LoginController extends MyController {
@override
void onInit() {
basicValidator.addField('username', required: true, label: "User_Name", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
basicValidator.addField('password',
required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
basicValidator.addField('password', required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
super.onInit();
}
@ -35,7 +33,10 @@ class LoginController extends MyController {
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
// Set loading to true
isLoading.value = true;
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
basicValidator.addErrors(errors);
@ -45,6 +46,9 @@ class LoginController extends MyController {
String nextUrl = Uri.parse(ModalRoute.of(Get.context!)?.settings.name ?? "").queryParameters['next'] ?? "/home";
Get.toNamed(nextUrl);
}
// Set loading to false after the API call is complete
isLoading.value = false;
update();
}
}
@ -57,3 +61,4 @@ class LoginController extends MyController {
Get.offAndToNamed('/auth/register_account');
}
}

View File

@ -24,6 +24,8 @@ class AttendanceController extends GetxController {
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = false.obs; // Added loading flag
@override
void onInit() {
super.onInit();
@ -42,7 +44,9 @@ class AttendanceController extends GetxController {
}
Future<void> fetchProjects() async {
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getProjects();
isLoading.value = false; // Set loading to false after API call completes
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
@ -57,19 +61,22 @@ class AttendanceController extends GetxController {
Future<void> _fetchProjectData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true; // Set loading to true before API call
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
isLoading.value = false; // Set loading to false after data is fetched
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
final response =
await ApiService.getEmployeesByProject(int.parse(projectId));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getEmployeesByProject(int.parse(projectId));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
update();
@ -84,20 +91,25 @@ class AttendanceController extends GetxController {
int projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true, // <- add this flag
}) async {
try {
final image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) return false;
XFile? image;
if (imageCapture) {
image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) return false;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final imageName =
ApiService.generateImageName(employeeId, employees.length + 1);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: ""; // Empty or null if not capturing image
return await ApiService.uploadAttendanceImage(
id,
@ -109,6 +121,7 @@ class AttendanceController extends GetxController {
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture, // <- pass flag down
);
} catch (e) {
print("Error capturing or uploading attendance: $e");
@ -125,8 +138,7 @@ class AttendanceController extends GetxController {
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDateAttendance ??
DateTime.now().subtract(const Duration(days: 7)),
start: startDateAttendance ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateAttendance ?? DateTime.now(),
),
);
@ -150,15 +162,16 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getAttendanceLogs(
int.parse(projectId),
dateFrom: dateFrom,
dateTo: dateTo,
);
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList();
print("Attendance logs fetched: ${response}");
update();
} else {
@ -173,8 +186,9 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
final response =
await ApiService.getRegularizationLogs(int.parse(projectId));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getRegularizationLogs(int.parse(projectId));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
regularizationLogs = response
@ -189,8 +203,9 @@ class AttendanceController extends GetxController {
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
final response =
await ApiService.getAttendanceLogView(int.parse(id));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getAttendanceLogView(int.parse(id));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
attendenceLogsView = response

View File

@ -128,7 +128,7 @@ class ApiService {
final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}";
final imageObject = {
imageObject = {
"fileName": '$imageName',
"contentType": '$contentType',
"fileSize": fileSize,
@ -147,10 +147,15 @@ class ApiService {
"comment": comment,
"action": action,
"date": DateFormat('yyyy-MM-dd').format(now),
"latitude": '$latitude',
"longitude": '$longitude',
};
// Only include latitude and longitude if imageCapture is true
if (imageCapture) {
body["latitude"] = '$latitude';
body["longitude"] = '$longitude';
}
// Only add imageObject if it's not null
if (imageObject != null) {
body["image"] = imageObject;
}

View File

@ -3,7 +3,7 @@ import 'package:marco/helpers/services/localizations/language.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; // Import the EmployeeInfo model
import 'package:marco/model/employee_info.dart';
import 'dart:convert';
class LocalStorage {
static const String _loggedInUserKey = "user";

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
// Define action texts
class ButtonActions {
static const String checkIn = "Check In";
static const String checkOut = "Check Out";
static const String requestRegularize = " Request Regularize";
static const String rejected = "Rejected";
static const String approved = "Approved";
static const String requested = "Requested";
static const String approve = "Approve";
static const String reject = "Reject";
}
// Map action texts to colors
class AttendanceActionColors {
static const Map<String, Color> colors = {
ButtonActions.checkIn: Colors.green,
ButtonActions.checkOut: Colors.red,
ButtonActions.requestRegularize: Colors.blue,
ButtonActions.rejected: Colors.orange,
ButtonActions.approved: Colors.green,
ButtonActions.requested: Colors.yellow,
ButtonActions.approve: Colors.blueAccent,
ButtonActions.reject: Colors.pink,
};
}

View File

@ -5,6 +5,7 @@ class AttendanceLogModel {
final DateTime? checkOut;
final int activity;
final int id;
final int employeeId;
AttendanceLogModel({
required this.name,
@ -13,6 +14,7 @@ class AttendanceLogModel {
this.checkOut,
required this.activity,
required this.id,
required this.employeeId,
});
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
@ -23,6 +25,7 @@ class AttendanceLogModel {
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
activity: json['activity'] ?? 0,
id: json['id'] != null ? json['id'] : null,
employeeId: json['employeeId'] != null ? json['employeeId'] : null,
);
}
}

View File

@ -2,19 +2,31 @@ import 'package:intl/intl.dart';
class AttendanceLogViewModel {
final DateTime? activityTime;
final String? imageUrl;
final String? description;
final String? comment;
final String? thumbPreSignedUrl;
final String? preSignedUrl;
final String? longitude;
final String? latitude;
AttendanceLogViewModel({
this.activityTime,
this.imageUrl,
this.description,
this.comment,
this.thumbPreSignedUrl,
this.preSignedUrl,
this.longitude,
this.latitude,
});
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
return AttendanceLogViewModel(
activityTime: json['activityTime'] != null ? DateTime.tryParse(json['activityTime']) : null,
imageUrl: json['imageUrl'],
description: json['description'],
comment: json['comment'],
thumbPreSignedUrl: json['thumbPreSignedUrl'],
preSignedUrl: json['preSignedUrl'],
longitude: json['longitude'],
latitude: json['latitude'],
);
}

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_container.dart';
class MyPaginatedTable extends StatefulWidget {
final String? title;
final List<DataColumn> columns;
final List<DataRow> rows;
final double columnSpacing;
final double horizontalMargin;
const MyPaginatedTable({
super.key,
this.title,
required this.columns,
required this.rows,
this.columnSpacing = 23,
this.horizontalMargin = 35,
});
@override
_MyPaginatedTableState createState() => _MyPaginatedTableState();
}
class _MyPaginatedTableState extends State<MyPaginatedTable> {
int _start = 0;
int _rowsPerPage = 10;
@override
Widget build(BuildContext context) {
final visibleRows = widget.rows.skip(_start).take(_rowsPerPage).toList();
final totalRows = widget.rows.length;
final totalPages = (totalRows / _rowsPerPage).ceil();
final currentPage = (_start / _rowsPerPage).ceil() + 1;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (widget.title != null)
Padding(
padding: MySpacing.xy(8, 6), // Using standard spacing for title
child: MyText.titleMedium(widget.title!, fontWeight: 600, fontSize: 20),
),
if (widget.rows.isEmpty)
Padding(
padding: MySpacing.all(16), // Standard padding for empty state
child: MyText.bodySmall('No data available'),
),
if (widget.rows.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final spacing = _calculateSmartSpacing(constraints.maxWidth);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: MyContainer.bordered(
borderColor: Colors.black.withAlpha(40),
padding: EdgeInsets.zero,
child: DataTable(
columns: widget.columns,
rows: visibleRows,
columnSpacing: spacing,
horizontalMargin: widget.horizontalMargin,
),
),
);
},
),
MySpacing.height(8), // Standard height spacing after table
PaginatedFooter(
currentPage: currentPage,
totalPages: totalPages,
onPrevious: () {
setState(() {
_start = (_start - _rowsPerPage).clamp(0, totalRows - _rowsPerPage);
});
},
onNext: () {
setState(() {
_start = (_start + _rowsPerPage).clamp(0, totalRows - _rowsPerPage);
});
},
onPageSizeChanged: (newRowsPerPage) {
setState(() {
_rowsPerPage = newRowsPerPage;
_start = 0;
});
},
),
],
);
}
double _calculateSmartSpacing(double maxWidth) {
int columnCount = widget.columns.length;
double horizontalPadding = widget.horizontalMargin * 2;
double availableWidth = maxWidth - horizontalPadding;
// Desired min/max column spacing
const double minSpacing = 16;
const double maxSpacing = 80;
// Total width assuming minimal spacing
double minTotalWidth = (columnCount * minSpacing) + horizontalPadding;
if (minTotalWidth >= availableWidth) {
// Not enough room return minimal spacing
return minSpacing;
}
// Fit evenly within the available width
double spacing = (availableWidth / columnCount) - 40; // 40 for estimated cell content width
return spacing.clamp(minSpacing, maxSpacing);
}
}
class PaginatedFooter extends StatelessWidget {
final int currentPage;
final int totalPages;
final VoidCallback onPrevious;
final VoidCallback onNext;
final Function(int) onPageSizeChanged;
const PaginatedFooter({
required this.currentPage,
required this.totalPages,
required this.onPrevious,
required this.onNext,
required this.onPageSizeChanged,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: MySpacing.x(16), // Standard horizontal spacing for footer
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (currentPage > 1)
IconButton(
onPressed: onPrevious,
icon: Icon(Icons.chevron_left),
),
Text(
'Page $currentPage of $totalPages',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onBackground,
),
),
SizedBox(width: 8),
if (currentPage < totalPages)
IconButton(
onPressed: onNext,
icon: Icon(Icons.chevron_right),
),
SizedBox(width: 16),
PopupMenuButton<int>(
icon: Icon(Icons.more_vert),
onSelected: (value) {
onPageSizeChanged(value);
},
itemBuilder: (BuildContext context) {
return [5, 10, 20, 50].map((e) {
return PopupMenuItem<int>(
value: e,
child: Text('$e rows per page'),
);
}).toList();
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
class PaginatedTableWidget<T> extends StatefulWidget {
final List<DataColumn> columns;
final List<DataRow> rows;
final int initialRowsPerPage;
final Function(int, int)? onPageChanged;
final double columnSpacing;
const PaginatedTableWidget({
required this.columns,
required this.rows,
this.initialRowsPerPage = 5,
this.onPageChanged,
this.columnSpacing = 16.0, // Default column spacing
});
@override
_PaginatedTableWidgetState createState() => _PaginatedTableWidgetState();
}
class _PaginatedTableWidgetState extends State<PaginatedTableWidget> {
late int rowsPerPage;
@override
void initState() {
super.initState();
rowsPerPage = widget.initialRowsPerPage;
}
@override
Widget build(BuildContext context) {
return PaginatedDataTable(
rowsPerPage: rowsPerPage,
availableRowsPerPage: [5, 10, 20, 50, 100],
onRowsPerPageChanged: (newRowsPerPage) {
setState(() {
rowsPerPage = newRowsPerPage ?? rowsPerPage;
});
},
columns: widget.columns,
source: _DataTableSource(
rows: widget.rows,
),
columnSpacing: widget.columnSpacing,
);
}
}
class _DataTableSource extends DataTableSource {
final List<DataRow> rows;
_DataTableSource({required this.rows});
@override
DataRow? getRow(int index) {
if (index >= rows.length) {
return null;
}
return rows[index];
}
@override
int get rowCount => rows.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => 0;
}

View File

@ -1,4 +1,6 @@
class RegularizationLogModel {
final int id;
final int employeeId;
final String name;
final String role;
final DateTime? checkIn;
@ -6,6 +8,8 @@ class RegularizationLogModel {
final int activity;
RegularizationLogModel({
required this.id,
required this.employeeId,
required this.name,
required this.role,
this.checkIn,
@ -13,12 +17,19 @@ class RegularizationLogModel {
required this.activity,
});
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
return RegularizationLogModel(
id: json['id'] ?? 0,
employeeId: json['employeeId'] ?? 0,
name: "${json['firstName'] ?? ''} ${json['lastName'] ?? ''}".trim(),
role: json['jobRoleName'] ?? '',
checkIn: json['checkInTime'] != null ? DateTime.tryParse(json['checkInTime']) : null,
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
checkIn: json['checkInTime'] != null
? DateTime.tryParse(json['checkInTime'])
: null,
checkOut: json['checkOutTime'] != null
? DateTime.tryParse(json['checkOutTime'])
: null,
activity: json['activity'] ?? 0,
);
}

View File

@ -34,192 +34,196 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
init: controller,
tag: 'login_controller',
builder: (controller) {
return Form(
key: controller.basicValidator.formKey,
child: SingleChildScrollView(
padding: MySpacing.xy(2, 40),
child: Container(
width: double.infinity,
padding: MySpacing.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.02),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: contentTheme.primary.withOpacity(0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Logo
Center(
child: Image.asset(
Images.logoDark,
height: 120,
fit: BoxFit.contain,
),
),
MySpacing.height(20),
/// Welcome Text
Center(
child: MyText.bodyLarge("Welcome Back!", fontWeight: 600),
),
MySpacing.height(4),
Center(
child: MyText.bodySmall("Please sign in to continue."),
),
MySpacing.height(20),
/// Email Field
MyText.bodySmall("Email Address", fontWeight: 600),
MySpacing.height(8),
Material(
elevation: 2,
shadowColor: contentTheme.secondary.withAlpha(30),
borderRadius: BorderRadius.circular(12),
child: TextFormField(
validator:
controller.basicValidator.getValidation('username'),
controller:
controller.basicValidator.getController('username'),
keyboardType: TextInputType.emailAddress,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
hintText: "Enter your email",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: BorderSide.none,
return Obx(() {
return controller.isLoading.value
? Center(child: CircularProgressIndicator()) // Show loading spinner when isLoading is true
: Form(
key: controller.basicValidator.formKey,
child: SingleChildScrollView(
padding: MySpacing.xy(2, 40),
child: Container(
width: double.infinity,
padding: MySpacing.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.02),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: contentTheme.primary.withOpacity(0.5),
),
prefixIcon: const Icon(LucideIcons.mail, size: 18),
contentPadding: MySpacing.xy(12, 16),
),
),
),
MySpacing.height(16),
/// Password Field Label
MyText.bodySmall("Password", fontWeight: 600),
MySpacing.height(8),
Material(
elevation: 2,
shadowColor: contentTheme.secondary.withAlpha(25),
borderRadius: BorderRadius.circular(12),
child: TextFormField(
validator:
controller.basicValidator.getValidation('password'),
controller:
controller.basicValidator.getController('password'),
keyboardType: TextInputType.visiblePassword,
obscureText: !controller.showPassword,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
hintText: "Enter your password",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(LucideIcons.lock, size: 18),
suffixIcon: InkWell(
onTap: controller.onChangeShowPassword,
child: Icon(
controller.showPassword
? LucideIcons.eye
: LucideIcons.eye_off,
size: 18,
),
),
contentPadding: MySpacing.all(3),
),
),
),
MySpacing.height(16),
/// Remember Me + Forgot Password
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () => controller
.onChangeCheckBox(!controller.isChecked),
child: Row(
children: [
Checkbox(
onChanged: controller.onChangeCheckBox,
value: controller.isChecked,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
fillColor: WidgetStatePropertyAll(
contentTheme.secondary),
checkColor: contentTheme.onPrimary,
visualDensity: getCompactDensity,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Logo
Center(
child: Image.asset(
Images.logoDark,
height: 120,
fit: BoxFit.contain,
),
MySpacing.width(8),
MyText.bodySmall("Remember Me"),
],
),
),
MyButton.text(
onPressed: controller.goToForgotPassword,
elevation: 0,
padding: MySpacing.xy(8, 0),
splashColor: contentTheme.secondary.withAlpha(36),
child: MyText.bodySmall(
'Forgot password?',
fontWeight: 600,
color: contentTheme.secondary,
),
),
],
),
MySpacing.height(28),
),
MySpacing.height(20),
/// Login Button
Center(
child: MyButton.rounded(
onPressed: controller.onLogin,
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 16,
backgroundColor: contentTheme.primary,
child: MyText.labelMedium(
'Login',
fontWeight: 600,
color: contentTheme.onPrimary,
/// Welcome Text
Center(
child: MyText.bodyLarge("Welcome Back!", fontWeight: 600),
),
MySpacing.height(4),
Center(
child: MyText.bodySmall("Please sign in to continue."),
),
MySpacing.height(20),
/// Email Field
MyText.bodySmall("Email Address", fontWeight: 600),
MySpacing.height(8),
Material(
elevation: 2,
shadowColor: contentTheme.secondary.withAlpha(30),
borderRadius: BorderRadius.circular(12),
child: TextFormField(
validator:
controller.basicValidator.getValidation('username'),
controller:
controller.basicValidator.getController('username'),
keyboardType: TextInputType.emailAddress,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
hintText: "Enter your email",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(LucideIcons.mail, size: 18),
contentPadding: MySpacing.xy(12, 16),
),
),
),
MySpacing.height(16),
/// Password Field Label
MyText.bodySmall("Password", fontWeight: 600),
MySpacing.height(8),
Material(
elevation: 2,
shadowColor: contentTheme.secondary.withAlpha(25),
borderRadius: BorderRadius.circular(12),
child: TextFormField(
validator:
controller.basicValidator.getValidation('password'),
controller:
controller.basicValidator.getController('password'),
keyboardType: TextInputType.visiblePassword,
obscureText: !controller.showPassword,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
hintText: "Enter your password",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(2),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(LucideIcons.lock, size: 18),
suffixIcon: InkWell(
onTap: controller.onChangeShowPassword,
child: Icon(
controller.showPassword
? LucideIcons.eye
: LucideIcons.eye_off,
size: 18,
),
),
contentPadding: MySpacing.all(3),
),
),
),
MySpacing.height(16),
/// Remember Me + Forgot Password
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () => controller
.onChangeCheckBox(!controller.isChecked),
child: Row(
children: [
Checkbox(
onChanged: controller.onChangeCheckBox,
value: controller.isChecked,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
fillColor: WidgetStatePropertyAll(
contentTheme.secondary),
checkColor: contentTheme.onPrimary,
visualDensity: getCompactDensity,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
MySpacing.width(8),
MyText.bodySmall("Remember Me"),
],
),
),
MyButton.text(
onPressed: controller.goToForgotPassword,
elevation: 0,
padding: MySpacing.xy(8, 0),
splashColor: contentTheme.secondary.withAlpha(36),
child: MyText.bodySmall(
'Forgot password?',
fontWeight: 600,
color: contentTheme.secondary,
),
),
],
),
MySpacing.height(28),
/// Login Button
Center(
child: MyButton.rounded(
onPressed: controller.onLogin,
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 16,
backgroundColor: contentTheme.primary,
child: MyText.labelMedium(
'Login',
fontWeight: 600,
color: contentTheme.onPrimary,
),
),
),
MySpacing.height(16),
/// Register Link
Center(
child: MyButton.text(
onPressed: controller.gotoRegister,
elevation: 0,
padding: MySpacing.xy(12, 8),
splashColor: contentTheme.secondary.withAlpha(30),
child: MyText.bodySmall(
"Request a Demo",
color: contentTheme.secondary,
fontWeight: 600,
),
),
),
],
),
),
),
MySpacing.height(16),
/// Register Link
Center(
child: MyButton.text(
onPressed: controller.gotoRegister,
elevation: 0,
padding: MySpacing.xy(12, 8),
splashColor: contentTheme.secondary.withAlpha(30),
child: MyText.bodySmall(
"Request a Demo",
color: contentTheme.secondary,
fontWeight: 600,
),
),
),
],
),
),
),
);
);
});
},
),
);

File diff suppressed because it is too large Load Diff

View File

@ -47,10 +47,10 @@ class DashboardScreen extends StatelessWidget with UIMixin {
List<Widget> _buildDashboardStats() {
final stats = [
_StatItem(LucideIcons.layout_dashboard, "Dashboard", contentTheme.primary),
_StatItem(LucideIcons.gauge, "Dashboard", contentTheme.primary),
_StatItem(LucideIcons.folder, "Projects", contentTheme.secondary),
_StatItem(LucideIcons.check, "Attendance", contentTheme.success),
_StatItem(LucideIcons.users, "Task", contentTheme.info),
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success),
_StatItem(LucideIcons.logs, "Task", contentTheme.info),
];
return List.generate(