Commit 39774c76 authored by Sai Srinivas's avatar Sai Srinivas
Browse files

08-09-2025 Mohit Kumar

HRM Module Test Cases and UI
parent 1b1bfdfb
......@@ -42,6 +42,7 @@ class RequestList {
String? checkOutTime;
String? status;
String? requestedDatetime;
String? employeeName;
RequestList(
{this.id,
......@@ -53,7 +54,9 @@ class RequestList {
this.chechOutType,
this.checkOutTime,
this.status,
this.requestedDatetime});
this.requestedDatetime,
this.employeeName,
});
RequestList.fromJson(Map<String, dynamic> json) {
id = json['id'];
......@@ -66,6 +69,7 @@ class RequestList {
checkOutTime = json['check_out_time'];
status = json['status'];
requestedDatetime = json['requested_datetime'];
employeeName = json['employee_name'];
}
Map<String, dynamic> toJson() {
......@@ -80,6 +84,7 @@ class RequestList {
data['check_out_time'] = this.checkOutTime;
data['status'] = this.status;
data['requested_datetime'] = this.requestedDatetime;
data['employee_name'] = this.employeeName;
return data;
}
}
......@@ -38,6 +38,8 @@ class RequestList {
String? toPeriod;
String? status;
String? leaveType;
String? rowColor;
String? employeeName;
RequestList(
......@@ -50,6 +52,8 @@ class RequestList {
toPeriod = json['to_period'];
status = json['status'];
leaveType = json["leave_type"];
rowColor = json["row_colur"];
employeeName = json["employee_name"];
}
Map<String, dynamic> toJson() {
......@@ -60,6 +64,8 @@ class RequestList {
data['to_period'] = this.toPeriod;
data['status'] = this.status;
data["leave_type"] = this.leaveType;
data["row_colur"] = this.rowColor;
data["employee_name"] = this.employeeName;
return data;
}
}
......@@ -15,7 +15,7 @@ class LeaveApplicationDetailsProvider extends ChangeNotifier {
bool get isSubmitting => _isSubmitting;
CommonResponse? _StatusResponse;
CommonResponse? get addResponse => _StatusResponse;
CommonResponse? get Response => _StatusResponse;
leaveApplicationDetailsResponse? get response => _response;
bool get isLoading => _isLoading;
......
......@@ -46,7 +46,7 @@ class AttendanceDetailsProvider extends ChangeNotifier {
_isLoading = false;
notifyListeners();
}
Future<void> rejectAttendanceRequest(
Future<void> rejectApproveAttendanceRequest(
BuildContext context, {
required String mode,
required String type,
......@@ -60,8 +60,9 @@ class AttendanceDetailsProvider extends ChangeNotifier {
try {
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
print("############################+++++++++++++++++##########");
final result = await ApiCalling.attendanceRequestRejectAPI(
final result = await ApiCalling.attendanceRequestApproveRejectAPI(
homeProvider.session,
homeProvider.empId,
mode,
......
......@@ -123,6 +123,8 @@ class Attendancelistprovider extends ChangeNotifier {
return null; // everything ok
}
CommonResponse? _RejectResponse;
CommonResponse? get RejectResponse => _RejectResponse;
/// Fetch attendance request list with filters
......@@ -231,6 +233,49 @@ class Attendancelistprovider extends ChangeNotifier {
notifyListeners();
}
Future<void> rejectApproveAttendanceRequest({
required String session,
required String empId,
required String mode,
required String type,
required String remarks,
required String id,
}) async {
_isSubmitting = true;
_errorMessage = null;
_RejectResponse = null;
notifyListeners();
try {
final result = await ApiCalling.attendanceRequestApproveRejectAPI(
session,
empId,
mode,
type,
remarks,
id,
);
print("*********************************object");
if (result != null) {
_RejectResponse = result;
if (result.error != null && result.error!.isNotEmpty) {
_errorMessage = result.error;
} else {
debugPrint("Attendance request $type successfully.");
}
} else {
_errorMessage = "Failed to process attendance request!";
}
} catch (e) {
_errorMessage = "Error processing attendance request: $e";
}
_isSubmitting = false;
notifyListeners();
}
/// Apply filters coming from bottom sheet
void updateFiltersFromSheet(
mode,
......
......@@ -254,9 +254,9 @@ class LeaveApplicationListProvider extends ChangeNotifier {
}
/// Show Cupertino DatePicker for leave form
void showDatePickerDialog(BuildContext context,
{bool isFromDate = true}) {
DateTime? currentDate = DateTime.now();
void showDatePickerDialog(BuildContext context, {bool isFromDate = true}) {
DateTime now = DateTime.now();
DateTime? currentDate = now;
showCupertinoModalPopup<void>(
context: context,
......@@ -287,10 +287,10 @@ class LeaveApplicationListProvider extends ChangeNotifier {
onPressed: () {
if (isFromDate) {
fromDateField.text =
_formatDate(currentDate ?? DateTime.now());
_formatDate(currentDate ?? now);
} else {
toDateField.text =
_formatDate(currentDate ?? DateTime.now());
_formatDate(currentDate ?? now);
}
Navigator.pop(context);
},
......@@ -301,7 +301,9 @@ class LeaveApplicationListProvider extends ChangeNotifier {
Expanded(
child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy,
initialDateTime: currentDate,
initialDateTime: now,
minimumDate: DateTime(now.year, now.month, now.day),
maximumDate: DateTime(now.year + 5), // limit
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) {
currentDate = newDate;
......
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import '../Utils/app_colors.dart';
import '../Utils/dropdownTheme.dart';
......@@ -351,27 +352,30 @@ class CommonFilter2 {
],
),
),
if (tempSelectedValue == 'Custom') ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: buildCalendar(setState),
),
if (tempSelectedDateRange != null)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Text(
'Selected: ${formatDate(tempSelectedDateRange!.start)} to ${formatDate(tempSelectedDateRange!.end)}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
),
],
const SizedBox(height: 20),
if (tempSelectedValue == 'Custom') ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: buildCalendar(setState),
),
if (tempSelectedDateRange != null)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Text(
'Selected: ${DateFormat("dd MMM yyyy").format(tempSelectedDateRange!.start)} to ${DateFormat("dd MMM yyyy").format(tempSelectedDateRange!.end)}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
),
],
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
......
......@@ -1863,21 +1863,43 @@ class _MyHomePageState extends State<MyHomePage> {
profile.employeeeID,
profile.mobileNUmber,
];
final itemText = textHeadings[index]?.toString() ?? "-";
return SizedBox(
height: 40,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"${textHeadings[index].toString()}",
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14,
color: AppColors.semi_black,
child: InkWell(
onTap: () {
showJobDescriptionSheet(context, [
"Statewise End to end sales activities reg booking and dispatches and payment collection and branch visit every month & quarterly basis.",
"Conducting monthly/Quarterly/Annually– sales meeting, review of targets and achievements of total team.",
"Team CRM Tracking, Order Update Track and as well as payment entry in CRM by Team.",
"If required special Price to be taken from Prasad, Madhavi Madam/MD Sir.",
"Preparation of MIS reports on monthly basis (Rating wise data, employee wise data, TIV etc.).",
"Dispatch co-ordination with factory team- Anuradha / Sai Ram (commercial clearance with Susmitha / Rajeevi).",
"Commercial / Technical Support to BDE team order finalisation. If required client visit.",
"Team tour bills approvals in CRM.",
"Level -1 approvals to be given to sales team orders.",
"Outstanding payment collection followed on regular basis.",
]);
},
// no click for others
child: Text(
itemText,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14,
color: index == 2 ? AppColors.semi_black : AppColors.semi_black, // highlight clickable
decoration: index == 2 ? TextDecoration.underline : null,
),
),
),
),
);
},
),
),
],
......@@ -1943,6 +1965,116 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
Future<void> showJobDescriptionSheet(
BuildContext context,
List<String> jobPoints,
) {
return showModalBottomSheet(
useSafeArea: true,
isDismissible: true,
isScrollControlled: true,
showDragHandle: true,
enableDrag: true,
backgroundColor: Colors.white,
context: context,
builder: (context) {
return SafeArea(
child: Container(
margin: const EdgeInsets.only(
bottom: 15,
left: 15,
right: 15,
top: 30,
),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
/// Heading
Text(
"Job Description",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 16,
color: AppColors.app_blue, // same as Logout "Yes, Logout" button
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 15),
/// Bullet points list
...jobPoints.map(
(point) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"• ",
style: TextStyle(
fontSize: 14,
color: AppColors.semi_black,
),
),
Expanded(
child: Text(
point,
style: TextStyle(
fontSize: 14,
color: AppColors.semi_black,
fontFamily: "JakartaRegular",
height: 1.4, // line spacing
),
),
),
],
),
),
),
const SizedBox(height: 20),
/// Close button
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
alignment: Alignment.center,
height: 45,
margin: const EdgeInsets.symmetric(
horizontal: 5.0,
vertical: 5.0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
),
child: Center(
child: Text(
"Close",
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
fontSize: 15,
),
),
),
),
),
],
),
),
),
);
},
);
}
Future<void> _showLogoutBottomSheet(BuildContext context) {
return showModalBottomSheet(
useSafeArea: true,
......
......@@ -125,187 +125,190 @@ class _AddLeaveRequestState extends State<AddLeaveRequest> {
}
Widget _scaffold(BuildContext context, LeaveApplicationListProvider provider) {
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: AppColors.scaffold_bg_color,
appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF),
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// From Date
TextWidget(context, "From Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: true);
if (fromDateError != null) {
setState(() => fromDateError = null);
}
},
child: textFieldNew(context, provider.fromDateField,
"Select Date", enabled: false),
),
errorWidget(context, fromDateError),
/// From Time
TextWidget(context, "From Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.fromTimeField.text = picked.format(context);
if (fromTimeError != null) {
setState(() => fromTimeError = null);
return SafeArea(
top: false,
child: Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: AppColors.scaffold_bg_color,
appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF),
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// From Date
TextWidget(context, "From Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: true);
if (fromDateError != null) {
setState(() => fromDateError = null);
}
}
},
child: textFieldNew(context, provider.fromTimeField,
"Select Time", enabled: false),
),
errorWidget(context, fromTimeError),
},
child: textFieldNew(context, provider.fromDateField,
"Select Date", enabled: false),
),
errorWidget(context, fromDateError),
/// To Date
TextWidget(context, "To Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: false);
if (toDateError != null) {
setState(() => toDateError = null);
}
},
child: textFieldNew(context, provider.toDateField, "Select Date",
enabled: false),
),
errorWidget(context, toDateError),
/// From Time
TextWidget(context, "From Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.fromTimeField.text = picked.format(context);
if (fromTimeError != null) {
setState(() => fromTimeError = null);
}
}
},
child: textFieldNew(context, provider.fromTimeField,
"Select Time", enabled: false),
),
errorWidget(context, fromTimeError),
/// To Time
TextWidget(context, "To Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.toTimeField.text = picked.format(context);
if (toTimeError != null) {
setState(() => toTimeError = null);
/// To Date
TextWidget(context, "To Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: false);
if (toDateError != null) {
setState(() => toDateError = null);
}
}
},
child: textFieldNew(context, provider.toTimeField, "Select Time",
enabled: false),
),
errorWidget(context, toTimeError),
},
child: textFieldNew(context, provider.toDateField, "Select Date",
enabled: false),
),
errorWidget(context, toDateError),
/// Leave Type
TextWidget(context, "Leave Type"),
Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
/// To Time
TextWidget(context, "To Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.toTimeField.text = picked.format(context);
if (toTimeError != null) {
setState(() => toTimeError = null);
}
}
},
child: textFieldNew(context, provider.toTimeField, "Select Time",
enabled: false),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint: const Text("Select Leave Type",
style: TextStyle(fontSize: 14, color: Colors.grey)),
value: leaveType,
items: leaveTypes
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) {
setState(() {
leaveType = val;
if (leaveTypeError != null) {
leaveTypeError = null;
}
});
},
errorWidget(context, toTimeError),
/// Leave Type
TextWidget(context, "Leave Type"),
Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint: const Text("Select Leave Type",
style: TextStyle(fontSize: 14, color: Colors.grey)),
value: leaveType,
items: leaveTypes
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) {
setState(() {
leaveType = val;
if (leaveTypeError != null) {
leaveTypeError = null;
}
});
},
),
),
),
),
errorWidget(context, leaveTypeError),
errorWidget(context, leaveTypeError),
/// Reason
TextWidget(context, "Reason"),
textFieldNew(context, provider.reasonController, "Enter Reason",
maxLines: 2),
errorWidget(context, reasonError),
/// Reason
TextWidget(context, "Reason"),
textFieldNew(context, provider.reasonController, "Enter Reason",
maxLines: 2),
errorWidget(context, reasonError),
const SizedBox(height: 70),
],
const SizedBox(height: 70),
],
),
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: InkResponse(
onTap: () {
if (validateForm(provider)) {
provider.addLeaveRequest(
context,
fromDate: provider.fromDateField.text,
fromTime: provider.fromTimeField.text,
toDate: provider.toDateField.text,
toTime: provider.toTimeField.text,
leaveType: leaveType!,
reason: provider.reasonController.text,
);
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: InkResponse(
onTap: () {
if (validateForm(provider)) {
provider.addLeaveRequest(
context,
fromDate: provider.fromDateField.text,
fromTime: provider.fromTimeField.text,
toDate: provider.toDateField.text,
toTime: provider.toTimeField.text,
leaveType: leaveType!,
reason: provider.reasonController.text,
);
// Reset after submit
setState(() {
provider.fromDateField.clear();
provider.fromTimeField.clear();
provider.toDateField.clear();
provider.toTimeField.clear();
provider.reasonController.clear();
leaveType = null;
fromDateError = null;
fromTimeError = null;
toDateError = null;
toTimeError = null;
leaveTypeError = null;
reasonError = null;
});
// Reset after submit
setState(() {
provider.fromDateField.clear();
provider.fromTimeField.clear();
provider.toDateField.clear();
provider.toTimeField.clear();
provider.reasonController.clear();
leaveType = null;
fromDateError = null;
fromTimeError = null;
toDateError = null;
toTimeError = null;
leaveTypeError = null;
reasonError = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Leave request submitted successfully!"),
backgroundColor: Colors.black87,
),
);
}
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: provider.isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: const Text(
"Submit",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white),
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Leave request submitted successfully!"),
backgroundColor: Colors.black87,
),
);
}
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: provider.isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: const Text(
"Submit",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white),
),
),
),
),
......
......@@ -55,12 +55,36 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
}
Future<void> _autoFetchLocation() async {
String loc = await getCurrentLocation();
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
// Save raw coordinates separately (for submission)
final coords = "${position.latitude},${position.longitude}";
// Convert to address for display
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
String displayAddress;
if (placemarks.isNotEmpty) {
final place = placemarks.first;
displayAddress =
"${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.country}";
} else {
displayAddress = coords; // fallback
}
setState(() {
locationController.text = loc;
locationController.text = displayAddress; // what user sees
_rawCoordinates = coords; // keep coords hidden for backend
});
}
// Add this field at the top of your State class:
String? _rawCoordinates;
Future<String> getCurrentLocation() async {
try {
LocationPermission permission = await Geolocator.checkPermission();
......@@ -164,7 +188,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
context,
process: "Live",
type: selectedType ?? "",
loc: locationController.text,
loc: _rawCoordinates ?? "", // send actual coordinates
checkDate: DateTime.now().toString().split(" ").first,
checkInTime:
selectedType == "Check In" ? TimeOfDay.now().format(context) : null,
......@@ -198,210 +222,213 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
automaticallyImplyLeading: false,
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
const SizedBox(width: 10),
Text(
"Add Live Attendance",
style: TextStyle(
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Type Dropdown
const Text("Type",
const SizedBox(width: 10),
Text(
"Add Live Attendance",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400),
),
value: selectedType,
items: types
.map((e) => DropdownMenuItem<String>(
value: e,
child: Text(
e,
style: const TextStyle(
fontSize: 14, fontFamily: "JakartaMedium"),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Type Dropdown
const Text("Type",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400),
),
))
.toList(),
onChanged: (val) => setState(() => selectedType = val),
iconStyleData: ddtheme.iconStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
value: selectedType,
items: types
.map((e) => DropdownMenuItem<String>(
value: e,
child: Text(
e,
style: const TextStyle(
fontSize: 14, fontFamily: "JakartaMedium"),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (val) => setState(() => selectedType = val),
iconStyleData: ddtheme.iconStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
),
),
if (typeError != null) ...[
const SizedBox(height: 4),
Text(typeError!,
if (typeError != null) ...[
const SizedBox(height: 4),
Text(typeError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16),
/// Location
Text(locationHeading,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: locationController,
decoration: _inputDecoration("Enter location"),
),
if (locationError != null) ...[
const SizedBox(height: 4),
Text(locationError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16),
/// Location
Text(locationHeading,
style: const TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: locationController,
decoration: _inputDecoration("Enter location"),
),
if (locationError != null) ...[
const SizedBox(height: 4),
Text(locationError!,
const SizedBox(height: 16),
/// Description
Text(descriptionHeading,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: _inputDecoration("Write Description"),
),
const SizedBox(height: 16),
/// Description
Text(descriptionHeading,
style: const TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: _inputDecoration("Write Description"),
),
const SizedBox(height: 20),
/// Attach Proof
InkResponse(
onTap: () => _showPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(14),
const SizedBox(height: 20),
/// Attach Proof
InkResponse(
onTap: () => _showPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
proofButtonText,
style: const TextStyle(
fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
),
),
),
),
child: Center(
child: Text(
proofButtonText,
),
if (proofError != null) ...[
const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
if (proofFile != null) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text("Attached: ${proofFile!.name}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => setState(() => proofFile = null),
),
],
)
],
const SizedBox(height: 24),
/// Submit Button
InkResponse(
onTap:
isSubmitEnabled && !isSubmitting ? () => submitAttendance(context) : null,
child: Container(
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSubmitEnabled
? AppColors.app_blue
: Colors.grey.shade400,
borderRadius: BorderRadius.circular(12),
),
child: isSubmitting
? const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)
: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
),
if (proofError != null) ...[
const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
if (proofFile != null) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text("Attached: ${proofFile!.name}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => setState(() => proofFile = null),
),
],
)
],
const SizedBox(height: 24),
/// Submit Button
InkResponse(
onTap:
isSubmitEnabled && !isSubmitting ? () => submitAttendance(context) : null,
child: Container(
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSubmitEnabled
? AppColors.app_blue
: Colors.grey.shade400,
borderRadius: BorderRadius.circular(12),
),
child: isSubmitting
? const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)
: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
),
),
],
),
),
),
);
......
......@@ -168,7 +168,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
}
void _submitForm(BuildContext context) async {
// reset errors first
// Reset errors first
dateError = null;
typeError = null;
checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null;
......@@ -176,15 +176,27 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// --- Date Validation ---
// --- Date Validation (allow today, yesterday, day before yesterday) ---
if (provider.dateController.text.isEmpty) {
dateError = "Please select a date";
} else {
try {
final enteredDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
provider.setSelectedDate(enteredDate);
if (!provider.isDateValid()) {
dateError = "Date must be today or yesterday";
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
final dayBeforeYesterday = today.subtract(const Duration(days: 2));
// Normalize dates (ignore time part)
bool isValid = enteredDate.year == today.year &&
enteredDate.month == today.month &&
(enteredDate.day == today.day ||
enteredDate.day == yesterday.day ||
enteredDate.day == dayBeforeYesterday.day);
if (!isValid) {
dateError = "Date must be today, yesterday, or the day before yesterday";
}
} catch (e) {
dateError = "Invalid date format (use dd MMM yyyy)";
......@@ -225,15 +237,15 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
checkOutDescError,
checkOutProofError
].any((e) => e != null)) {
setState(() {});
setState(() {}); // refresh UI to show error messages
return;
}
// --- Format date for server (convert from "03 Sep 2025" to "2025-09-03" or whatever format server expects) ---
// --- Format date for server ---
String formattedDate = "";
try {
final parsedDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
formattedDate = DateFormat("yyyy-MM-dd").format(parsedDate); // Change format as per server requirement
formattedDate = DateFormat("yyyy-MM-dd").format(parsedDate);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error formatting date: $e")),
......@@ -280,7 +292,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
: selectedType == "Check Out"
? checkOutLocation.text
: "${checkInLocation.text}, ${checkOutLocation.text}",
checkDate: formattedDate, // Use the formatted date here
checkDate: formattedDate,
checkInTime: finalCheckInTime,
checkInLoc: finalCheckInLoc,
checkInProof: finalCheckInProof,
......@@ -290,14 +302,13 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
note: finalNote,
);
// Check the response from provider
// --- Response handling ---
if (provider.addResponse != null && provider.addResponse!.error == "0") {
// Success case
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(provider.addResponse!.message ?? "Attendance Submitted Successfully")),
);
// --- Reset fields ---
// Reset fields
setState(() {
selectedType = null;
provider.dateController.clear();
......@@ -313,19 +324,20 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
_fetchInitialLocation();
} else {
// Error case - show appropriate message
String errorMessage = provider.errorMessage ?? "Failed to submit attendance";
// Handle specific server error for Check Out without Check In
if (errorMessage.contains("Check In is not Available")) {
errorMessage = "Cannot submit Check Out without a Check In record for this date";
}
if (errorMessage.contains("2")){
errorMessage = "Only One manual Request can be added in a month !";
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
);
}
}
// it's date picker need to take day before yesterday, yesterday and today
......
This diff is collapsed.
......@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:generp/screens/hrm/Attendancelist.dart';
import 'package:provider/provider.dart';
import '../../Utils/app_colors.dart';
import 'AttendanceRequestDetail.dart';
import 'LeaveApplicationScreen.dart';
......@@ -39,186 +38,189 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFCEEDFF),
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
return SafeArea(
top: false,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFCEEDFF),
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
const SizedBox(width: 10),
Text(
"HRM",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
const SizedBox(width: 10),
Text(
"HRM",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
),
],
],
),
),
),
backgroundColor: const Color(0xffF6F6F8),
body: SingleChildScrollView(
child: Column(
children: [
/// Background
Stack(
children: [
Container(
width: double.infinity,
height: 490,
color: const Color(0xffF6F6F8),
),
Container(
width: double.infinity,
height: 490,
padding: const EdgeInsets.only(top: 1, bottom: 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFFCEEDFF),
Color(0xFFf9f9fb),
Color(0xffF6F6F8),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
backgroundColor: const Color(0xffF6F6F8),
body: SingleChildScrollView(
child: Column(
children: [
/// Background
Stack(
children: [
Container(
width: double.infinity,
height: 490,
color: const Color(0xffF6F6F8),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 1, bottom: 30),
child: Image.asset(
"assets/images/vector.png",
height: 230,
Container(
width: double.infinity,
fit: BoxFit.fitWidth,
height: 490,
padding: const EdgeInsets.only(top: 1, bottom: 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFFCEEDFF),
Color(0xFFf9f9fb),
Color(0xffF6F6F8),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
/// Content
Column(
children: [
/// Top Illustration & Button
Container(
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 1, bottom: 30),
child: Image.asset(
"assets/images/vector.png",
height: 230,
width: double.infinity,
padding: const EdgeInsets.only(top: 60, bottom: 30),
child: Column(
children: [
SvgPicture.asset(
"assets/images/capa.svg",
height: 146,
width: 400,
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFF1487C9), // border color
width: 1.2, // thickness of the border
fit: BoxFit.fitWidth,
),
),
/// Content
Column(
children: [
/// Top Illustration & Button
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 60, bottom: 30),
child: Column(
children: [
SvgPicture.asset(
"assets/images/capa.svg",
height: 146,
width: 400,
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFF1487C9), // border color
width: 1.2, // thickness of the border
),
color: const Color(0xffEDF8FF),
borderRadius: BorderRadius.circular(30),
),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OrgChartt(),
),
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
"assets/svg/hrm/groupIc.svg",
height: 29,
width: 29,
fit: BoxFit.contain,
),
const SizedBox(width: 7),
const Text(
"Organization Structure",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, fontFamily: "Plus Jakarta Sans"),
),
const Icon(Icons.chevron_right, color: Colors.black54),
],
),
),
color: const Color(0xffEDF8FF),
borderRadius: BorderRadius.circular(30),
),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OrgChartt(),
],
),
),
/// Grid Section
LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.all(14),
child: Consumer<HrmAccessiblePagesProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(
child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Text(provider.errorMessage!));
}
final pages = (provider.response?.pagesAccessible ?? [])
.where((page) =>
allowedPages.contains(page.pageName))
.toList();
return GridView.builder(
itemCount: pages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (constraints.maxWidth / 180).floor().clamp(2, 4),
crossAxisSpacing: 8.5,
mainAxisSpacing: 16,
childAspectRatio: 1.7,
),
itemBuilder: (context, index) {
final page = pages[index];
return _buildTile(
label: page.pageName ?? "",
subtitle: _getSubtitle(page.pageName ?? ""),
assetIcon: _getIcon(page.pageName ?? ""),
txtColor: const Color(0xff1487C9),
onTap: () => _handleNavigation(
context,
page.pageName ?? "",
page.mode ?? "",
),
);
},
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
"assets/svg/hrm/groupIc.svg",
height: 29,
width: 29,
fit: BoxFit.contain,
),
const SizedBox(width: 7),
const Text(
"Organization Structure",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, fontFamily: "Plus Jakarta Sans"),
),
const Icon(Icons.chevron_right, color: Colors.black54),
],
),
),
),
],
);
},
),
),
/// Grid Section
LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.all(14),
child: Consumer<HrmAccessiblePagesProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(
child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Text(provider.errorMessage!));
}
final pages = (provider.response?.pagesAccessible ?? [])
.where((page) =>
allowedPages.contains(page.pageName))
.toList();
return GridView.builder(
itemCount: pages.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (constraints.maxWidth / 180).floor().clamp(2, 4),
crossAxisSpacing: 8.5,
mainAxisSpacing: 16,
childAspectRatio: 1.7,
),
itemBuilder: (context, index) {
final page = pages[index];
return _buildTile(
label: page.pageName ?? "",
subtitle: _getSubtitle(page.pageName ?? ""),
assetIcon: _getIcon(page.pageName ?? ""),
txtColor: const Color(0xff1487C9),
onTap: () => _handleNavigation(
context,
page.pageName ?? "",
page.mode ?? "",
),
);
},
);
},
),
);
},
),
],
),
],
),
],
],
),
],
),
],
),
),
),
);
......@@ -287,8 +289,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
Expanded(
flex: 1,
child: Container(
height: constraints.maxHeight * 0.5, // Responsive size
width: constraints.maxHeight * 0.5, // Responsive size
height: constraints.maxHeight * 0.39,
width: constraints.maxHeight * 0.39,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF),
......@@ -296,8 +298,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
child: Center(
child: SvgPicture.asset(
assetIcon,
height: constraints.maxHeight * 0.3, // Responsive size
width: constraints.maxHeight * 0.3, // Responsive size
height: constraints.maxHeight * 0.19,
width: constraints.maxHeight * 0.19,
),
),
),
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment