......@@ -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,7 +237,19 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
checkOutDescError,
checkOutProofError
].any((e) => e != null)) {
setState(() {});
setState(() {}); // refresh UI to show error messages
return;
}
// --- Format date for server ---
String formattedDate = "";
try {
final parsedDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
formattedDate = DateFormat("yyyy-MM-dd").format(parsedDate);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error formatting date: $e")),
);
return;
}
......@@ -268,7 +292,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
: selectedType == "Check Out"
? checkOutLocation.text
: "${checkInLocation.text}, ${checkOutLocation.text}",
checkDate: provider.dateController.text,
checkDate: formattedDate,
checkInTime: finalCheckInTime,
checkInLoc: finalCheckInLoc,
checkInProof: finalCheckInProof,
......@@ -278,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();
......@@ -301,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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -168,38 +168,36 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
],
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: InkResponse(
onTap: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const AddBillScreen(pageTitleName: "Add Bill",),
settings: const RouteSettings(
name: 'AddTourExpBillScreen'),
bottomNavigationBar: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
color: Colors.white,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xff1487c9), // App blue
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
).then((_) {
provider.fetchTourExpenses(context, "1");
});
// show add bill screen here
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0, // Optional: remove shadow
),
child: Text(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddBillScreen(pageTitleName: "Add Bill"),
settings: const RouteSettings(name: 'AddTourExpBillScreen'),
),
).then((_) {
provider.fetchTourExpenses(context, "1");
});
},
child: const Text(
"Add Bill",
style: TextStyle(
fontSize: 15,
fontSize: 16,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
......
......@@ -57,6 +57,7 @@ export 'package:generp/Notifiers/crmProvider/followUpUpdateProvider.dart';
export 'package:generp/Notifiers/crmProvider/appointmentCalendarProvider.dart';
export 'package:generp/Notifiers/crmProvider/addNewLeadsandProspectsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/hrmAccessiblePagesProvider.dart';
export 'package:generp/Notifiers/hrmProvider/attendanceListProvider.dart';
export 'package:generp/Notifiers/hrmProvider/AttendanceDetailsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/tourExpensesProvider.dart';
......
......@@ -4995,11 +4995,13 @@ class ApiCalling {
type,
from,
to,
mode
) async {
try {
Map<String, String> data = {
'emp_id': (empId).toString(),
'session_id': (session).toString(),
'mode': (mode),
'type': (type),
'from': (from),
'to': (to),
......@@ -5019,6 +5021,7 @@ class ApiCalling {
}
}
static Future<attendanceRequestDetailsResponse?> attendanceRequestDetailAPI(
empId,
session,
......@@ -5045,6 +5048,38 @@ class ApiCalling {
}
}
static Future<CommonResponse?> attendanceRequestApproveRejectAPI(
session,
empId,
mode,
type,
remarks,
id,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'mode': (mode).toString(),
'type': (type).toString(),
'remarks': (remarks).toString(),
'id': (id).toString(),
};
final res = await post(data, AttendanceRequestRejectUrl, {});
if (res != null) {
print("Attendance App Reje:${data}");
debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
static Future<CommonResponse?> addAttendanceRequestAPI({
required String sessionId,
required String empId,
......@@ -5317,12 +5352,14 @@ class ApiCalling {
// Leave Application api
// Leave Application api
static Future<leaveApplicationLIstResponse?> leaveApplicationListAPI(
session,
empId,
dateFrom,
dateTo
dateTo,
mode
) async {
try {
Map<String, String> data = {
......@@ -5330,6 +5367,8 @@ class ApiCalling {
'emp_id': (empId).toString(),
'requested_date_from': (dateFrom),
'requested_date_to': (dateTo),
'mode': (mode),
};
final res = await post(data, LeaveApplicationListUrl, {});
if (res != null) {
......@@ -5408,6 +5447,38 @@ class ApiCalling {
}
}
static Future<CommonResponse?> leaveRequestRejectApproveAPI(
session,
empId,
mode,
type,
remarks,
id,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'mode': (mode).toString(),
'type': (type).toString(),
'remarks': (remarks).toString(),
'id': (id).toString(),
};
final res = await post(data, LeaveRequestRejectAprroveUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
// static Future<CommonResponse?> TpcIssueListApprovalAPI(
// empId,
......
......@@ -180,13 +180,16 @@ const crmDashboardQuotationsUrl = "${baseUrl_test}crm_dashboard_quotations_list"
const ogcharturl = "${baseUrl_test}organisation_structures";
const JobDesciptionUrl ="${baseUrl_test}job_description";
///HRM
//Attendance
const HrmAccessiblePagesUrl ="${baseUrl_test}hrm_accessible_pages";
const AttendanceRequestListUrl ="${baseUrl_test}attendance_request_list";
const AttendanceRequestDetailsUrl ="${baseUrl_test}attendance_request_details";
const AddAttendanceRequestUrl ="${baseUrl_test}add_attendance_request";
const AttendanceRequestRejectUrl ="${baseUrl_test}attendance_approve_reject";
const AttendanceRequestAproveUrl ="${baseUrl_test}attendance_approve_reject";
// reward list
const RewardListUrl ="${baseUrl_test}hrm_emp_self_rewards";
// Tour Expenses hrm_emp_self_rewards
......@@ -198,6 +201,7 @@ const AddTourExpensesUrl ="${baseUrl_test}add_tour_bill";
const LeaveApplicationListUrl ="${baseUrl_test}leave_request_list";
const LeaveApplicationDetailsUrl ="${baseUrl_test}leave_request_details";
const LeaveRequestAdditionUrl ="${baseUrl_test}add_leave_request";
const LeaveRequestRejectAprroveUrl ="${baseUrl_test}leaves_approve_reject";
......