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 { ...@@ -42,6 +42,7 @@ class RequestList {
String? checkOutTime; String? checkOutTime;
String? status; String? status;
String? requestedDatetime; String? requestedDatetime;
String? employeeName;
RequestList( RequestList(
{this.id, {this.id,
...@@ -53,7 +54,9 @@ class RequestList { ...@@ -53,7 +54,9 @@ class RequestList {
this.chechOutType, this.chechOutType,
this.checkOutTime, this.checkOutTime,
this.status, this.status,
this.requestedDatetime}); this.requestedDatetime,
this.employeeName,
});
RequestList.fromJson(Map<String, dynamic> json) { RequestList.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
...@@ -66,6 +69,7 @@ class RequestList { ...@@ -66,6 +69,7 @@ class RequestList {
checkOutTime = json['check_out_time']; checkOutTime = json['check_out_time'];
status = json['status']; status = json['status'];
requestedDatetime = json['requested_datetime']; requestedDatetime = json['requested_datetime'];
employeeName = json['employee_name'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
...@@ -80,6 +84,7 @@ class RequestList { ...@@ -80,6 +84,7 @@ class RequestList {
data['check_out_time'] = this.checkOutTime; data['check_out_time'] = this.checkOutTime;
data['status'] = this.status; data['status'] = this.status;
data['requested_datetime'] = this.requestedDatetime; data['requested_datetime'] = this.requestedDatetime;
data['employee_name'] = this.employeeName;
return data; return data;
} }
} }
...@@ -38,6 +38,8 @@ class RequestList { ...@@ -38,6 +38,8 @@ class RequestList {
String? toPeriod; String? toPeriod;
String? status; String? status;
String? leaveType; String? leaveType;
String? rowColor;
String? employeeName;
RequestList( RequestList(
...@@ -50,6 +52,8 @@ class RequestList { ...@@ -50,6 +52,8 @@ class RequestList {
toPeriod = json['to_period']; toPeriod = json['to_period'];
status = json['status']; status = json['status'];
leaveType = json["leave_type"]; leaveType = json["leave_type"];
rowColor = json["row_colur"];
employeeName = json["employee_name"];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
...@@ -60,6 +64,8 @@ class RequestList { ...@@ -60,6 +64,8 @@ class RequestList {
data['to_period'] = this.toPeriod; data['to_period'] = this.toPeriod;
data['status'] = this.status; data['status'] = this.status;
data["leave_type"] = this.leaveType; data["leave_type"] = this.leaveType;
data["row_colur"] = this.rowColor;
data["employee_name"] = this.employeeName;
return data; return data;
} }
} }
...@@ -15,7 +15,7 @@ class LeaveApplicationDetailsProvider extends ChangeNotifier { ...@@ -15,7 +15,7 @@ class LeaveApplicationDetailsProvider extends ChangeNotifier {
bool get isSubmitting => _isSubmitting; bool get isSubmitting => _isSubmitting;
CommonResponse? _StatusResponse; CommonResponse? _StatusResponse;
CommonResponse? get addResponse => _StatusResponse; CommonResponse? get Response => _StatusResponse;
leaveApplicationDetailsResponse? get response => _response; leaveApplicationDetailsResponse? get response => _response;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
......
...@@ -46,7 +46,7 @@ class AttendanceDetailsProvider extends ChangeNotifier { ...@@ -46,7 +46,7 @@ class AttendanceDetailsProvider extends ChangeNotifier {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
Future<void> rejectAttendanceRequest( Future<void> rejectApproveAttendanceRequest(
BuildContext context, { BuildContext context, {
required String mode, required String mode,
required String type, required String type,
...@@ -60,8 +60,9 @@ class AttendanceDetailsProvider extends ChangeNotifier { ...@@ -60,8 +60,9 @@ class AttendanceDetailsProvider extends ChangeNotifier {
try { try {
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false); final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
print("############################+++++++++++++++++##########");
final result = await ApiCalling.attendanceRequestRejectAPI( final result = await ApiCalling.attendanceRequestApproveRejectAPI(
homeProvider.session, homeProvider.session,
homeProvider.empId, homeProvider.empId,
mode, mode,
......
...@@ -123,6 +123,8 @@ class Attendancelistprovider extends ChangeNotifier { ...@@ -123,6 +123,8 @@ class Attendancelistprovider extends ChangeNotifier {
return null; // everything ok return null; // everything ok
} }
CommonResponse? _RejectResponse;
CommonResponse? get RejectResponse => _RejectResponse;
/// Fetch attendance request list with filters /// Fetch attendance request list with filters
...@@ -231,6 +233,49 @@ class Attendancelistprovider extends ChangeNotifier { ...@@ -231,6 +233,49 @@ class Attendancelistprovider extends ChangeNotifier {
notifyListeners(); 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 /// Apply filters coming from bottom sheet
void updateFiltersFromSheet( void updateFiltersFromSheet(
mode, mode,
......
...@@ -254,9 +254,9 @@ class LeaveApplicationListProvider extends ChangeNotifier { ...@@ -254,9 +254,9 @@ class LeaveApplicationListProvider extends ChangeNotifier {
} }
/// Show Cupertino DatePicker for leave form /// Show Cupertino DatePicker for leave form
void showDatePickerDialog(BuildContext context, void showDatePickerDialog(BuildContext context, {bool isFromDate = true}) {
{bool isFromDate = true}) { DateTime now = DateTime.now();
DateTime? currentDate = DateTime.now(); DateTime? currentDate = now;
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
...@@ -287,10 +287,10 @@ class LeaveApplicationListProvider extends ChangeNotifier { ...@@ -287,10 +287,10 @@ class LeaveApplicationListProvider extends ChangeNotifier {
onPressed: () { onPressed: () {
if (isFromDate) { if (isFromDate) {
fromDateField.text = fromDateField.text =
_formatDate(currentDate ?? DateTime.now()); _formatDate(currentDate ?? now);
} else { } else {
toDateField.text = toDateField.text =
_formatDate(currentDate ?? DateTime.now()); _formatDate(currentDate ?? now);
} }
Navigator.pop(context); Navigator.pop(context);
}, },
...@@ -301,7 +301,9 @@ class LeaveApplicationListProvider extends ChangeNotifier { ...@@ -301,7 +301,9 @@ class LeaveApplicationListProvider extends ChangeNotifier {
Expanded( Expanded(
child: CupertinoDatePicker( child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy, dateOrder: DatePickerDateOrder.dmy,
initialDateTime: currentDate, initialDateTime: now,
minimumDate: DateTime(now.year, now.month, now.day),
maximumDate: DateTime(now.year + 5), // limit
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
currentDate = newDate; currentDate = newDate;
......
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import '../Utils/app_colors.dart'; import '../Utils/app_colors.dart';
import '../Utils/dropdownTheme.dart'; import '../Utils/dropdownTheme.dart';
...@@ -351,27 +352,30 @@ class CommonFilter2 { ...@@ -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( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
......
...@@ -1863,21 +1863,43 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -1863,21 +1863,43 @@ class _MyHomePageState extends State<MyHomePage> {
profile.employeeeID, profile.employeeeID,
profile.mobileNUmber, profile.mobileNUmber,
]; ];
final itemText = textHeadings[index]?.toString() ?? "-";
return SizedBox( return SizedBox(
height: 40, height: 40,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: InkWell(
"${textHeadings[index].toString()}", onTap: () {
textAlign: TextAlign.left, showJobDescriptionSheet(context, [
style: TextStyle( "Statewise End to end sales activities reg booking and dispatches and payment collection and branch visit every month & quarterly basis.",
fontSize: 14, "Conducting monthly/Quarterly/Annually– sales meeting, review of targets and achievements of total team.",
color: AppColors.semi_black, "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> { ...@@ -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) { Future<void> _showLogoutBottomSheet(BuildContext context) {
return showModalBottomSheet( return showModalBottomSheet(
useSafeArea: true, useSafeArea: true,
......
...@@ -125,187 +125,190 @@ class _AddLeaveRequestState extends State<AddLeaveRequest> { ...@@ -125,187 +125,190 @@ class _AddLeaveRequestState extends State<AddLeaveRequest> {
} }
Widget _scaffold(BuildContext context, LeaveApplicationListProvider provider) { Widget _scaffold(BuildContext context, LeaveApplicationListProvider provider) {
return Scaffold( return SafeArea(
resizeToAvoidBottomInset: true, top: false,
backgroundColor: AppColors.scaffold_bg_color, child: Scaffold(
appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF), resizeToAvoidBottomInset: true,
body: SingleChildScrollView( backgroundColor: AppColors.scaffold_bg_color,
child: Container( appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), body: SingleChildScrollView(
margin: const EdgeInsets.all(12), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
color: AppColors.white, margin: const EdgeInsets.all(12),
borderRadius: BorderRadius.circular(20), decoration: BoxDecoration(
), color: AppColors.white,
child: Column( borderRadius: BorderRadius.circular(20),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Column(
/// From Date crossAxisAlignment: CrossAxisAlignment.start,
TextWidget(context, "From Date"), children: [
GestureDetector( /// From Date
onTap: () { TextWidget(context, "From Date"),
provider.showDatePickerDialog(context, isFromDate: true); GestureDetector(
if (fromDateError != null) { onTap: () {
setState(() => fromDateError = null); 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);
} }
} },
}, child: textFieldNew(context, provider.fromDateField,
child: textFieldNew(context, provider.fromTimeField, "Select Date", enabled: false),
"Select Time", enabled: false), ),
), errorWidget(context, fromDateError),
errorWidget(context, fromTimeError),
/// To Date /// From Time
TextWidget(context, "To Date"), TextWidget(context, "From Time"),
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
provider.showDatePickerDialog(context, isFromDate: false); TimeOfDay? picked = await showTimePicker(
if (toDateError != null) { context: context,
setState(() => toDateError = null); initialTime: TimeOfDay.now(),
} );
}, if (picked != null) {
child: textFieldNew(context, provider.toDateField, "Select Date", provider.fromTimeField.text = picked.format(context);
enabled: false), if (fromTimeError != null) {
), setState(() => fromTimeError = null);
errorWidget(context, toDateError), }
}
},
child: textFieldNew(context, provider.fromTimeField,
"Select Time", enabled: false),
),
errorWidget(context, fromTimeError),
/// To Time /// To Date
TextWidget(context, "To Time"), TextWidget(context, "To Date"),
GestureDetector( GestureDetector(
onTap: () async { onTap: () {
TimeOfDay? picked = await showTimePicker( provider.showDatePickerDialog(context, isFromDate: false);
context: context, if (toDateError != null) {
initialTime: TimeOfDay.now(), setState(() => toDateError = null);
);
if (picked != null) {
provider.toTimeField.text = picked.format(context);
if (toTimeError != null) {
setState(() => toTimeError = null);
} }
} },
}, child: textFieldNew(context, provider.toDateField, "Select Date",
child: textFieldNew(context, provider.toTimeField, "Select Time", enabled: false),
enabled: false), ),
), errorWidget(context, toDateError),
errorWidget(context, toTimeError),
/// Leave Type /// To Time
TextWidget(context, "Leave Type"), TextWidget(context, "To Time"),
Container( GestureDetector(
margin: const EdgeInsets.only(bottom: 6), onTap: () async {
padding: const EdgeInsets.symmetric(horizontal: 12), TimeOfDay? picked = await showTimePicker(
decoration: BoxDecoration( context: context,
color: AppColors.text_field_color, initialTime: TimeOfDay.now(),
borderRadius: BorderRadius.circular(14), );
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( errorWidget(context, toTimeError),
child: DropdownButton<String>(
isExpanded: true, /// Leave Type
hint: const Text("Select Leave Type", TextWidget(context, "Leave Type"),
style: TextStyle(fontSize: 14, color: Colors.grey)), Container(
value: leaveType, margin: const EdgeInsets.only(bottom: 6),
items: leaveTypes padding: const EdgeInsets.symmetric(horizontal: 12),
.map((e) => decoration: BoxDecoration(
DropdownMenuItem(value: e, child: Text(e))) color: AppColors.text_field_color,
.toList(), borderRadius: BorderRadius.circular(14),
onChanged: (val) { ),
setState(() { child: DropdownButtonHideUnderline(
leaveType = val; child: DropdownButton<String>(
if (leaveTypeError != null) { isExpanded: true,
leaveTypeError = null; 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 /// Reason
TextWidget(context, "Reason"), TextWidget(context, "Reason"),
textFieldNew(context, provider.reasonController, "Enter Reason", textFieldNew(context, provider.reasonController, "Enter Reason",
maxLines: 2), maxLines: 2),
errorWidget(context, reasonError), errorWidget(context, reasonError),
const SizedBox(height: 70), const SizedBox(height: 70),
], ],
),
), ),
), ),
), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, bottomNavigationBar: InkResponse(
bottomNavigationBar: InkResponse( onTap: () {
onTap: () { if (validateForm(provider)) {
if (validateForm(provider)) { provider.addLeaveRequest(
provider.addLeaveRequest( context,
context, fromDate: provider.fromDateField.text,
fromDate: provider.fromDateField.text, fromTime: provider.fromTimeField.text,
fromTime: provider.fromTimeField.text, toDate: provider.toDateField.text,
toDate: provider.toDateField.text, toTime: provider.toTimeField.text,
toTime: provider.toTimeField.text, leaveType: leaveType!,
leaveType: leaveType!, reason: provider.reasonController.text,
reason: provider.reasonController.text, );
);
// Reset after submit // Reset after submit
setState(() { setState(() {
provider.fromDateField.clear(); provider.fromDateField.clear();
provider.fromTimeField.clear(); provider.fromTimeField.clear();
provider.toDateField.clear(); provider.toDateField.clear();
provider.toTimeField.clear(); provider.toTimeField.clear();
provider.reasonController.clear(); provider.reasonController.clear();
leaveType = null; leaveType = null;
fromDateError = null; fromDateError = null;
fromTimeError = null; fromTimeError = null;
toDateError = null; toDateError = null;
toTimeError = null; toTimeError = null;
leaveTypeError = null; leaveTypeError = null;
reasonError = null; reasonError = null;
}); });
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Leave request submitted successfully!"), content: Text("Leave request submitted successfully!"),
backgroundColor: Colors.black87, backgroundColor: Colors.black87,
), ),
); );
} }
}, },
child: Container( child: Container(
height: 45, height: 45,
alignment: Alignment.center, alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.app_blue, color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
child: provider.isSubmitting child: provider.isSubmitting
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(color: Colors.white)
: const Text( : const Text(
"Submit", "Submit",
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: "JakartaMedium", fontFamily: "JakartaMedium",
color: Colors.white), color: Colors.white),
),
), ),
), ),
), ),
......
...@@ -55,12 +55,36 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -55,12 +55,36 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
} }
Future<void> _autoFetchLocation() async { 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(() { 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 { Future<String> getCurrentLocation() async {
try { try {
LocationPermission permission = await Geolocator.checkPermission(); LocationPermission permission = await Geolocator.checkPermission();
...@@ -164,7 +188,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -164,7 +188,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
context, context,
process: "Live", process: "Live",
type: selectedType ?? "", type: selectedType ?? "",
loc: locationController.text, loc: _rawCoordinates ?? "", // send actual coordinates
checkDate: DateTime.now().toString().split(" ").first, checkDate: DateTime.now().toString().split(" ").first,
checkInTime: checkInTime:
selectedType == "Check In" ? TimeOfDay.now().format(context) : null, selectedType == "Check In" ? TimeOfDay.now().format(context) : null,
...@@ -198,210 +222,213 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -198,210 +222,213 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
backgroundColor: Colors.white, top: false,
appBar: AppBar( child: Scaffold(
automaticallyImplyLeading: false,
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
children: [ backgroundColor: Colors.white,
InkResponse( elevation: 0,
onTap: () => Navigator.pop(context, true), title: Row(
child: SvgPicture.asset( children: [
"assets/svg/appbar_back_button.svg", InkResponse(
height: 25, onTap: () => Navigator.pop(context, true),
), child: SvgPicture.asset(
), "assets/svg/appbar_back_button.svg",
const SizedBox(width: 10), height: 25,
Text( ),
"Add Live Attendance",
style: TextStyle(
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
), ),
), const SizedBox(width: 10),
], Text(
), "Add Live Attendance",
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Type Dropdown
const Text("Type",
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 18,
fontFamily: "JakartaMedium", fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)), fontWeight: FontWeight.w600,
const SizedBox(height: 6), color: AppColors.semi_black,
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( body: SingleChildScrollView(
"Select Type", padding: const EdgeInsets.all(18),
overflow: TextOverflow.ellipsis, child: Column(
style: TextStyle( crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 15, children: [
fontFamily: "JakartaMedium", /// Type Dropdown
fontWeight: FontWeight.w400), const Text("Type",
), style: TextStyle(
value: selectedType, fontSize: 15,
items: types fontFamily: "JakartaMedium",
.map((e) => DropdownMenuItem<String>( fontWeight: FontWeight.w500)),
value: e, const SizedBox(height: 6),
child: Text( Container(
e, padding: const EdgeInsets.symmetric(horizontal: 2),
style: const TextStyle( decoration: BoxDecoration(
fontSize: 14, fontFamily: "JakartaMedium"), color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400),
), ),
)) value: selectedType,
.toList(), items: types
onChanged: (val) => setState(() => selectedType = val), .map((e) => DropdownMenuItem<String>(
iconStyleData: ddtheme.iconStyleData, value: e,
dropdownStyleData: ddtheme.dropdownStyleData, 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) ...[
if (typeError != null) ...[ const SizedBox(height: 4),
const SizedBox(height: 4), Text(typeError!,
Text(typeError!, style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16),
/// Location
Text(locationHeading,
style: const TextStyle( style: const TextStyle(
color: Colors.red, fontSize: 15,
fontSize: 13, fontFamily: "JakartaMedium",
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), const SizedBox(height: 16),
/// Location /// Description
Text(locationHeading, Text(descriptionHeading,
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!,
style: const TextStyle( style: const TextStyle(
color: Colors.red, fontSize: 15,
fontSize: 13, fontFamily: "JakartaMedium",
fontFamily: "JakartaMedium")), fontWeight: FontWeight.w500)),
], const SizedBox(height: 6),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: _inputDecoration("Write Description"),
),
const SizedBox(height: 16), const SizedBox(height: 20),
/// Description /// Attach Proof
Text(descriptionHeading, InkResponse(
style: const TextStyle( onTap: () => _showPicker(context),
fontSize: 15, child: Container(
fontFamily: "JakartaMedium", width: double.infinity,
fontWeight: FontWeight.w500)), padding: const EdgeInsets.symmetric(vertical: 14),
const SizedBox(height: 6), decoration: BoxDecoration(
TextField( color: Colors.blue.shade50,
controller: descriptionController, border: Border.all(color: Colors.blue.shade200),
maxLines: 3, borderRadius: BorderRadius.circular(14),
decoration: _inputDecoration("Write Description"), ),
), child: Center(
child: Text(
const SizedBox(height: 20), proofButtonText,
style: const TextStyle(
/// Attach Proof fontSize: 16,
InkResponse( color: Colors.blue,
onTap: () => _showPicker(context), fontFamily: "JakartaMedium",
child: Container( fontWeight: FontWeight.w500,
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( if (proofError != null) ...[
proofButtonText, const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle( 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, fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium", 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> { ...@@ -168,7 +168,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
} }
void _submitForm(BuildContext context) async { void _submitForm(BuildContext context) async {
// reset errors first // Reset errors first
dateError = null; dateError = null;
typeError = null; typeError = null;
checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null; checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null;
...@@ -176,15 +176,27 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -176,15 +176,27 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final provider = Provider.of<Attendancelistprovider>(context, listen: false); final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// --- Date Validation --- // --- Date Validation (allow today, yesterday, day before yesterday) ---
if (provider.dateController.text.isEmpty) { if (provider.dateController.text.isEmpty) {
dateError = "Please select a date"; dateError = "Please select a date";
} else { } else {
try { try {
final enteredDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text); final enteredDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
provider.setSelectedDate(enteredDate); 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) { } catch (e) {
dateError = "Invalid date format (use dd MMM yyyy)"; dateError = "Invalid date format (use dd MMM yyyy)";
...@@ -225,15 +237,15 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -225,15 +237,15 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
checkOutDescError, checkOutDescError,
checkOutProofError checkOutProofError
].any((e) => e != null)) { ].any((e) => e != null)) {
setState(() {}); setState(() {}); // refresh UI to show error messages
return; 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 = ""; String formattedDate = "";
try { try {
final parsedDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text); 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) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error formatting date: $e")), SnackBar(content: Text("Error formatting date: $e")),
...@@ -280,7 +292,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -280,7 +292,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
: selectedType == "Check Out" : selectedType == "Check Out"
? checkOutLocation.text ? checkOutLocation.text
: "${checkInLocation.text}, ${checkOutLocation.text}", : "${checkInLocation.text}, ${checkOutLocation.text}",
checkDate: formattedDate, // Use the formatted date here checkDate: formattedDate,
checkInTime: finalCheckInTime, checkInTime: finalCheckInTime,
checkInLoc: finalCheckInLoc, checkInLoc: finalCheckInLoc,
checkInProof: finalCheckInProof, checkInProof: finalCheckInProof,
...@@ -290,14 +302,13 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -290,14 +302,13 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
note: finalNote, note: finalNote,
); );
// Check the response from provider // --- Response handling ---
if (provider.addResponse != null && provider.addResponse!.error == "0") { if (provider.addResponse != null && provider.addResponse!.error == "0") {
// Success case
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(provider.addResponse!.message ?? "Attendance Submitted Successfully")), SnackBar(content: Text(provider.addResponse!.message ?? "Attendance Submitted Successfully")),
); );
// --- Reset fields --- // Reset fields
setState(() { setState(() {
selectedType = null; selectedType = null;
provider.dateController.clear(); provider.dateController.clear();
...@@ -313,19 +324,20 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -313,19 +324,20 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
_fetchInitialLocation(); _fetchInitialLocation();
} else { } else {
// Error case - show appropriate message
String errorMessage = provider.errorMessage ?? "Failed to submit attendance"; 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")) { if (errorMessage.contains("Check In is not Available")) {
errorMessage = "Cannot submit Check Out without a Check In record for this date"; 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( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red), SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
); );
} }
} }
// it's date picker need to take day before yesterday, yesterday and today
......
...@@ -23,311 +23,340 @@ class AttendanceRequestDetailScreen extends StatefulWidget { ...@@ -23,311 +23,340 @@ class AttendanceRequestDetailScreen extends StatefulWidget {
class _AttendanceRequestDetailScreenState class _AttendanceRequestDetailScreenState
extends State<AttendanceRequestDetailScreen> { extends State<AttendanceRequestDetailScreen> {
bool _actionSubmitted = false;
late AttendanceDetailsProvider provider; late AttendanceDetailsProvider provider;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return SafeArea(
create: (_) => top: false,
AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId), child: ChangeNotifierProvider(
child: Consumer<AttendanceDetailsProvider>( create: (_) =>
builder: (context, provider, child) { AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId),
// Get screen dimensions for responsive scaling child: Consumer<AttendanceDetailsProvider>(
final screenWidth = MediaQuery.of(context).size.width; builder: (context, provider, child) {
final screenHeight = MediaQuery.of(context).size.height; // Get screen dimensions for responsive scaling
final screenWidth = MediaQuery.of(context).size.width;
// Scale factors based on screen size final screenHeight = MediaQuery.of(context).size.height;
final scaleFactor = screenWidth / 360; // Base width for scaling
final textScaleFactor = MediaQuery.of(context).textScaleFactor.clamp(1.0, 1.2); // Scale factors based on screen size
final scaleFactor = screenWidth / 360; // Base width for scaling
return Scaffold( final textScaleFactor = MediaQuery.of(context).textScaleFactor.clamp(1.0, 1.2);
appBar: AppBar(
automaticallyImplyLeading: false, return Scaffold(
backgroundColor: Color(0xFFFFFFFF), appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
mainAxisAlignment: MainAxisAlignment.start, backgroundColor: Color(0xFFFFFFFF),
crossAxisAlignment: CrossAxisAlignment.center, title: Row(
children: [ mainAxisAlignment: MainAxisAlignment.start,
InkResponse( crossAxisAlignment: CrossAxisAlignment.center,
onTap: () => Navigator.pop(context, true), children: [
child: SvgPicture.asset( InkResponse(
"assets/svg/appbar_back_button.svg", onTap: () => Navigator.pop(context, true),
height: 25 * scaleFactor, child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25 * scaleFactor,
),
), ),
), SizedBox(width: 10 * scaleFactor),
SizedBox(width: 10 * scaleFactor), InkResponse(
InkResponse( onTap: () => Navigator.pop(context, true),
onTap: () => Navigator.pop(context, true), child: Text(
child: Text( "Attendance Details",
"Attendance Details", style: TextStyle(
style: TextStyle( fontSize: 18,
fontSize: 18, height: 1.1,
height: 1.1, fontFamily: "Plus Jakarta Sans",
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), ),
), ],
], ),
), ),
), backgroundColor: Color(0xFFF6F6F8),
backgroundColor: Color(0xFFF6F6F8), body: Builder(
body: Builder( builder: (context) {
builder: (context) { if (provider.isLoading) {
if (provider.isLoading) { return const Center(child: CircularProgressIndicator(color: Colors.blue,));
return const Center(child: CircularProgressIndicator(color: Colors.blue,)); }
} // if (provider.errorMessage != null) {
if (provider.errorMessage != null) { // return Center(child: Text(provider.errorMessage!));
return Center(child: Text(provider.errorMessage!)); // }
} if (provider.response?.requestDetails == null) {
if (provider.response?.requestDetails == null) { return const Center(child: Text("No details found"));
return const Center(child: Text("No details found")); }
}
final details = provider.response!.requestDetails!;
final details = provider.response!.requestDetails!;
/// scr
/// scr return SingleChildScrollView(
return SingleChildScrollView( padding: EdgeInsets.all(16.0 * scaleFactor),
padding: EdgeInsets.all(16.0 * scaleFactor), child: Column(
child: Column( children: [
children: [ Card(
Card( shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16 * scaleFactor),
borderRadius: BorderRadius.circular(16 * scaleFactor), ),
), elevation: 0,
elevation: 2, child: Padding(
child: Padding( padding: EdgeInsets.all(16.0 * scaleFactor),
padding: EdgeInsets.all(16.0 * scaleFactor), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Container(
Container( margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
margin: EdgeInsets.only(bottom: 0.5 * scaleFactor), padding: EdgeInsets.symmetric(horizontal: 2.5 * scaleFactor, vertical: 12 * scaleFactor),
padding: EdgeInsets.all(12 * scaleFactor), decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.white,
color: Colors.white, borderRadius: BorderRadius.circular(12 * scaleFactor),
borderRadius: BorderRadius.circular(12 * scaleFactor), ),
), child: Row(
child: Row( children: [
children: [ /// Left Avatar
/// Left Avatar Container(
Container( height: 44 * scaleFactor,
height: 48 * scaleFactor, width: 44 * scaleFactor,
width: 48 * scaleFactor, decoration: BoxDecoration(
decoration: BoxDecoration( shape: BoxShape.circle,
shape: BoxShape.circle, color: const Color(0xFFEDF8FF), // icon bg
color: const Color(0xFFEDF8FF), // icon bg ),
), child: Center(
child: Center( child: SvgPicture.asset(
child: SvgPicture.asset( height: 24 * scaleFactor,
height: 28 * scaleFactor, width: 24 * scaleFactor,
width: 28 * scaleFactor, "assets/svg/hrm/attendanceList.svg",
"assets/svg/hrm/attendanceList.svg", fit: BoxFit.contain,
fit: BoxFit.contain, ),
), ),
), ),
), SizedBox(width: 12 * scaleFactor),
SizedBox(width: 12 * scaleFactor),
/// Middle text
/// Middle text Expanded(
Expanded( child: Column(
child: Column( crossAxisAlignment:
crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start, children: [
children: [ Text(
Text( details.type ?? "-",
details.type ?? "-", style: TextStyle(
style: TextStyle( decoration: TextDecoration.underline,
decoration: TextDecoration.underline, decorationStyle:
decorationStyle: TextDecorationStyle.dotted,
TextDecorationStyle.dotted, decorationColor: AppColors.grey_thick,
decorationColor: AppColors.grey_thick, height: 1.2,
height: 1.2, fontFamily: "JakartaRegular",
fontFamily: "JakartaRegular", fontSize: 14,
fontSize: 14, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), SizedBox(height: 2 * scaleFactor),
SizedBox(height: 2 * scaleFactor), Text(
Text( details.date ?? "-",
details.date ?? "-", style: TextStyle(
style: TextStyle( fontFamily: "JakartaRegular",
fontFamily: "JakartaRegular", fontSize: 14,
fontSize: 14, color: AppColors.app_blue,
color: AppColors.app_blue, ),
), ),
), ],
], ),
), ),
),
/// Right side (Live/Manual) /// Right side (Live/Manual)
Container( Container(
height: 30 * scaleFactor, height: 30 * scaleFactor,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 12 * scaleFactor, horizontal: 12 * scaleFactor,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6 * scaleFactor), borderRadius: BorderRadius.circular(6 * scaleFactor),
color: getDecorationColor(details.status) color: getDecorationColor(details.status)
), ),
child: Center( child: Center(
child: Text( child: Text(
details.status ?? "-", details.status ?? "-",
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: getTextColor(details.status.toString()), color: getTextColor(details.status.toString()),
),
), ),
), ),
), ),
), ],
], ),
), ),
),
// Employee Details // Employee Details
_buildSectionHeader("Employee Details", scaleFactor), _buildSectionHeader("Employee Details", scaleFactor),
_buildDetailTile("Employee Name", details.employeeName, scaleFactor), _buildDetailTile("Employee Name", details.employeeName, scaleFactor),
_buildDetailTile("Created Employee", details.createdEmpName, scaleFactor), _buildDetailTile("Created Employee", details.createdEmpName, scaleFactor),
// Check In/Out // Check In/Out
_buildSectionHeader("Check In/Out Details", scaleFactor), _buildSectionHeader("Check In/Out Details", scaleFactor),
_buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime, scaleFactor), _buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime, scaleFactor),
_buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime, scaleFactor), _buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime, scaleFactor),
_buildDetailTile("Original Check In", details.checkInTime, scaleFactor), _buildDetailTile("Original Check In", details.checkInTime, scaleFactor),
_buildDetailTile("Original Check Out", "--", scaleFactor), _buildDetailTile("Original Check Out", "--", scaleFactor),
_buildDetailTile("Original Check In Location", details.checkInLocation, scaleFactor), _buildDetailTile("Original Check In Location", details.checkInLocation, scaleFactor),
_buildDetailTile("Original Check Out Location", details.checkOutLocation, scaleFactor), _buildDetailTile("Original Check Out Location", details.checkOutLocation, scaleFactor),
buildLocationTile("Location", details.location, scaleFactor), buildLocationTile("Location", details.location, scaleFactor),
// Proofs // Proofs
if ((details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) || if ((details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) ||
(details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)) ...[ (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)) ...[
_buildSectionHeader("Proofs", scaleFactor), _buildSectionHeader("Proofs", scaleFactor),
if (details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) if (details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty)
_buildProofLink(context, "Check In Proof", details.checkInProofDirFilePath, scaleFactor), _buildProofLink(context, "Check In Proof", details.checkInProofDirFilePath, scaleFactor),
if (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty) if (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)
_buildProofLink(context, "Check Out Proof", details.checkOutProofDirFilePath, scaleFactor), _buildProofLink(context, "Check Out Proof", details.checkOutProofDirFilePath, scaleFactor),
],
// Remarks & Approvals
_buildSectionHeader("Remarks & Approvals", scaleFactor),
_buildDetailTile("Level 1 Approved By", details.level1EmpName, scaleFactor),
_buildDetailTile("Level 2 Approved By", details.level2EmpName, scaleFactor),
_buildDetailTile("Level 1 Remark", details.level1Remarks, scaleFactor),
_buildDetailTile("Level 2 Remark", details.level2Remarks, scaleFactor),
///remain data
_buildSectionHeader("Other Details", scaleFactor),
_buildDetailTile("Check In Type", details.checkInType, scaleFactor),
_buildDetailTile("Check Out Type", details.chechOutType, scaleFactor),
_buildDetailTile("Check Out Time", details.checkOutTime, scaleFactor),
// Attendance Info
_buildDetailTile("ID", details.id, scaleFactor),
_buildDetailTile("Attendance Type", details.attendanceType, scaleFactor),
_buildDetailTile("Note", details.note, scaleFactor),
_buildDetailTile("Created Datetime", details.requestedDatetime, scaleFactor),
], ],
),
// Remarks & Approvals
_buildSectionHeader("Remarks & Approvals", scaleFactor),
_buildDetailTile("Level 1 Approved By", details.level1EmpName, scaleFactor),
_buildDetailTile("Level 2 Approved By", details.level2EmpName, scaleFactor),
_buildDetailTile("Level 1 Remark", details.level1Remarks, scaleFactor),
_buildDetailTile("Level 2 Remark", details.level2Remarks, scaleFactor),
///remain data
_buildSectionHeader("Other Details", scaleFactor),
_buildDetailTile("Check In Type", details.checkInType, scaleFactor),
_buildDetailTile("Check Out Type", details.chechOutType, scaleFactor),
_buildDetailTile("Check Out Time", details.checkOutTime, scaleFactor),
// Attendance Info
_buildDetailTile("ID", details.id, scaleFactor),
_buildDetailTile("Attendance Type", details.attendanceType, scaleFactor),
_buildDetailTile("Note", details.note, scaleFactor),
_buildDetailTile("Created Datetime", details.requestedDatetime, scaleFactor),
],
), ),
), ),
), SizedBox(height: 30 * scaleFactor),
SizedBox(height: 30 * scaleFactor), ],
),
);
},
),
bottomNavigationBar: (widget.mode == "apr_lvl1"
&& !_actionSubmitted
&& provider.response?.requestDetails?.status != "Level 1 Approved"
&& provider.response?.requestDetails?.status != "Rejected")
? Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffFFFFFF),
Color(0x00FFFFFF),
], ],
), ),
); ),
}, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
), height: 61,
bottomNavigationBar: widget.mode == "apr_lvl1" child: Column(
? Container( children: [
decoration: const BoxDecoration(color: Colors.white), Row(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), children: [
height: 80, /// Reject Button
child: Row( Expanded(
children: [ child: InkWell(
/// Reject Button onTap: () {
Expanded( showRemarkSheet(
child: InkWell( context: context,
onTap: () { actionType: "Reject",
showRemarkSheet( onSubmit: (remark) {
context: context, provider.rejectApproveAttendanceRequest(
actionType: "Reject", context,
onSubmit: (remark) { mode: widget.mode,
provider.rejectAttendanceRequest( type: "Rejected",
context, remarks: remark,
mode: widget.mode, id: provider.response!.requestDetails!.id!,
type: "Rejected", );
remarks: remark, },
id: provider.response!.requestDetails!.id!, ).then((_) {
); provider.fetchAttendanceRequestDetail(context, widget.attendanceListId); // or setState(() {}) if needed
}, });
); },
}, child: Container(
child: Container( alignment: Alignment.center,
alignment: Alignment.center, decoration: BoxDecoration(
height: 45, borderRadius: BorderRadius.circular(8),
decoration: BoxDecoration( ),
borderRadius: BorderRadius.circular(8), child: Row(
color: const Color(0xFFFFFFFF), mainAxisAlignment: MainAxisAlignment.center,
), children: [
child: Row( SvgPicture.asset("assets/svg/finance/level_reject_ic.svg"),
mainAxisAlignment: MainAxisAlignment.center, const SizedBox(width: 6),
children: [ const Text("Reject"),
SvgPicture.asset("assets/svg/finance/level_reject_ic.svg"), ],
const SizedBox(width: 6), ),
const Text("Reject"), ),
], ),
), ),
),
), /// Vertical Divider
), Container(
const SizedBox(width: 10), width: 1,
height: 45,
/// Approve Button color: Colors.grey.shade300,
Expanded(
child: InkWell(
onTap: () {
showRemarkSheet(
context: context,
actionType: "Approve",
onSubmit: (remark) {
provider.rejectAttendanceRequest(
context,
mode: widget.mode,
type: "Approved",
remarks: remark,
id: provider.response!.requestDetails!.id!,
);
},
);
},
child: Container(
alignment: Alignment.center,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: const Color(0xFFFFFFFF),
), ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, /// Approve Button
children: [ Expanded(
SvgPicture.asset("assets/svg/finance/level_approve_ic.svg"), child: InkWell(
const SizedBox(width: 6), onTap: () {
const Text("Approve"), showRemarkSheet(
], context: context,
actionType: "Approve",
onSubmit: (remark) async {
await provider.rejectApproveAttendanceRequest(
context,
mode: widget.mode,
type: "Approved",
remarks: remark,
id: provider.response!.requestDetails!.id!,
);
},
).then((_) {
provider.fetchAttendanceRequestDetail(context, widget.attendanceListId); // or setState(() {}) if needed
});
},
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/finance/level_approve_ic.svg"),
const SizedBox(width: 6),
const Text("Approve"),
],
),
),
),
), ),
), ],
), ),
), SizedBox(height: 0,)
], ],
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
); );
}, },
) )
),
); );
} }
...@@ -411,24 +440,28 @@ class _AttendanceRequestDetailScreenState ...@@ -411,24 +440,28 @@ class _AttendanceRequestDetailScreenState
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( Container(
controller: remarkController, margin: const EdgeInsets.only(bottom: 6),
maxLines: 3, decoration: BoxDecoration(
onChanged: (val) { color: AppColors.text_field_color,
if (remarkError != null && val.isNotEmpty) { borderRadius: BorderRadius.circular(14),
updateState(() => remarkError = null); ),
} child: TextField(
}, controller: remarkController,
decoration: InputDecoration( maxLines: 3,
hintText: "Enter your remark here...", onChanged: (val) {
filled: true, if (remarkError != null && val.isNotEmpty) {
fillColor: Colors.grey.shade100, updateState(() => remarkError = null);
border: OutlineInputBorder( }
borderRadius: BorderRadius.circular(12), },
), decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric( hintText: "Enter your remark here...",
vertical: 12, hintStyle: TextStyle(
horizontal: 12, color: Colors.grey.shade500, // Customize this color
fontSize: 14, // Optional: tweak font size
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
), ),
), ),
), ),
...@@ -444,7 +477,7 @@ class _AttendanceRequestDetailScreenState ...@@ -444,7 +477,7 @@ class _AttendanceRequestDetailScreenState
child: Container( child: Container(
height: 45, height: 45,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.shade100, color: Color(0x12AAAAAA),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Center( child: const Center(
...@@ -478,7 +511,6 @@ class _AttendanceRequestDetailScreenState ...@@ -478,7 +511,6 @@ class _AttendanceRequestDetailScreenState
); );
} }
}, },
child: Container( child: Container(
height: 45, height: 45,
decoration: BoxDecoration( decoration: BoxDecoration(
...@@ -553,20 +585,23 @@ class _AttendanceRequestDetailScreenState ...@@ -553,20 +585,23 @@ class _AttendanceRequestDetailScreenState
/// for location /// for location
/// Location Tile
Widget buildLocationTile(String label, String? value, double scaleFactor) { Widget buildLocationTile(String label, String? value, double scaleFactor) {
return FutureBuilder<String>( return FutureBuilder<String>(
future: getReadableLocation(value), future: getReadableLocation(value),
builder: (context, snapshot) { builder: (context, snapshot) {
final locationText = snapshot.data ?? "-"; final locationText = snapshot.connectionState == ConnectionState.done
? (snapshot.data ?? value ?? "-")
: value ?? "-";
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor), padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // aligns top when wrapping crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Label // Label
Expanded( Expanded(
flex: 5, // ratio (adjust same as your Date/Time tile) flex: 5,
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
...@@ -579,14 +614,13 @@ class _AttendanceRequestDetailScreenState ...@@ -579,14 +614,13 @@ class _AttendanceRequestDetailScreenState
// Value (Clickable Location) // Value (Clickable Location)
Expanded( Expanded(
flex: 5, // take remaining space flex: 5,
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
final uri = Uri.parse( final uri = Uri.parse(
"https://www.google.com/maps/search/?api=1&query=$value"); "https://www.google.com/maps/search/?api=1&query=$value");
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri, await launchUrl(uri, mode: LaunchMode.externalApplication);
mode: LaunchMode.externalApplication);
} }
}, },
child: Text( child: Text(
...@@ -609,20 +643,40 @@ class _AttendanceRequestDetailScreenState ...@@ -609,20 +643,40 @@ class _AttendanceRequestDetailScreenState
); );
} }
/// Convert coordinates -> human readable location
/// Convert coordinates -> full human readable location
Future<String> getReadableLocation(String? value) async { Future<String> getReadableLocation(String? value) async {
if (value == null) return "-"; if (value == null || value.isEmpty) return "-";
try { try {
List<Location> locations = await locationFromAddress(value); // Expecting "lat,lng"
List<Placemark> placemarks = await placemarkFromCoordinates( final parts = value.split(',');
locations[0].latitude, if (parts.length != 2) return value;
locations[0].longitude,
); final lat = double.tryParse(parts[0].trim());
return placemarks.first.locality ?? value; final lng = double.tryParse(parts[1].trim());
if (lat == null || lng == null) return value;
final placemarks = await placemarkFromCoordinates(lat, lng);
final place = placemarks.first;
// Include more details
final address = [
place.name,
place.street, // e.g. "A-46, Lata Enclave"
place.subLocality, // e.g. "Madhura Nagar"
place.locality, // e.g. "Hyderabad"
place.administrativeArea, // e.g. "Telangana"
place.postalCode, // e.g. "500038"
place.country // e.g. "India"
].where((e) => e != null && e.isNotEmpty).join(", ");
return address;
} catch (e) { } catch (e) {
return value; // fallback to raw coordinates return value; // fallback to raw coordinates
} }
} }
/// for date and time /// for date and time
Widget _buildDate_TimeTile(String label, String? date, String? time, double scaleFactor) { Widget _buildDate_TimeTile(String label, String? date, String? time, double scaleFactor) {
return Padding( return Padding(
...@@ -718,8 +772,16 @@ class _AttendanceRequestDetailScreenState ...@@ -718,8 +772,16 @@ class _AttendanceRequestDetailScreenState
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: builder:
(context) => Image.network(filePath), (
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"), context,
) => Fileviewer(
fileName:
filePath ??
"",
fileUrl:
filePath ??
"",
),
), ),
); );
}, },
......
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:generp/Notifiers/hrmProvider/AttendanceDetailsProvider.dart';
import 'package:generp/Utils/GlobalConstants.dart'; import 'package:generp/Utils/GlobalConstants.dart';
import 'package:generp/screens/hrm/AddManualAttendance.dart'; import 'package:generp/screens/hrm/AddManualAttendance.dart';
import 'package:generp/screens/hrm/AttendanceRequestDetail.dart'; import 'package:generp/screens/hrm/AttendanceRequestDetail.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../Notifiers/HomeScreenNotifier.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart'; import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart'; import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart'; import '../../Utils/commonWidgets.dart';
...@@ -33,6 +36,11 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -33,6 +36,11 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String _truncate(String text, int maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength).trim() + '...';
}
return SafeArea( return SafeArea(
top: false, top: false,
child: ChangeNotifierProvider( child: ChangeNotifierProvider(
...@@ -46,6 +54,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -46,6 +54,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
builder: (context, child) { builder: (context, child) {
return Consumer<Attendancelistprovider>( return Consumer<Attendancelistprovider>(
builder: (context, provider, child) { builder: (context, provider, child) {
final requestProvider = Provider.of<AttendanceDetailsProvider>(context, listen: false);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
...@@ -80,8 +89,8 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -80,8 +89,8 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
final provider = Provider.of<Attendancelistprovider>(context, listen: false); final provider = Provider.of<Attendancelistprovider>(context, listen: false);
provider.updateFiltersFromSheet( provider.updateFiltersFromSheet(
context,
widget.mode, widget.mode,
context,
type: result['type'] ?? "All", type: result['type'] ?? "All",
selectedValue: result['selectedValue'] ?? "This Month", selectedValue: result['selectedValue'] ?? "This Month",
customRange: result['dateRange'], customRange: result['dateRange'],
...@@ -134,9 +143,9 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -134,9 +143,9 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue)); return const Center(child: CircularProgressIndicator(color: Colors.blue));
} }
if (provider.errorMessage != null) { // if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!)); // return Center(child: Text(provider.errorMessage!));
} // }
if (provider.response?.requestList == null || if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) { provider.response!.requestList!.isEmpty) {
return const Center( return const Center(
...@@ -154,105 +163,221 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -154,105 +163,221 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
return InkWell( final canSwipe = widget.mode == "apr_lvl1" &&
borderRadius: BorderRadius.circular(16), item.status != "Level 1 Approved" &&
onTap: () { item.status != "Rejected";
/// navigation flow
Navigator.push( final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
context, return Slidable(
MaterialPageRoute( key: ValueKey(item.id),
builder: (context) => AttendanceRequestDetailScreen(
attendanceListId: item.id, // Left swipe (Reject)
mode: widget.mode, startActionPane: canSwipe
), ? ActionPane(
motion: const ScrollMotion(),
dragDismissible: false,
children: [
SlidableAction(
onPressed: (_) {
showRemarkSheet(
context: context,
actionType: "Reject",
onSubmit: (remark) async {
await provider.rejectApproveAttendanceRequest(
session: homeProvider.session,
empId: homeProvider.empId,
mode: widget.mode,
type: "Rejected",
remarks: remark,
id: item.id ?? "0",
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Attendance request rejected successfully.")),
);
// refresh list
provider.fetchAttendanceRequests(context, widget.mode);
},
).then((_) {
provider.fetchAttendanceRequests(context, widget.mode); // or setState(() {}) if needed
});
},
backgroundColor: const Color(0xFFFFE5E5),
foregroundColor: const Color(0xFFEF3739),
icon: Icons.clear,
label: 'Reject',
),
],
)
: null,
// Right swipe (Approve)
endActionPane: canSwipe
? ActionPane(
motion: const ScrollMotion(),
dragDismissible: false,
children: [
SlidableAction(
onPressed: (context) {
showRemarkSheet(
context: context,
actionType: "Approve",
onSubmit: (remark) async {
await provider.rejectApproveAttendanceRequest(
session: homeProvider.session,
empId: homeProvider.empId,
mode: widget.mode,
type: "Approved",
remarks: remark,
id: item.id ?? "0",
);
},
).then((_) {
provider.fetchAttendanceRequests(context, widget.mode);
});
print("######################################");
},
backgroundColor: const Color(0xFFE9FFE8),
foregroundColor: const Color(0xFF4CB443),
icon: Icons.check,
label: 'Approve',
), ),
); ],
}, )
child: Container( : null,
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5), child: InkWell(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(16),
color: Colors.white, onTap: () {
borderRadius: BorderRadius.circular(16), Navigator.push(
), context,
child: Row( MaterialPageRoute(
children: [ builder: (context) => AttendanceRequestDetailScreen(
/// Left Avatar Circle attendanceListId: item.id,
Container( mode: widget.mode,
height: 48,
width: 50,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: _getAvatarColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Text(
getText(item.status),
style: TextStyle(
color: _getTextColor(item.status),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
const SizedBox(width: 10), );
},
/// Middle Section child: Container(
Expanded( margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
child: Column( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ color: Colors.white,
Text( borderRadius: BorderRadius.circular(16),
item.type ?? "-", ),
maxLines: 1, child: Row(
overflow: TextOverflow.ellipsis, children: [
/// Left Avatar Circle
Container(
height: 48,
width: 50,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: _getAvatarColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Text(
getText(item.status),
style: TextStyle( style: TextStyle(
fontFamily: "JakartaRegular", color: _getTextColor(item.status),
fontSize: 14, fontSize: 14,
color: AppColors.semi_black, fontWeight: FontWeight.bold,
), ),
), ),
Text( ),
item.date ?? "-", ),
style: TextStyle( const SizedBox(width: 10),
fontFamily: "JakartaRegular",
fontSize: 14, /// Middle Section
color: AppColors.grey_semi, Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.mode == "apr_lvl1"
? _truncate(item.employeeName ?? "-", 20)
: item.type ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
), ),
), Row(
], children: [
Text(
widget.mode == "apr_lvl1"
? item.type ?? "-"
: item.type ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
const SizedBox(width: 2),
Text(
" - ${item.date}" ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
],
),
],
),
), ),
),
/// Right Status (Live / Manual) /// Right Status (Live / Manual)
Text( Text(
item.attendanceType ?? "-", item.attendanceType ?? "-",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle( style: TextStyle(
fontFamily: "JakartaMedium", fontFamily: "JakartaMedium",
fontSize: 14, fontSize: 14,
color: (item.attendanceType ?? "").toLowerCase() == "live" color: (item.attendanceType ?? "").toLowerCase() == "live"
? Colors.green ? Colors.green
: Colors.orange, : Colors.orange,
),
), ),
), ],
], ),
), ),
), ),
); );
}, },
); );
}, },
), ),
) )
], ],
), ),
bottomNavigationBar: Container( bottomNavigationBar: widget.mode == "apr_lvl1"
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
height: 54, height: 61,
decoration: const BoxDecoration(color: Colors.white), decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffFFFFFF),
Color(0x00FFFFFF),
],
),
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
...@@ -269,7 +394,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -269,7 +394,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
), ),
), ),
).then((_) { ).then((_) {
provider.fetchAttendanceRequests(context,widget.mode); provider.fetchAttendanceRequests(context, widget.mode);
}); });
}, },
child: Row( child: Row(
...@@ -314,6 +439,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -314,6 +439,7 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
], ],
), ),
), ),
); );
}, },
...@@ -323,6 +449,190 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> { ...@@ -323,6 +449,190 @@ class _AttendanceListScreenState extends State<AttendanceListScreen> {
); );
} }
Future<void> showRemarkSheet({
required BuildContext context,
required String actionType, // "Approved" or "Rejected"
required Function(String remark) onSubmit,
}) {
final remarkController = TextEditingController();
String? remarkError;
return showModalBottomSheet(
useSafeArea: true,
isDismissible: true,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: Colors.white,
enableDrag: true,
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
void updateState(VoidCallback fn) {
setState(fn);
}
bool validateFields() {
String? newRemarkError =
remarkController.text.trim().isEmpty ? "Remark required" : null;
if (remarkError != newRemarkError) {
updateState(() {
remarkError = newRemarkError;
});
}
return newRemarkError == null;
}
Widget errorText(String? msg) => msg == null
? const SizedBox()
: Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
msg,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
fontFamily: "JakartaMedium",
),
),
);
return SafeArea(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"$actionType Attendance Request",
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 16),
Text(
"Remark",
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 6),
Container(
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: TextField(
controller: remarkController,
maxLines: 3,
onChanged: (val) {
if (remarkError != null && val.isNotEmpty) {
updateState(() => remarkError = null);
}
},
decoration: InputDecoration(
hintText: "Enter your remark here...",
hintStyle: TextStyle(
color: Colors.grey.shade500, // Customize this color
fontSize: 14, // Optional: tweak font size
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
),
),
errorText(remarkError),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: InkResponse(
onTap: () => Navigator.pop(context),
child: Container(
height: 45,
decoration: BoxDecoration(
color: Color(0x12AAAAAA),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
"Cancel",
style: TextStyle(
color: Colors.red,
fontFamily: "JakartaMedium",
),
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: InkResponse(
onTap: () async {
if (validateFields()) {
final remark = remarkController.text.trim();
// Call provider
await onSubmit(remark);
// SnackBar here
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Request submitted successfully"),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
"Submit",
style: TextStyle(
color: Colors.white,
fontFamily: "JakartaMedium",
),
),
),
),
),
),
],
),
],
),
),
),
);
},
);
},
);
}
/// Avatar color generator /// Avatar color generator
Color _getAvatarColor(value) { Color _getAvatarColor(value) {
......
...@@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; ...@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:generp/screens/hrm/Attendancelist.dart'; import 'package:generp/screens/hrm/Attendancelist.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../Utils/app_colors.dart'; import '../../Utils/app_colors.dart';
import 'AttendanceRequestDetail.dart'; import 'AttendanceRequestDetail.dart';
import 'LeaveApplicationScreen.dart'; import 'LeaveApplicationScreen.dart';
...@@ -39,186 +38,189 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -39,186 +38,189 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
appBar: AppBar( top: false,
automaticallyImplyLeading: false, child: Scaffold(
backgroundColor: const Color(0xFFCEEDFF), appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
children: [ backgroundColor: const Color(0xFFCEEDFF),
InkResponse( title: Row(
onTap: () => Navigator.pop(context, true), children: [
child: SvgPicture.asset( InkResponse(
"assets/svg/appbar_back_button.svg", onTap: () => Navigator.pop(context, true),
height: 25, child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), Text(
Text( "HRM",
"HRM", style: TextStyle(
style: TextStyle( fontSize: 18,
fontSize: 18, fontFamily: "Plus Jakarta Sans",
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), ],
], ),
), ),
), backgroundColor: const Color(0xffF6F6F8),
backgroundColor: const Color(0xffF6F6F8), body: SingleChildScrollView(
body: SingleChildScrollView( child: Column(
child: Column( children: [
children: [ /// Background
/// Background Stack(
Stack( children: [
children: [ Container(
Container( width: double.infinity,
width: double.infinity, height: 490,
height: 490, color: const Color(0xffF6F6F8),
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,
),
), ),
), 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, 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,
),
),
), ),
), Container(
width: double.infinity,
/// Content padding: const EdgeInsets.only(top: 1, bottom: 30),
Column( child: Image.asset(
children: [ "assets/images/vector.png",
/// Top Illustration & Button height: 230,
Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.only(top: 60, bottom: 30), fit: BoxFit.fitWidth,
child: Column( ),
children: [ ),
SvgPicture.asset(
"assets/images/capa.svg", /// Content
height: 146, Column(
width: 400, children: [
), /// Top Illustration & Button
const SizedBox(height: 32), Container(
Container( width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.only(top: 60, bottom: 30),
horizontal: 20, vertical: 8), child: Column(
decoration: BoxDecoration( children: [
border: Border.all( SvgPicture.asset(
color: const Color(0xFF1487C9), // border color "assets/images/capa.svg",
width: 1.2, // thickness of the border 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( /// Grid Section
builder: (context) => OrgChartt(), 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> { ...@@ -287,8 +289,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
Expanded( Expanded(
flex: 1, flex: 1,
child: Container( child: Container(
height: constraints.maxHeight * 0.5, // Responsive size height: constraints.maxHeight * 0.39,
width: constraints.maxHeight * 0.5, // Responsive size width: constraints.maxHeight * 0.39,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), color: const Color(0xFFEDF8FF),
...@@ -296,8 +298,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -296,8 +298,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
child: Center( child: Center(
child: SvgPicture.asset( child: SvgPicture.asset(
assetIcon, assetIcon,
height: constraints.maxHeight * 0.3, // Responsive size height: constraints.maxHeight * 0.19,
width: constraints.maxHeight * 0.3, // Responsive size width: constraints.maxHeight * 0.19,
), ),
), ),
), ),
......
...@@ -20,306 +20,334 @@ class LeaveApplicationDetailScreen extends StatefulWidget { ...@@ -20,306 +20,334 @@ class LeaveApplicationDetailScreen extends StatefulWidget {
} }
class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScreen> { class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScreen> {
bool _actionSubmitted = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return SafeArea(
create: (_) => LeaveApplicationDetailsProvider()..fetchLeaveApplicationDetails(context, widget.leaveRequestId), top: false,
child: Consumer<LeaveApplicationDetailsProvider>( child: ChangeNotifierProvider(
builder: (context, provider, child) { create: (_) => LeaveApplicationDetailsProvider()..fetchLeaveApplicationDetails(context, widget.leaveRequestId),
return Scaffold( child: Consumer<LeaveApplicationDetailsProvider>(
appBar: AppBar( builder: (context, provider, child) {
automaticallyImplyLeading: false, return Scaffold(
backgroundColor: const Color(0xFFFFFFFF), appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
mainAxisAlignment: MainAxisAlignment.start, backgroundColor: const Color(0xFFFFFFFF),
crossAxisAlignment: CrossAxisAlignment.center, title: Row(
children: [ mainAxisAlignment: MainAxisAlignment.start,
InkResponse( crossAxisAlignment: CrossAxisAlignment.center,
onTap: () => Navigator.pop(context, true), children: [
child: SvgPicture.asset( InkResponse(
"assets/svg/appbar_back_button.svg", onTap: () => Navigator.pop(context, true),
height: 25, child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), InkResponse(
InkResponse( onTap: () => Navigator.pop(context, true),
onTap: () => Navigator.pop(context, true), child: Text(
child: Text( "Leave Application Details",
"Leave Application Details", style: TextStyle(
style: TextStyle( fontSize: 18,
fontSize: 18, height: 1.1,
height: 1.1, fontFamily: "Plus Jakarta Sans",
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), ),
), ],
], ),
), ),
), backgroundColor: const Color(0xFFF6F6F8),
backgroundColor: const Color(0xFFF6F6F8), body: Builder(
body: Builder( builder: (context) {
builder: (context) { if (provider.isLoading) {
if (provider.isLoading) { return const Center(child: CircularProgressIndicator(color: Colors.blue));
return const Center(child: CircularProgressIndicator(color: Colors.blue)); }
} if (provider.response?.requestDetails == null) {
if (provider.errorMessage != null) { return const Center(child: Text("No details found"));
return Center(child: Text(provider.errorMessage!)); }
}
if (provider.response?.requestDetails == null) { // Get screen dimensions for responsive scaling
return const Center(child: Text("No details found")); final screenWidth = MediaQuery.of(context).size.width;
} final screenHeight = MediaQuery.of(context).size.height;
// Scale factors based on screen size
final details = provider.response!.requestDetails!; final scaleFactor = screenWidth / 360; // Base width for scaling
final details = provider.response!.requestDetails!;
/// Screen content
return SingleChildScrollView( /// Screen content
padding: const EdgeInsets.all(16.0), return SingleChildScrollView(
child: Column( padding: EdgeInsets.all(16.0 * scaleFactor),
children: [ child: Column(
Card( children: [
shape: RoundedRectangleBorder( Card(
borderRadius: BorderRadius.circular(16), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(16 * scaleFactor),
elevation: 2, ),
child: Padding( elevation: 0,
padding: const EdgeInsets.all(10.0), child: Padding(
child: Column( padding: EdgeInsets.all(10.0 * scaleFactor),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
/// Header with status children: [
Container( /// Header with status
margin: const EdgeInsets.only(bottom: 0.5), Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 2), margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
decoration: BoxDecoration( padding: EdgeInsets.symmetric(
color: Colors.white, vertical: 10 * scaleFactor,
borderRadius: BorderRadius.circular(12), horizontal: 2 * scaleFactor,
), ),
child: Row( decoration: BoxDecoration(
children: [ color: Colors.white,
/// Left Avatar borderRadius: BorderRadius.circular(12 * scaleFactor),
Container( ),
height: 48, child: Row(
width: 48, children: [
decoration: BoxDecoration( /// Left Avatar
shape: BoxShape.circle, Container(
color: const Color(0xFFEDF8FF), // icon bg height: 48 * scaleFactor,
), width: 48 * scaleFactor,
child: Center( decoration: BoxDecoration(
child: SvgPicture.asset( shape: BoxShape.circle,
height: 28, color: const Color(0xFFEDF8FF),
width: 28, ),
"assets/svg/hrm/leaveApplication.svg", // Use appropriate icon child: Center(
fit: BoxFit.contain, child: SvgPicture.asset(
"assets/svg/hrm/leaveApplication.svg",
height: 28 * scaleFactor,
width: 28 * scaleFactor,
fit: BoxFit.contain,
),
), ),
), ),
), SizedBox(width: 12 * scaleFactor),
const SizedBox(width: 12),
/// Middle text
/// Middle text Expanded(
Expanded( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( details.leaveType ?? "-",
details.leaveType ?? "-", style: TextStyle(
style: TextStyle( decoration: TextDecoration.underline,
decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.dotted,
decorationStyle: decorationColor: AppColors.grey_thick,
TextDecorationStyle.dotted, height: 1.2,
decorationColor: AppColors.grey_thick, fontFamily: "JakartaRegular",
height: 1.2, fontSize: 12 * scaleFactor,
fontFamily: "JakartaRegular", color: AppColors.semi_black,
fontSize: 14, ),
color: AppColors.semi_black,
), ),
), SizedBox(height: 2 * scaleFactor),
const SizedBox(height: 2), Text(
Text( "Applied: ${details.appliedDate ?? "-"}",
"Applied: ${details.appliedDate ?? "-"}", style: TextStyle(
style: TextStyle( fontFamily: "JakartaRegular",
fontFamily: "JakartaRegular", fontSize: 12 * scaleFactor,
fontSize: 14, color: AppColors.app_blue,
color: AppColors.app_blue, ),
), ),
), ],
], ),
), ),
),
/// Right side status badge /// Right side status badge
Container( Container(
height: 30, height: 28 * scaleFactor,
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), padding: EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: 5 * scaleFactor,
borderRadius: BorderRadius.circular(8), vertical: 1 * scaleFactor,
color: _getStatusBackgroundColor(details.status), ),
), decoration: BoxDecoration(
child: Center( borderRadius: BorderRadius.circular(8 * scaleFactor),
child: Text( color: _getStatusBackgroundColor(details.status),
details.status ?? "-", ),
textAlign: TextAlign.center, child: Center(
style: TextStyle( child: Text(
fontFamily: "JakartaMedium", details.status ?? "-",
fontSize: 12, textAlign: TextAlign.center,
color: _getStatusTextColor(details.status), style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 10 * scaleFactor,
color: _getStatusTextColor(details.status),
),
), ),
), ),
), ),
), ],
], ),
), ),
),
/// Leave Details /// Leave Details
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0 * scaleFactor),
child: Column( child: Column(
children: [ children: [
_buildSectionHeader("Leave Details"), _buildSectionHeader("Leave Details", ),
_buildDetailTile("Application ID", details.id), _buildDetailTile("Application ID", details.id, scaleFactor),
_buildDetailTile("Applied Date", details.appliedDate), _buildDetailTile("Applied Date", details.appliedDate, scaleFactor),
_buildDetailTile("Leave Type", details.leaveType), _buildDetailTile("Leave Type", details.leaveType, scaleFactor),
_buildDateRangeTile("Leave Period", details.fromDate, details.toDate), _buildDateRangeTile("Leave Period", details.fromDate, details.toDate, scaleFactor),
_buildTimeRangeTile("Time Period", details.fromTime, details.toTime), _buildTimeRangeTile("Time Period", details.fromTime, details.toTime, scaleFactor),
_buildDetailTile("Reason", details.reason), _buildDetailTile("Reason", details.reason, scaleFactor),
/// Approval Details /// Approval Details
_buildSectionHeader("Approval Details"), _buildSectionHeader("Approval Details", ),
_buildDetailTile("Requested To", details.requestedTo), _buildDetailTile("Requested To", details.requestedTo, scaleFactor),
_buildDetailTile("Approved By", details.approvedBy), _buildDetailTile("Approved By", details.approvedBy, scaleFactor),
_buildDetailTile("Approved Date", details.approvedDate), _buildDetailTile("Approved Date", details.approvedDate, scaleFactor),
_buildDetailTile("Approval Remarks", details.approvalRemarks), _buildDetailTile("Approval Remarks", details.approvalRemarks, scaleFactor),
/// Additional Information /// Additional Information
_buildSectionHeader("Additional Information"), _buildSectionHeader("Additional Information", ),
_buildDetailTile("Status", details.status), _buildDetailTile("Status", details.status, scaleFactor),
_buildDetailTile("From Time", details.fromTime), _buildDetailTile("From Time", details.fromTime, scaleFactor),
_buildDetailTile("To Time", details.toTime), _buildDetailTile("To Time", details.toTime, scaleFactor),
], ],
),
), ),
), ],
], ),
), ),
), ),
), SizedBox(height: 30 * scaleFactor),
const SizedBox(height: 30), ],
),
);
},
),
bottomNavigationBar: (widget.mode == "teamleader"
&& !_actionSubmitted
&& provider.response?.requestDetails?.status != "Approved"
&& provider.response?.requestDetails?.status != "Rejected")
? Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffFFFFFF),
Color(0x00FFFFFF),
], ],
), ),
); ),
}, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
), height: 61,
bottomNavigationBar: widget.mode == "teamleader" child: Column(
? Container( children: [
decoration: const BoxDecoration(color: Colors.white), Row(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), children: [
height: 80, /// Reject Button
child: Row( Expanded(
children: [ child: InkWell(
/// Reject Button onTap: () {
Expanded( showRemarkSheet(
child: InkWell( context: context,
onTap: () { actionType: "Reject",
showRemarkSheet( onSubmit: (remark) {
context: context, provider.leaveRequestRejectApprove(
actionType: "Reject", context,
onSubmit: (remark) { mode: widget.mode,
provider.leaveRequestRejectApprove( type: "Rejected",
context, remarks: remark,
mode: widget.mode, id: provider.response!.requestDetails!.id!,
type: "Rejected", );
remarks: remark, },
id: provider.response!.requestDetails!.id!, ).then((_) {
); provider.fetchLeaveApplicationDetails(context, widget.leaveRequestId);
}, });
); },
}, child: Container(
child: Container( alignment: Alignment.center,
alignment: Alignment.center, decoration: BoxDecoration(
height: 45, borderRadius: BorderRadius.circular(8),
decoration: BoxDecoration( ),
borderRadius: BorderRadius.circular(8), child: Row(
color: const Color(0xFFFFFFFF), mainAxisAlignment: MainAxisAlignment.center,
border: Border.all( children: [
color: const Color(0xFFE0E0E0), SvgPicture.asset("assets/svg/finance/level_reject_ic.svg"),
width: 0.5, const SizedBox(width: 6),
), const Text(
), "Reject",
child: Row( style: TextStyle(
mainAxisAlignment: MainAxisAlignment.center, color: Colors.black87,
children: [ fontSize: 14,
SvgPicture.asset("assets/svg/finance/level_reject_ic.svg"), fontFamily: "JakartaMedium",
const SizedBox(width: 6), ),
const Text( ),
"Reject", ],
style: TextStyle(
color: Colors.black87,
fontSize: 14,
fontFamily: "JakartaMedium",
), ),
), ),
],
),
),
),
),
const SizedBox(width: 10),
/// Approve Button
Expanded(
child: InkWell(
onTap: () {
showRemarkSheet(
context: context,
actionType: "Approve",
onSubmit: (remark) {
provider.leaveRequestRejectApprove(
context,
mode: widget.mode,
type: "Approved",
remarks: remark,
id: provider.response!.requestDetails!.id!,
);
},
);
},
child: Container(
alignment: Alignment.center,
height: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: const Color(0xFFFFFFFF),
border: Border.all(
color: const Color(0xFFE0E0E0),
width: 0.5,
), ),
), ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, /// Vertical Divider
children: [ Container(
SvgPicture.asset("assets/svg/finance/level_approve_ic.svg"), width: 1,
const SizedBox(width: 6), height: 45,
const Text( color: Colors.grey.shade300,
"Approve", ),
style: TextStyle(
color: Colors.black87, /// Approve Button
fontSize: 14, Expanded(
fontFamily: "JakartaMedium", child: InkWell(
onTap: () {
showRemarkSheet(
context: context,
actionType: "Approve",
onSubmit: (remark) async {
await provider.leaveRequestRejectApprove(
context,
mode: widget.mode,
type: "Approved",
remarks: remark,
id: provider.response!.requestDetails!.id!,
);
},
).then((_) {
provider.fetchLeaveApplicationDetails(context, widget.leaveRequestId);
});
},
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/finance/level_approve_ic.svg"),
const SizedBox(width: 6),
const Text(
"Approve",
style: TextStyle(
color: Colors.black87,
fontSize: 14,
fontFamily: "JakartaMedium",
),
),
],
), ),
), ),
], ),
), ),
), ],
), ),
), SizedBox(height: 2,)
], ],
), ),
)
: const SizedBox.shrink(), )
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, : const SizedBox.shrink(),
); floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
}, );
},
),
), ),
); );
} }
...@@ -404,30 +432,38 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -404,30 +432,38 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( Container(
controller: remarkController, margin: const EdgeInsets.only(bottom: 6),
maxLines: 3, decoration: BoxDecoration(
onChanged: (val) { color: AppColors.text_field_color,
if (remarkError != null && val.isNotEmpty) { borderRadius: BorderRadius.circular(14),
updateState(() => remarkError = null); ),
} child: TextField(
}, controller: remarkController,
decoration: InputDecoration( maxLines: 3,
hintText: "Enter your remark here...", style: TextStyle(
filled: true, color: Colors.black, // Entered text color
fillColor: Colors.grey.shade100, fontSize: 14, // Optional: adjust font size
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
), ),
contentPadding: const EdgeInsets.symmetric( onChanged: (val) {
vertical: 12, if (remarkError != null && val.isNotEmpty) {
horizontal: 12, updateState(() => remarkError = null);
}
},
decoration: InputDecoration(
hintText: "Enter your remark here...",
hintStyle: TextStyle(
color: Colors.grey.shade500, // Customize this color
fontSize: 14, // Optional: tweak font size
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
), ),
), ),
), ),
errorText(remarkError), errorText(remarkError),
const SizedBox(height: 20), const SizedBox(height: 5),
Row( Row(
children: [ children: [
...@@ -437,7 +473,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -437,7 +473,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
child: Container( child: Container(
height: 45, height: 45,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.shade100, color: Color(0x12AAAAAA),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Center( child: const Center(
...@@ -458,10 +494,14 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -458,10 +494,14 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
onTap: () async { onTap: () async {
if (validateFields()) { if (validateFields()) {
final remark = remarkController.text.trim(); final remark = remarkController.text.trim();
// Call provider
await onSubmit(remark); await onSubmit(remark);
// SnackBar here
Navigator.pop(context); Navigator.pop(context);
if (mounted) {
setState(() {
_actionSubmitted = true;
});
}
// Show snackbar
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text("Request submitted successfully"), content: Text("Request submitted successfully"),
...@@ -472,6 +512,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -472,6 +512,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
} }
}, },
child: Container( child: Container(
height: 45, height: 45,
decoration: BoxDecoration( decoration: BoxDecoration(
...@@ -493,6 +534,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -493,6 +534,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
), ),
], ],
), ),
SizedBox(height: 2,)
], ],
), ),
), ),
...@@ -506,31 +548,38 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -506,31 +548,38 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
/// Reusable Row Widget for details /// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value) { Widget _buildDetailTile(String label, String? value, double scaleFactor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // Align top if value wraps
children: [ children: [
// Label
Expanded( Expanded(
flex: 6, flex: 5,
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
fontFamily: "JakartaRegular", fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 12 * scaleFactor,
color: AppColors.semi_black, color: AppColors.semi_black,
), ),
), ),
), ),
const SizedBox(width: 4),
// Value
Expanded( Expanded(
flex: 0, flex: 5,
child: Text( child: Text(
value ?? "-", value ?? "-",
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 12 * scaleFactor,
color: Color(0xff818181), color: const Color(0xFF818181),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
softWrap: true,
overflow: TextOverflow.visible,
), ),
), ),
], ],
...@@ -538,32 +587,40 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -538,32 +587,40 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
); );
} }
/// For date range display /// For date range display
Widget _buildDateRangeTile(String label, String? fromDate, String? toDate) { Widget _buildDateRangeTile(String label, String? fromDate, String? toDate, double scaleFactor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Label
Expanded( Expanded(
flex: 6, flex: 5,
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
fontFamily: "JakartaRegular", fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 12 * scaleFactor,
color: AppColors.semi_black, color: AppColors.semi_black,
), ),
), ),
), ),
const SizedBox(width: 4),
// Value
Expanded( Expanded(
flex: 0, flex: 5,
child: Text( child: Text(
'${fromDate ?? "-"} to ${toDate ?? "-"}', '${fromDate ?? "-"} to ${toDate ?? "-"}',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 12 * scaleFactor,
color: Color(0xff818181), color: const Color(0xFF818181),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
softWrap: true,
overflow: TextOverflow.visible,
), ),
), ),
], ],
...@@ -571,38 +628,46 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -571,38 +628,46 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
); );
} }
/// For time range display /// For time range display
Widget _buildTimeRangeTile(String label, String? fromTime, String? toTime) { Widget _buildTimeRangeTile(String label, String? fromTime, String? toTime, double scaleFactor) {
if ((fromTime == null || fromTime.isEmpty) && (toTime == null || toTime.isEmpty)) { if ((fromTime == null || fromTime.isEmpty) && (toTime == null || toTime.isEmpty)) {
return const SizedBox.shrink(); // Hide if no time data return const SizedBox.shrink(); // Hide if no time data
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Label
Expanded( Expanded(
flex: 6, flex: 5,
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 12 * scaleFactor,
color: Color(0xff2D2D2D), color: const Color(0xff2D2D2D),
fontStyle: FontStyle.normal, fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans", fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
), ),
), ),
const SizedBox(width: 4),
// Value
Expanded( Expanded(
flex: 0, flex: 5,
child: Text( child: Text(
'${fromTime ?? "-"} to ${toTime ?? "-"}', '${fromTime ?? "-"} to ${toTime ?? "-"}',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 12 * scaleFactor,
color: Color(0xff818181), color: const Color(0xff818181),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
softWrap: true,
overflow: TextOverflow.visible,
), ),
), ),
], ],
...@@ -610,6 +675,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr ...@@ -610,6 +675,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
); );
} }
/// Section header with dotted line /// Section header with dotted line
Widget _buildSectionHeader(String title) { Widget _buildSectionHeader(String title) {
return Padding( return Padding(
......
...@@ -31,278 +31,292 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen> ...@@ -31,278 +31,292 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return SafeArea(
create: (_) { top: false,
final provider = LeaveApplicationListProvider(); child: ChangeNotifierProvider(
Future.microtask(() { create: (_) {
provider.fetchLeaveApplications(context, widget.mode); final provider = LeaveApplicationListProvider();
}); Future.microtask(() {
return provider; provider.fetchLeaveApplications(context, widget.mode);
}, });
builder: (context, child) { return provider;
return Consumer<LeaveApplicationListProvider>( },
builder: (context, provider, child) { builder: (context, child) {
return Scaffold( return Consumer<LeaveApplicationListProvider>(
appBar: appbar2New( builder: (context, provider, child) {
context, return Scaffold(
"Leave Application List", appBar: appbar2New(
provider.resetForm, context,
Row( "Leave Application List",
children: [ provider.resetForm,
InkResponse( Row(
onTap: () async { children: [
var cf = Commondaterangefilter(); InkResponse(
var result = await cf.showFilterBottomSheet(context); onTap: () async {
if (result != null) { var cf = Commondaterangefilter();
var dateRange = result['dateRange'] as DateTimeRange?; var result = await cf.showFilterBottomSheet(context);
var formatted = result['formatted'] as List<String>; if (result != null) {
if (formatted.isNotEmpty) { var dateRange = result['dateRange'] as DateTimeRange?;
provider.setDateRangeFilter("Custom", customRange: dateRange); var formatted = result['formatted'] as List<String>;
provider.fetchLeaveApplications( if (formatted.isNotEmpty) {
context, provider.setDateRangeFilter("Custom", customRange: dateRange);
widget.mode, provider.fetchLeaveApplications(
dateRange: "Custom", context,
customRange: dateRange, widget.mode,
); dateRange: "Custom",
customRange: dateRange,
);
}
} }
} },
}, child: SvgPicture.asset("assets/svg/filter_ic.svg", height: 25),
child: SvgPicture.asset("assets/svg/filter_ic.svg", height: 25), ),
), ],
], ),
0xFFFFFFFF,
), ),
0xFFFFFFFF, backgroundColor: const Color(0xFFF6F6F8),
), body: Column(
backgroundColor: const Color(0xFFF6F6F8), children: [
body: Column( /// Filter chips (if you want visible filter indicators)
children: [ // if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month")
/// Filter chips (if you want visible filter indicators) // Container(
// if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month") // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// Container( // color: Colors.white,
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), // child: Row(
// color: Colors.white, // children: [
// child: Row( // if (provider.selectedStatus != "All")
// children: [ // Chip(
// if (provider.selectedStatus != "All") // label: Text('Status: ${provider.selectedStatus}'),
// Chip( // onDeleted: () {
// label: Text('Status: ${provider.selectedStatus}'), // provider.setStatusFilter("All");
// onDeleted: () { // provider.fetchLeaveApplications(context);
// provider.setStatusFilter("All"); // },
// provider.fetchLeaveApplications(context); // ),
// }, // if (provider.selectedDateRange != "This Month")
// ), // Chip(
// if (provider.selectedDateRange != "This Month") // label: Text('Date: ${provider.selectedDateRange}'),
// Chip( // onDeleted: () {
// label: Text('Date: ${provider.selectedDateRange}'), // provider.setDateRangeFilter("This Month");
// onDeleted: () { // provider.fetchLeaveApplications(context);
// provider.setDateRangeFilter("This Month"); // },
// provider.fetchLeaveApplications(context); // ),
// }, // ],
// ), // ),
// ], // ),
// ),
// ),
/// Leave application list /// Leave application list
Expanded( Expanded(
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue)); return const Center(child: CircularProgressIndicator(color: Colors.blue));
} }
if (provider.errorMessage != null) { if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!)); return Center(child: Text(provider.errorMessage!));
} }
if (provider.response?.requestList == null || if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) { provider.response!.requestList!.isEmpty) {
return const Center(child: Text("No leave applications found")); return const Center(child: Text("No leave applications found"));
} }
final list = provider.response!.requestList!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
// Parse the full string into a DateTime object
DateTime parsedFromDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.fromPeriod.toString());
String dateFromMonth = DateFormat("dd MMM").format(parsedFromDate);
// Parse the full string into a DateTime object
DateTime parsedToDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.toPeriod.toString());
String dateToMonth = DateFormat("dd MMM yyyy").format(parsedToDate); final list = provider.response!.requestList!;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
// Parse the full string into a DateTime object
DateTime parsedFromDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.fromPeriod.toString());
String dateFromMonth = DateFormat("dd MMM").format(parsedFromDate);
// Parse the full string into a DateTime object
DateTime parsedToDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.toPeriod.toString());
return InkWell( String dateToMonth = DateFormat("dd MMM yyyy").format(parsedToDate);
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationDetailScreen(
leaveRequestId: item.id.toString(),
mode: widget.mode,
),
),
).then((_) {
provider.fetchLeaveApplications(context,widget.mode);
});
},
child: Container( return InkWell(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5), borderRadius: BorderRadius.circular(16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), onTap: () {
decoration: BoxDecoration( Navigator.push(
color: Colors.white, context,
borderRadius: BorderRadius.circular(16), MaterialPageRoute(
), builder: (context) => LeaveApplicationDetailScreen(
child: Row( leaveRequestId: item.id.toString(),
children: [ mode: widget.mode,
/// Left Status Circle
Container(
height: 48,
width: 48,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Text(
_getStatusInitials(item.status),
style: TextStyle(
color: _getStatusTextColor(item.status),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
const SizedBox(width: 12), ).then((_) {
provider.fetchLeaveApplications(context,widget.mode);
});
},
/// Middle Section - Leave Details child: Container(
Expanded( margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
child: Column( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ color: Colors.white,//Color(int.parse(item.rowColor!.replaceFirst('#', '0xff'))),
Text( borderRadius: BorderRadius.circular(16),
item.leaveType ?? "-", ),
maxLines: 1, child: Row(
overflow: TextOverflow.ellipsis, children: [
/// Left Status Circle
Container(
height: 48,
width: 48,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Text(
_getStatusInitials(item.status),
style: TextStyle( style: TextStyle(
fontFamily: "JakartaRegular", color: _getStatusTextColor(item.status),
fontSize: 14, fontSize: 14,
color: AppColors.semi_black, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 4), ),
Row( ),
children: [ const SizedBox(width: 12),
Text(
dateFromMonth ?? "-", /// Middle Section - Leave Details
style: TextStyle( Expanded(
fontFamily: "JakartaRegular", child: Column(
fontSize: 14, crossAxisAlignment: CrossAxisAlignment.start,
color: AppColors.grey_semi, children: [
), Text(
widget.mode == "teamleader"
? item.employeeName ?? "-"
: item.leaveType ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
), ),
Text( ),
" - ${dateToMonth}" ?? "-", const SizedBox(height: 4),
style: TextStyle( Row(
fontFamily: "JakartaRegular", children: [
fontSize: 14, Text(
color: AppColors.grey_semi, dateFromMonth ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
), ),
), Text(
], " - ${dateToMonth}" ?? "-",
), style: TextStyle(
// const SizedBox(height: 2), fontFamily: "JakartaRegular",
// Text( fontSize: 14,
// "Period: ${item.fromPeriod ?? "-"} to ${item.toPeriod ?? "-"}", color: AppColors.grey_semi,
// style: const TextStyle( ),
// fontSize: 12.5, ),
// color: Color(0xff818181), ],
// fontFamily: "Plus Jakarta Sans", ),
// ), // const SizedBox(height: 2),
// ), // Text(
], // "Period: ${item.fromPeriod ?? "-"} to ${item.toPeriod ?? "-"}",
// style: const TextStyle(
// fontSize: 12.5,
// color: Color(0xff818181),
// fontFamily: "Plus Jakarta Sans",
// ),
// ),
],
),
), ),
),
// /// Right Status /// Right Status
// Container( if (widget.mode == "teamleader")
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), Container(
// decoration: BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// color: _getStatusBackgroundColor(item.status), decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(10), color: Color(0x00FFFFFF),
// ), borderRadius: BorderRadius.circular(10),
// child: Text( ),
// item.status ?? "-", child: Text(
// style: TextStyle( item.leaveType ?? "-",
// fontFamily: "JakartaMedium", style: TextStyle(
// fontSize: 13, fontFamily: "JakartaMedium",
// color: _getStatusTextColor(item.status), fontSize: 13,
// ), color: AppColors.app_blue,
// ), ),
// ), ),
], ),
],
),
), ),
), );
); },
}, );
); },
},
),
)
],
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: InkResponse(
onTap: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider(
create: (_) => LeaveApplicationListProvider(),
child: AddLeaveRequest(pageTitleName: "Add Leave Request"),
), ),
), ),
).then((_) { SizedBox(height: 28,)
provider.fetchLeaveApplications(context, widget.mode); ],
}); ),
// show add bill screen here bottomNavigationBar: widget.mode == "teamleader"
}, ? null
child: Container( : Container(
height: 45, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
alignment: Alignment.center, color: Colors.white,
margin: EdgeInsets.symmetric(horizontal: 20), child: InkResponse(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), onTap: () {
decoration: BoxDecoration( HapticFeedback.selectionClick();
color: AppColors.app_blue, Navigator.push(
borderRadius: BorderRadius.circular(15), context,
), MaterialPageRoute(
child: Text( builder: (_) => ChangeNotifierProvider(
"Add Leave Request", create: (_) => LeaveApplicationListProvider(),
style: TextStyle( child: AddLeaveRequest(pageTitleName: "Add Leave Request"),
fontSize: 15, ),
fontFamily: "JakartaMedium", ),
color: Colors.white, ).then((_) {
provider.fetchLeaveApplications(context, widget.mode);
});
// show add bill screen here
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: EdgeInsets.symmetric(horizontal: 14),
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: Text(
"Add Leave Request",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
), ),
), ),
), ),
), );
); },
}, );
); },
}, ),
); );
} }
/// Get status background color /// Get status background color
Color _getStatusBackgroundColor(String? status) { Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) { switch (status?.toLowerCase()) {
...@@ -316,13 +330,14 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen> ...@@ -316,13 +330,14 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen>
} }
} }
/// Get status text color /// Get status text color
Color _getStatusTextColor(String? status) { Color _getStatusTextColor(String? status) {
switch (status?.toLowerCase()) { switch (status?.toLowerCase()) {
case 'approved': case 'approved':
return AppColors.approved_text_color; return AppColors.approved_text_color;
case 'rejected': case 'rejected':
return AppColors.rejected_text_color; return Colors.redAccent.shade200;
case 'requested': case 'requested':
default: default:
return AppColors.requested_text_color; return AppColors.requested_text_color;
......
...@@ -17,285 +17,288 @@ class RewardListScreen extends StatefulWidget { ...@@ -17,285 +17,288 @@ class RewardListScreen extends StatefulWidget {
class _RewardListScreenState extends State<RewardListScreen> { class _RewardListScreenState extends State<RewardListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return SafeArea(
create: (_) => top: false,
RewardListProvider()..fetchRewardList(context), child: ChangeNotifierProvider(
child: Consumer<RewardListProvider>( create: (_) =>
builder: (context, provider, child) { RewardListProvider()..fetchRewardList(context),
return Scaffold( child: Consumer<RewardListProvider>(
appBar: AppBar( builder: (context, provider, child) {
automaticallyImplyLeading: false, return Scaffold(
backgroundColor: Colors.white, appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
children: [ backgroundColor: Colors.white,
InkResponse( title: Row(
onTap: () => Navigator.pop(context, true), children: [
child: SvgPicture.asset( InkResponse(
"assets/svg/appbar_back_button.svg", onTap: () => Navigator.pop(context, true),
height: 25, child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), Text(
Text( "Reward List",
"Reward List", style: TextStyle(
style: TextStyle( fontSize: 18,
fontSize: 18, fontFamily: "Plus Jakarta Sans",
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), ],
], ),
// actions: [
// InkResponse(
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => RewardSearchScreen(),
// settings: const RouteSettings(
// name: 'AddLiveAttendanceScreen',
// ),
// ),
// ).then((_) {
// });
// },
// child: SvgPicture.asset(
// "assets/svg/search_ic.svg",
// height: 25,
// ),
// ),
// const SizedBox(width: 20),
// ],
), ),
// actions: [
// InkResponse(
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => RewardSearchScreen(),
// settings: const RouteSettings(
// name: 'AddLiveAttendanceScreen',
// ),
// ),
// ).then((_) {
// });
// },
// child: SvgPicture.asset(
// "assets/svg/search_ic.svg",
// height: 25,
// ),
// ),
// const SizedBox(width: 20),
// ],
),
backgroundColor: Color(0xFFF6F6F8), backgroundColor: Color(0xFFF6F6F8),
body: Builder( body: Builder(
builder: (context) { builder: (context) {
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator( return const Center(child: CircularProgressIndicator(
color: Colors.blue,)); color: Colors.blue,));
} }
if (provider.errorMessage != null) { if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!)); return Center(child: Text(provider.errorMessage!));
} }
if (provider.response == null) { if (provider.response == null) {
return const Center(child: Text("No details found")); return const Center(child: Text("No details found"));
} }
final rewardDetail = provider.response!; final rewardDetail = provider.response!;
final rewardResponse = provider.response!; final rewardResponse = provider.response!;
final rewards = rewardResponse.rewardsList; // main list object final rewards = rewardResponse.rewardsList; // main list object
final achieved = rewardResponse.achievedAmount ?? "0"; final achieved = rewardResponse.achievedAmount ?? "0";
final disbursed = rewardResponse.disbursedAmount ?? "0"; final disbursed = rewardResponse.disbursedAmount ?? "0";
final balance = rewardResponse.balanceAmount ?? "0"; final balance = rewardResponse.balanceAmount ?? "0";
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
/// --- Top Summary Cards --- /// --- Top Summary Cards ---
Stack( Stack(
children: [ children: [
Container( Container(
height: 110, height: 110,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xffd9ffd6), color: const Color(0xffd9ffd6),
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"₹${achieved}", // Achieved Amount from response "₹${achieved}", // Achieved Amount from response
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
color: Color(0xff0D9C00), color: Color(0xff0D9C00),
fontStyle: FontStyle.normal, fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
),
), ),
), const SizedBox(height: 10),
const SizedBox(height: 10), const Text(
const Text( "Achievement Amount",
"Achievement Amount", style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, color: Color(0xff2D2D2D),
color: Color(0xff2D2D2D), fontStyle: FontStyle.normal,
fontStyle: FontStyle.normal, fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, ),
), ),
), ],
], ),
), ),
),
// Positioned SVG Icon // Positioned SVG Icon
Positioned( Positioned(
bottom: 8, bottom: 8,
right: 12, right: 12,
child: Container( child: Container(
height: 42, height: 42,
width: 42, width: 42,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg color: const Color(0xA0FFFFFF), // icon bg
), ),
child: Center( child: Center(
child: SvgPicture.asset( child: SvgPicture.asset(
height: 25, height: 25,
width: 25, width: 25,
"assets/svg/hrm/achievement_ic.svg", "assets/svg/hrm/achievement_ic.svg",
fit: BoxFit.contain, fit: BoxFit.contain,
),
), ),
), ),
), ),
), ],
], ),
), const SizedBox(height: 12),
const SizedBox(height: 12),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Container( child: Container(
height: 110, height: 110,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xffe8ddff), color: const Color(0xffe8ddff),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Stack( child: Stack(
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"₹${disbursed}", // Disbursed Amount "₹${disbursed}", // Disbursed Amount
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
color: Color(0xff493272), color: Color(0xff493272),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), const Text(
const Text( "Disbursed \nAmount",
"Disbursed \nAmount", style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, color: Color(0xff2D2D2D),
color: Color(0xff2D2D2D), fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, ),
), ),
), ],
], ),
), Positioned(
Positioned( bottom: 2,
bottom: 2, right: 2,
right: 2, child: Container(
child: Container( height: 42,
height: 42, width: 42,
width: 42, decoration: BoxDecoration(
decoration: BoxDecoration( shape: BoxShape.circle,
shape: BoxShape.circle, color: const Color(0xA0FFFFFF), // icon bg
color: const Color(0xA0FFFFFF), // icon bg ),
), child: Center(
child: Center( child: SvgPicture.asset(
child: SvgPicture.asset( height: 25,
height: 25, width: 25,
width: 25, "assets/svg/hrm/location_ic.svg",
"assets/svg/hrm/location_ic.svg", fit: BoxFit.contain,
fit: BoxFit.contain, ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 12), Expanded(
Expanded( child: Container(
child: Container( height: 110,
height: 110, padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), decoration: BoxDecoration(
decoration: BoxDecoration( color: const Color(0xfffffbc3),
color: const Color(0xfffffbc3), borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), ),
), child: Stack(
child: Stack( children: [
children: [ Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( "₹${balance}", // Balance Amount
"₹${balance}", // Balance Amount style: const TextStyle(
style: const TextStyle( fontSize: 18,
fontSize: 18, color: Color(0xff605C00),
color: Color(0xff605C00), ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), const Text(
const Text( "Balance \nAmount",
"Balance \nAmount", style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, color: Color(0xff2D2D2D),
color: Color(0xff2D2D2D), fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, ),
), ),
), ],
], ),
), Positioned(
Positioned( bottom: 2,
bottom: 2, right: 2,
right: 2, child: Container(
child: Container( height: 42,
height: 42, width: 42,
width: 42, decoration: BoxDecoration(
decoration: BoxDecoration( shape: BoxShape.circle,
shape: BoxShape.circle, color: const Color(0xA0FFFFFF), // icon bg
color: const Color(0xA0FFFFFF), // icon bg ),
), child: Center(
child: Center( child: SvgPicture.asset(
child: SvgPicture.asset( height: 25,
height: 25, width: 25,
width: 25, "assets/svg/hrm/ballance_ic.svg",
"assets/svg/hrm/ballance_ic.svg", fit: BoxFit.contain,
fit: BoxFit.contain, ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
), ],
], ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
/// --- Reward List Card --- /// --- Reward List Card ---
if (rewards != null) if (rewards != null)
_rewardListCard( _rewardListCard(
title: rewards.description ?? "-", // rewardsList fields title: rewards.description ?? "-", // rewardsList fields
dateTime: rewards.dateTime ?? "-", dateTime: rewards.dateTime ?? "-",
achieved: achieved, achieved: achieved,
disbursed: disbursed, disbursed: disbursed,
balance: balance, balance: balance,
enteredBy: rewards.enteredBy ?? "-", enteredBy: rewards.enteredBy ?? "-",
) )
else else
const Text("No rewards available"), const Text("No rewards available"),
], ],
), ),
); );
} }
), ),
); );
} }
) )
),
); );
} }
...@@ -315,13 +318,13 @@ class _RewardListScreenState extends State<RewardListScreen> { ...@@ -315,13 +318,13 @@ class _RewardListScreenState extends State<RewardListScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.grey.withOpacity(0.1), // color: Colors.grey.withOpacity(0.1),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(0, 3), // offset: const Offset(0, 3),
) // )
], // ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
......
...@@ -19,299 +19,316 @@ class TourExpensesDetailsScreen extends StatefulWidget { ...@@ -19,299 +19,316 @@ class TourExpensesDetailsScreen extends StatefulWidget {
class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return SafeArea(
create: (_) => TourExpensesDetailsProvider() top: false,
..fetchTourExpensesDetails(context, widget.tourBillId), child: ChangeNotifierProvider(
child: Scaffold( create: (_) => TourExpensesDetailsProvider()
appBar: AppBar( ..fetchTourExpensesDetails(context, widget.tourBillId),
automaticallyImplyLeading: false, child: Scaffold(
backgroundColor: Color(0xFFFFFFFF), appBar: AppBar(
title: Row( automaticallyImplyLeading: false,
mainAxisAlignment: MainAxisAlignment.start, backgroundColor: Color(0xFFFFFFFF),
crossAxisAlignment: CrossAxisAlignment.center, title: Row(
children: [ mainAxisAlignment: MainAxisAlignment.start,
InkResponse( crossAxisAlignment: CrossAxisAlignment.center,
onTap: () => Navigator.pop(context, true), children: [
child: SvgPicture.asset( InkResponse(
"assets/svg/appbar_back_button.svg", onTap: () => Navigator.pop(context, true),
height: 25, child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
), ),
), SizedBox(width: 10),
SizedBox(width: 10), InkResponse(
InkResponse( onTap: () => Navigator.pop(context, true),
onTap: () => Navigator.pop(context, true), child: Text(
child: Text( "Tour Expenses",
"Tour Expenses", style: TextStyle(
style: TextStyle( fontSize: 18,
fontSize: 18, height: 1.1,
height: 1.1, fontFamily: "Plus Jakarta Sans",
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, color: AppColors.semi_black,
color: AppColors.semi_black, ),
), ),
), ),
), ],
], ),
), ),
), backgroundColor: AppColors.scaffold_bg_color,
backgroundColor: AppColors.scaffold_bg_color, body: Consumer<TourExpensesDetailsProvider>(
body: Consumer<TourExpensesDetailsProvider>( builder: (context, provider, child) {
builder: (context, provider, child) { if (provider.isLoading) {
if (provider.isLoading) { return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator()); }
}
if (provider.errorMessage != null) {
if (provider.errorMessage != null) { return Center(child: Text(provider.errorMessage!));
return Center(child: Text(provider.errorMessage!)); }
}
final response = provider.response;
final response = provider.response; if (response == null) {
if (response == null) { return const Center(child: Text("No data available"));
return const Center(child: Text("No data available")); }
} debugPrint("==================requestDetails: ${response.requestDetails?.approvalStatus}");
debugPrint("==================requestDetails: ${response.requestDetails?.approvalStatus}");
return SingleChildScrollView(
return SingleChildScrollView( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [
/// Header Card at the very top /// Header Card at the very top
_expenseHeaderCard( _expenseHeaderCard(
title: response.requestDetails?.placeOfVisit ?? "Tour", title: response.requestDetails?.placeOfVisit ?? "Tour",
date: response.tourExpenses?.fromDate ?? "-", date: response.tourExpenses?.fromDate ?? "-",
status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false) status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false)
? response.requestDetails!.approvalStatus! ? response.requestDetails!.approvalStatus!
: "No Status", : "No Status",
details: [ details: [
{"key": "TL Pending Approval Amount", "value": "-"}, {"key": "TL Pending Approval Amount", "value": "-"},
{"key": "Total Approved Amount", "value": response.tourExpenses?.appliedAmount ?? "-"}, {"key": "Total Approved Amount", "value": response.tourExpenses?.appliedAmount ?? "-"},
{"key": "Total Balance Amount", "value": "-"}, {"key": "Total Balance Amount", "value": "-"},
{"key": "HR Expiring Amount (Within 24Hrs)", "value": "-"}, {"key": "HR Expiring Amount (Within 24Hrs)", "value": "-"},
{"key": "HR Pending Approval Amount", "value": "-"}, {"key": "HR Pending Approval Amount", "value": "-"},
{"key": "Total Disbursed Amount", "value": "-"}, {"key": "Total Disbursed Amount", "value": "-"},
], ],
),
const SizedBox(height: 16),
/// Tour Expense Card (Main Summary)
if (response.requestDetails != null && response.tourExpenses != null) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Text("Tour Summary",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 16),
/// Tour Expense Card (Main Summary)
SizedBox( if (response.requestDetails != null && response.tourExpenses != null) ...[
height: 220, // adjust height to match your card const SizedBox(height: 10),
child: ListView( Padding(
scrollDirection: Axis.horizontal, padding: const EdgeInsets.only(left: 30.0),
padding: EdgeInsets.symmetric( // horizontal margin for centering child: Text("Tour Summary",
horizontal: MediaQuery.of(context).size.width * 0.05, style: TextStyle(
), fontFamily: "JakartaMedium",
children: [ fontSize: 14,
SizedBox( color: AppColors.grey_thick,
width: MediaQuery.of(context).size.width * 0.85,
child: _tourExpenseCard(
employeeName: response.requestDetails?.employeeName ?? "-",
placeOfVisit: response.requestDetails?.placeOfVisit ?? "-",
daAmount: response.tourExpenses?.da ?? "0",
totalAmount: response.tourExpenses?.appliedAmount ?? "0",
fromDate: response.tourExpenses?.fromDate ?? "-",
toDate: response.tourExpenses?.toDate ?? "-",
remarks: response.tourExpenses?.extraNote ?? "-",
),
), ),
],
),
),
],
const SizedBox(height: 10),
/// Travel Expenses Cards
if (response.travelExpenses != null &&
response.travelExpenses!.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Text(
"Travel Expenses",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
), ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8),
SizedBox(
SizedBox( height: 220, // adjust height to match your card
height: 216, child: ListView(
child: ListView.separated( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( // horizontal margin for centering
padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.05,
horizontal: MediaQuery.of(context).size.width * 0.04, ),
), children: [
itemCount: response.travelExpenses!.length, SizedBox(
separatorBuilder: (_, __) => const SizedBox(width: 12), width: MediaQuery.of(context).size.width * 0.85,
itemBuilder: (context, index) { child: _tourExpenseCard(
final t = response.travelExpenses![index]; employeeName: response.requestDetails?.employeeName ?? "-",
return SizedBox( placeOfVisit: response.requestDetails?.placeOfVisit ?? "-",
width: MediaQuery.of(context).size.width * 0.90, // card width daAmount: response.tourExpenses?.da ?? "0",
child: _travelExpenseCard( totalAmount: response.tourExpenses?.appliedAmount ?? "0",
travelType: t.travelType ?? "-", fromDate: response.tourExpenses?.fromDate ?? "-",
amount: t.fare ?? "0", toDate: response.tourExpenses?.toDate ?? "-",
from: t.froma ?? "-", remarks: response.tourExpenses?.extraNote ?? "-",
to: t.toa ?? "-", ),
onViewTap: () {
debugPrint("Open: ${t.imageDirFilePath}");
//Fileviewer(fileName: "", fileUrl: t.imageDirFilePath.toString())
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => Image.network(t.imageDirFilePath.toString()),
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
),
);
},
), ),
); ],
}, ),
), ),
) ],
],
const SizedBox(height: 10),
/// Hotel Expenses Cards
if (response.hotelExpenses != null &&
response.hotelExpenses!.isNotEmpty) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Padding( /// Travel Expenses Cards
padding: const EdgeInsets.only(left: 30.0), if (response.travelExpenses != null &&
child: Text("Hotel Expenses", response.travelExpenses!.isNotEmpty) ...[
style: TextStyle( const SizedBox(height: 10),
fontFamily: "JakartaMedium", Padding(
fontSize: 14, padding: const EdgeInsets.only(left: 30.0),
color: AppColors.grey_thick, child: Text(
"Travel Expenses",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
),
), ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8),
SizedBox(
SizedBox( height: 216,
height: 216, child: ListView.separated(
child: ListView.separated( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.04,
horizontal: MediaQuery.of(context).size.width * 0.04, ),
), itemCount: response.travelExpenses!.length,
itemCount: response.hotelExpenses!.length, separatorBuilder: (_, __) => const SizedBox(width: 12),
separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) {
itemBuilder: (context, index) { final t = response.travelExpenses![index];
final h = response.hotelExpenses![index]; return SizedBox(
return SizedBox( width: MediaQuery.of(context).size.width * 0.90, // card width
width: MediaQuery.of(context).size.width * 0.90, child: _travelExpenseCard(
child: _hotelExpenseCard( travelType: t.travelType ?? "-",
hotelName: h.hotelName ?? "-", amount: t.fare ?? "0",
amount: h.amount ?? "0", from: t.froma ?? "-",
fromDate: h.fromDate ?? "-", to: t.toa ?? "-",
toDate: h.toDate ?? "-", onViewTap: () {
onViewTap: () { debugPrint("Open: ${t.imageDirFilePath}");
debugPrint("Open: ${h.imageDirFilePath}"); //Fileviewer(fileName: "", fileUrl: t.imageDirFilePath.toString())
showDialog(
context: context, Navigator.push(
builder: (_) => Dialog( context,
shape: RoundedRectangleBorder( MaterialPageRoute(
borderRadius: BorderRadius.circular(12), builder:
(
context,
) => Fileviewer(
fileName:
t.imageDirFilePath ??
"",
fileUrl:
t.imageDirFilePath ??
"",
),
), ),
child: ClipRRect( );
borderRadius: BorderRadius.circular(12), },
child: Image.network(h.imageDirFilePath.toString()) ),
);
},
),
)
],
const SizedBox(height: 10),
/// Hotel Expenses Cards
if (response.hotelExpenses != null &&
response.hotelExpenses!.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Text("Hotel Expenses",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: 216,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.04,
),
itemCount: response.hotelExpenses!.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final h = response.hotelExpenses![index];
return SizedBox(
width: MediaQuery.of(context).size.width * 0.90,
child: _hotelExpenseCard(
hotelName: h.hotelName ?? "-",
amount: h.amount ?? "0",
fromDate: h.fromDate ?? "-",
toDate: h.toDate ?? "-",
onViewTap: () {
debugPrint("Open: ${h.imageDirFilePath}");
Navigator.push(
context,
MaterialPageRoute(
builder:
(
context,
) => Fileviewer(
fileName:
h.imageDirFilePath ??
"",
fileUrl:
h.imageDirFilePath ??
"",
),
), ),
), );
); },
}, ),
), );
); },
}, ),
), ),
), ],
],
const SizedBox(height: 10),
/// Other Expenses Cards
if (response.otherExpenses != null &&
response.otherExpenses!.isNotEmpty) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 30.0), /// Other Expenses Cards
child: Text("Other Expenses", if (response.otherExpenses != null &&
style: TextStyle( response.otherExpenses!.isNotEmpty) ...[
fontFamily: "JakartaMedium", const SizedBox(height: 10),
fontSize: 14, Padding(
color: AppColors.grey_thick, padding: const EdgeInsets.only(left: 30.0),
child: Text("Other Expenses",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
),
), ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8),
SizedBox(
SizedBox( height: 216,
height: 216, child: ListView.separated(
child: ListView.separated( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.04,
horizontal: MediaQuery.of(context).size.width * 0.04, ),
), itemCount: response.otherExpenses!.length,
itemCount: response.otherExpenses!.length, separatorBuilder: (_, __) => const SizedBox(width: 12),
separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) {
itemBuilder: (context, index) { final o = response.otherExpenses![index];
final o = response.otherExpenses![index]; return SizedBox(
return SizedBox( width: MediaQuery.of(context).size.width * 0.90,
width: MediaQuery.of(context).size.width * 0.90, child: _otherExpenseCard(
child: _otherExpenseCard( description: o.otherDesc ?? "-",
description: o.otherDesc ?? "-", amount: o.otherAmount ?? "0",
amount: o.otherAmount ?? "0", date: o.otherDate ?? "-",
date: o.otherDate ?? "-", onViewTap: () {
onViewTap: () { debugPrint("Open: ${o.imageDirFilePath}");
debugPrint("Open: ${o.imageDirFilePath}"); Navigator.push(
showDialog( context,
context: context, MaterialPageRoute(
builder: (_) => Dialog( builder:
shape: RoundedRectangleBorder( (
borderRadius: BorderRadius.circular(12), context,
), ) => Fileviewer(
child: ClipRRect( fileName:
borderRadius: BorderRadius.circular(12), o.imageDirFilePath ??
child: Image.network(o.imageDirFilePath.toString()) "",
fileUrl:
o.imageDirFilePath ??
"",
),
), ),
), );
); },
}, ),
), );
); },
}, ),
), ),
), ],
],
const SizedBox(height: 25), const SizedBox(height: 25),
], ],
), ),
); );
}, },
),
), ),
), ),
); );
...@@ -334,13 +351,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ ...@@ -334,13 +351,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.grey.withOpacity(0.1), // color: Colors.grey.withOpacity(0.1),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(0, 3), // offset: const Offset(0, 3),
) // )
], // ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
...@@ -407,13 +424,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ ...@@ -407,13 +424,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.grey.withOpacity(0.1), // color: Colors.grey.withOpacity(0.1),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(0, 3), // offset: const Offset(0, 3),
) // )
], // ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
...@@ -477,13 +494,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ ...@@ -477,13 +494,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.grey.withOpacity(0.1), // color: Colors.grey.withOpacity(0.1),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(0, 3), // offset: const Offset(0, 3),
) // )
], // ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
...@@ -548,13 +565,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ ...@@ -548,13 +565,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.grey.withOpacity(0.1), // color: Colors.grey.withOpacity(0.1),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(0, 3), // offset: const Offset(0, 3),
) // )
], // ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
...@@ -681,7 +698,7 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{ ...@@ -681,7 +698,7 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
bottomRight: Radius.circular(30), bottomRight: Radius.circular(30),
), ),
), ),
elevation: 2, elevation: 0,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
......
...@@ -168,38 +168,36 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> { ...@@ -168,38 +168,36 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
], ],
), ),
floatingActionButtonLocation: bottomNavigationBar: Container(
FloatingActionButtonLocation.centerFloat, padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
floatingActionButton: InkResponse( color: Colors.white,
onTap: () { child: ElevatedButton(
HapticFeedback.selectionClick(); style: ElevatedButton.styleFrom(
Navigator.push( backgroundColor: const Color(0xff1487c9), // App blue
context, shape: RoundedRectangleBorder(
MaterialPageRoute( borderRadius: BorderRadius.circular(15),
builder: (context) =>
const AddBillScreen(pageTitleName: "Add Bill",),
settings: const RouteSettings(
name: 'AddTourExpBillScreen'),
), ),
).then((_) { padding: const EdgeInsets.symmetric(vertical: 14),
provider.fetchTourExpenses(context, "1"); elevation: 0, // Optional: remove shadow
});
// 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),
), ),
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", "Add Bill",
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 16,
fontFamily: "JakartaMedium", fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: Colors.white, color: Colors.white,
), ),
), ),
......
...@@ -5048,7 +5048,7 @@ class ApiCalling { ...@@ -5048,7 +5048,7 @@ class ApiCalling {
} }
} }
static Future<CommonResponse?> attendanceRequestRejectAPI( static Future<CommonResponse?> attendanceRequestApproveRejectAPI(
session, session,
empId, empId,
mode, mode,
...@@ -5067,7 +5067,7 @@ class ApiCalling { ...@@ -5067,7 +5067,7 @@ class ApiCalling {
}; };
final res = await post(data, AttendanceRequestRejectUrl, {}); final res = await post(data, AttendanceRequestRejectUrl, {});
if (res != null) { if (res != null) {
print(data); print("Attendance App Reje:${data}");
debugPrint(res.body); debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body)); return CommonResponse.fromJson(jsonDecode(res.body));
} else { } else {
...@@ -5368,6 +5368,7 @@ class ApiCalling { ...@@ -5368,6 +5368,7 @@ class ApiCalling {
'requested_date_from': (dateFrom), 'requested_date_from': (dateFrom),
'requested_date_to': (dateTo), 'requested_date_to': (dateTo),
'mode': (mode), 'mode': (mode),
}; };
final res = await post(data, LeaveApplicationListUrl, {}); final res = await post(data, LeaveApplicationListUrl, {});
if (res != null) { if (res != null) {
...@@ -5463,7 +5464,7 @@ class ApiCalling { ...@@ -5463,7 +5464,7 @@ class ApiCalling {
'remarks': (remarks).toString(), 'remarks': (remarks).toString(),
'id': (id).toString(), 'id': (id).toString(),
}; };
final res = await post(data, AttendanceRequestRejectUrl, {}); final res = await post(data, LeaveRequestRejectAprroveUrl, {});
if (res != null) { if (res != null) {
print(data); print(data);
debugPrint(res.body); debugPrint(res.body);
......
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