......@@ -55,12 +55,36 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
}
Future<void> _autoFetchLocation() async {
String loc = await getCurrentLocation();
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
// Save raw coordinates separately (for submission)
final coords = "${position.latitude},${position.longitude}";
// Convert to address for display
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
String displayAddress;
if (placemarks.isNotEmpty) {
final place = placemarks.first;
displayAddress =
"${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.country}";
} else {
displayAddress = coords; // fallback
}
setState(() {
locationController.text = loc;
locationController.text = displayAddress; // what user sees
_rawCoordinates = coords; // keep coords hidden for backend
});
}
// Add this field at the top of your State class:
String? _rawCoordinates;
Future<String> getCurrentLocation() async {
try {
LocationPermission permission = await Geolocator.checkPermission();
......@@ -164,7 +188,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
context,
process: "Live",
type: selectedType ?? "",
loc: locationController.text,
loc: _rawCoordinates ?? "", // send actual coordinates
checkDate: DateTime.now().toString().split(" ").first,
checkInTime:
selectedType == "Check In" ? TimeOfDay.now().format(context) : null,
......@@ -198,210 +222,213 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
automaticallyImplyLeading: false,
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
const SizedBox(width: 10),
Text(
"Add Live Attendance",
style: TextStyle(
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Type Dropdown
const Text("Type",
const SizedBox(width: 10),
Text(
"Add Live Attendance",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400),
),
value: selectedType,
items: types
.map((e) => DropdownMenuItem<String>(
value: e,
child: Text(
e,
style: const TextStyle(
fontSize: 14, fontFamily: "JakartaMedium"),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Type Dropdown
const Text("Type",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400),
),
))
.toList(),
onChanged: (val) => setState(() => selectedType = val),
iconStyleData: ddtheme.iconStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
value: selectedType,
items: types
.map((e) => DropdownMenuItem<String>(
value: e,
child: Text(
e,
style: const TextStyle(
fontSize: 14, fontFamily: "JakartaMedium"),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (val) => setState(() => selectedType = val),
iconStyleData: ddtheme.iconStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
),
),
if (typeError != null) ...[
const SizedBox(height: 4),
Text(typeError!,
if (typeError != null) ...[
const SizedBox(height: 4),
Text(typeError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16),
/// Location
Text(locationHeading,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: locationController,
decoration: _inputDecoration("Enter location"),
),
if (locationError != null) ...[
const SizedBox(height: 4),
Text(locationError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16),
/// Location
Text(locationHeading,
style: const TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: locationController,
decoration: _inputDecoration("Enter location"),
),
if (locationError != null) ...[
const SizedBox(height: 4),
Text(locationError!,
const SizedBox(height: 16),
/// Description
Text(descriptionHeading,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: _inputDecoration("Write Description"),
),
const SizedBox(height: 16),
/// Description
Text(descriptionHeading,
style: const TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: descriptionController,
maxLines: 3,
decoration: _inputDecoration("Write Description"),
),
const SizedBox(height: 20),
/// Attach Proof
InkResponse(
onTap: () => _showPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(14),
const SizedBox(height: 20),
/// Attach Proof
InkResponse(
onTap: () => _showPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
proofButtonText,
style: const TextStyle(
fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
),
),
),
),
child: Center(
child: Text(
proofButtonText,
),
if (proofError != null) ...[
const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
if (proofFile != null) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text("Attached: ${proofFile!.name}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => setState(() => proofFile = null),
),
],
)
],
const SizedBox(height: 24),
/// Submit Button
InkResponse(
onTap:
isSubmitEnabled && !isSubmitting ? () => submitAttendance(context) : null,
child: Container(
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSubmitEnabled
? AppColors.app_blue
: Colors.grey.shade400,
borderRadius: BorderRadius.circular(12),
),
child: isSubmitting
? const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)
: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
),
if (proofError != null) ...[
const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
if (proofFile != null) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text("Attached: ${proofFile!.name}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => setState(() => proofFile = null),
),
],
)
],
const SizedBox(height: 24),
/// Submit Button
InkResponse(
onTap:
isSubmitEnabled && !isSubmitting ? () => submitAttendance(context) : null,
child: Container(
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSubmitEnabled
? AppColors.app_blue
: Colors.grey.shade400,
borderRadius: BorderRadius.circular(12),
),
child: isSubmitting
? const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)
: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
),
),
],
),
),
),
);
......
......@@ -168,7 +168,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
}
void _submitForm(BuildContext context) async {
// reset errors first
// Reset errors first
dateError = null;
typeError = null;
checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null;
......@@ -176,15 +176,27 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// --- Date Validation ---
// --- Date Validation (allow today, yesterday, day before yesterday) ---
if (provider.dateController.text.isEmpty) {
dateError = "Please select a date";
} else {
try {
final enteredDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
provider.setSelectedDate(enteredDate);
if (!provider.isDateValid()) {
dateError = "Date must be today or yesterday";
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
final dayBeforeYesterday = today.subtract(const Duration(days: 2));
// Normalize dates (ignore time part)
bool isValid = enteredDate.year == today.year &&
enteredDate.month == today.month &&
(enteredDate.day == today.day ||
enteredDate.day == yesterday.day ||
enteredDate.day == dayBeforeYesterday.day);
if (!isValid) {
dateError = "Date must be today, yesterday, or the day before yesterday";
}
} catch (e) {
dateError = "Invalid date format (use dd MMM yyyy)";
......@@ -225,7 +237,19 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
checkOutDescError,
checkOutProofError
].any((e) => e != null)) {
setState(() {});
setState(() {}); // refresh UI to show error messages
return;
}
// --- Format date for server ---
String formattedDate = "";
try {
final parsedDate = DateFormat("dd MMM yyyy").parse(provider.dateController.text);
formattedDate = DateFormat("yyyy-MM-dd").format(parsedDate);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error formatting date: $e")),
);
return;
}
......@@ -268,7 +292,7 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
: selectedType == "Check Out"
? checkOutLocation.text
: "${checkInLocation.text}, ${checkOutLocation.text}",
checkDate: provider.dateController.text,
checkDate: formattedDate,
checkInTime: finalCheckInTime,
checkInLoc: finalCheckInLoc,
checkInProof: finalCheckInProof,
......@@ -278,14 +302,13 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
note: finalNote,
);
// Check the response from provider
// --- Response handling ---
if (provider.addResponse != null && provider.addResponse!.error == "0") {
// Success case
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(provider.addResponse!.message ?? "Attendance Submitted Successfully")),
);
// --- Reset fields ---
// Reset fields
setState(() {
selectedType = null;
provider.dateController.clear();
......@@ -301,19 +324,20 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
_fetchInitialLocation();
} else {
// Error case - show appropriate message
String errorMessage = provider.errorMessage ?? "Failed to submit attendance";
// Handle specific server error for Check Out without Check In
if (errorMessage.contains("Check In is not Available")) {
errorMessage = "Cannot submit Check Out without a Check In record for this date";
}
if (errorMessage.contains("2")){
errorMessage = "Only One manual Request can be added in a month !";
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
);
}
}
// it's date picker need to take day before yesterday, yesterday and today
......
......@@ -12,8 +12,9 @@ import '../finance/FileViewer.dart';
/// screen for attendance details
class AttendanceRequestDetailScreen extends StatefulWidget {
final String mode;
final attendanceListId;
const AttendanceRequestDetailScreen({super.key, required this.attendanceListId});
const AttendanceRequestDetailScreen({super.key, required this.attendanceListId, required this.mode});
@override
State<AttendanceRequestDetailScreen> createState() =>
......@@ -22,228 +23,527 @@ class AttendanceRequestDetailScreen extends StatefulWidget {
class _AttendanceRequestDetailScreenState
extends State<AttendanceRequestDetailScreen> {
bool _actionSubmitted = false;
late AttendanceDetailsProvider provider;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) =>
AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId),
child: Consumer<AttendanceDetailsProvider>(
builder: (context, provider, child) {
// Get screen dimensions for responsive scaling
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
// Scale factors based on screen size
final scaleFactor = screenWidth / 360; // Base width for scaling
final textScaleFactor = MediaQuery.of(context).textScaleFactor.clamp(1.0, 1.2);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25 * scaleFactor,
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) =>
AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId),
child: Consumer<AttendanceDetailsProvider>(
builder: (context, provider, child) {
// Get screen dimensions for responsive scaling
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
// Scale factors based on screen size
final scaleFactor = screenWidth / 360; // Base width for scaling
final textScaleFactor = MediaQuery.of(context).textScaleFactor.clamp(1.0, 1.2);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25 * scaleFactor,
),
),
),
SizedBox(width: 10 * scaleFactor),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Attendance Details",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
SizedBox(width: 10 * scaleFactor),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Attendance Details",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
),
),
],
],
),
),
),
backgroundColor: Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue,));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestDetails == null) {
return const Center(child: Text("No details found"));
}
final details = provider.response!.requestDetails!;
/// scr
return SingleChildScrollView(
padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scaleFactor),
),
elevation: 2,
child: Padding(
padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
padding: EdgeInsets.all(12 * scaleFactor),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12 * scaleFactor),
),
child: Row(
children: [
/// Left Avatar
Container(
height: 48 * scaleFactor,
width: 48 * scaleFactor,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 28 * scaleFactor,
width: 28 * scaleFactor,
"assets/svg/hrm/attendanceList.svg",
fit: BoxFit.contain,
backgroundColor: Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue,));
}
// if (provider.errorMessage != null) {
// return Center(child: Text(provider.errorMessage!));
// }
if (provider.response?.requestDetails == null) {
return const Center(child: Text("No details found"));
}
final details = provider.response!.requestDetails!;
/// scr
return SingleChildScrollView(
padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scaleFactor),
),
elevation: 0,
child: Padding(
padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
padding: EdgeInsets.symmetric(horizontal: 2.5 * scaleFactor, vertical: 12 * scaleFactor),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12 * scaleFactor),
),
child: Row(
children: [
/// Left Avatar
Container(
height: 44 * scaleFactor,
width: 44 * scaleFactor,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 24 * scaleFactor,
width: 24 * scaleFactor,
"assets/svg/hrm/attendanceList.svg",
fit: BoxFit.contain,
),
),
),
),
SizedBox(width: 12 * scaleFactor),
/// Middle text
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
details.type ?? "-",
style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle:
TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
SizedBox(width: 12 * scaleFactor),
/// Middle text
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
details.type ?? "-",
style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle:
TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
SizedBox(height: 2 * scaleFactor),
Text(
details.date ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.app_blue,
SizedBox(height: 2 * scaleFactor),
Text(
details.date ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.app_blue,
),
),
),
],
],
),
),
),
/// Right side (Live/Manual)
Container(
height: 30 * scaleFactor,
padding: EdgeInsets.symmetric(
horizontal: 12 * scaleFactor,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6 * scaleFactor),
color: getDecorationColor(details.status)
),
child: Center(
child: Text(
details.status ?? "-",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: getTextColor(details.status.toString()),
/// Right side (Live/Manual)
Container(
height: 30 * scaleFactor,
padding: EdgeInsets.symmetric(
horizontal: 12 * scaleFactor,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6 * scaleFactor),
color: getDecorationColor(details.status)
),
child: Center(
child: Text(
details.status ?? "-",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: getTextColor(details.status.toString()),
),
),
),
),
),
],
],
),
),
),
// Employee Details
_buildSectionHeader("Employee Details", scaleFactor),
_buildDetailTile("Employee Name", details.employeeName, scaleFactor),
_buildDetailTile("Created Employee", details.createdEmpName, scaleFactor),
// Check In/Out
_buildSectionHeader("Check In/Out Details", scaleFactor),
_buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime, scaleFactor),
_buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime, scaleFactor),
_buildDetailTile("Original Check In", details.checkInTime, scaleFactor),
_buildDetailTile("Original Check Out", "--", scaleFactor),
_buildDetailTile("Original Check In Location", details.checkInLocation, scaleFactor),
_buildDetailTile("Original Check Out Location", details.checkOutLocation, scaleFactor),
buildLocationTile("Location", details.location, scaleFactor),
// Proofs
if ((details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) ||
(details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)) ...[
_buildSectionHeader("Proofs", scaleFactor),
if (details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty)
_buildProofLink(context, "Check In Proof", details.checkInProofDirFilePath, scaleFactor),
if (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)
_buildProofLink(context, "Check Out Proof", details.checkOutProofDirFilePath, scaleFactor),
],
// Employee Details
_buildSectionHeader("Employee Details", scaleFactor),
_buildDetailTile("Employee Name", details.employeeName, scaleFactor),
_buildDetailTile("Created Employee", details.createdEmpName, scaleFactor),
// Check In/Out
_buildSectionHeader("Check In/Out Details", scaleFactor),
_buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime, scaleFactor),
_buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime, scaleFactor),
_buildDetailTile("Original Check In", details.checkInTime, scaleFactor),
_buildDetailTile("Original Check Out", "--", scaleFactor),
_buildDetailTile("Original Check In Location", details.checkInLocation, scaleFactor),
_buildDetailTile("Original Check Out Location", details.checkOutLocation, scaleFactor),
buildLocationTile("Location", details.location, 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),
],
// Proofs
if ((details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) ||
(details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)) ...[
_buildSectionHeader("Proofs", scaleFactor),
if (details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty)
_buildProofLink(context, "Check In Proof", details.checkInProofDirFilePath, scaleFactor),
if (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)
_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),
],
),
),
),
),
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,
child: Column(
children: [
Row(
children: [
/// Reject Button
Expanded(
child: InkWell(
onTap: () {
showRemarkSheet(
context: context,
actionType: "Reject",
onSubmit: (remark) {
provider.rejectApproveAttendanceRequest(
context,
mode: widget.mode,
type: "Rejected",
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_reject_ic.svg"),
const SizedBox(width: 6),
const Text("Reject"),
],
),
),
),
),
/// Vertical Divider
Container(
width: 1,
height: 45,
color: Colors.grey.shade300,
),
/// Approve Button
Expanded(
child: InkWell(
onTap: () {
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(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
},
)
),
);
}
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",
),
),
),
),
),
),
],
),
],
),
),
),
);
},
)
);
},
);
}
/// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value, double scaleFactor) {
return Padding(
......@@ -285,20 +585,23 @@ class _AttendanceRequestDetailScreenState
/// for location
/// Location Tile
Widget buildLocationTile(String label, String? value, double scaleFactor) {
return FutureBuilder<String>(
future: getReadableLocation(value),
builder: (context, snapshot) {
final locationText = snapshot.data ?? "-";
final locationText = snapshot.connectionState == ConnectionState.done
? (snapshot.data ?? value ?? "-")
: value ?? "-";
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // aligns top when wrapping
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Expanded(
flex: 5, // ratio (adjust same as your Date/Time tile)
flex: 5,
child: Text(
label,
style: TextStyle(
......@@ -311,14 +614,13 @@ class _AttendanceRequestDetailScreenState
// Value (Clickable Location)
Expanded(
flex: 5, // take remaining space
flex: 5,
child: GestureDetector(
onTap: () async {
final uri = Uri.parse(
"https://www.google.com/maps/search/?api=1&query=$value");
if (await canLaunchUrl(uri)) {
await launchUrl(uri,
mode: LaunchMode.externalApplication);
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
child: Text(
......@@ -341,20 +643,40 @@ class _AttendanceRequestDetailScreenState
);
}
/// Convert coordinates -> human readable location
/// Convert coordinates -> full human readable location
Future<String> getReadableLocation(String? value) async {
if (value == null) return "-";
if (value == null || value.isEmpty) return "-";
try {
List<Location> locations = await locationFromAddress(value);
List<Placemark> placemarks = await placemarkFromCoordinates(
locations[0].latitude,
locations[0].longitude,
);
return placemarks.first.locality ?? value;
// Expecting "lat,lng"
final parts = value.split(',');
if (parts.length != 2) return value;
final lat = double.tryParse(parts[0].trim());
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) {
return value; // fallback to raw coordinates
}
}
/// for date and time
Widget _buildDate_TimeTile(String label, String? date, String? time, double scaleFactor) {
return Padding(
......@@ -450,8 +772,16 @@ class _AttendanceRequestDetailScreenState
context,
MaterialPageRoute(
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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_svg/svg.dart';
import 'package:generp/Notifiers/hrmProvider/AttendanceDetailsProvider.dart';
import 'package:generp/Utils/GlobalConstants.dart';
import 'package:generp/screens/hrm/AddManualAttendance.dart';
import 'package:generp/screens/hrm/AttendanceRequestDetail.dart';
import 'package:provider/provider.dart';
import '../../Notifiers/HomeScreenNotifier.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart';
......@@ -13,14 +16,15 @@ import '../CommonFilter2.dart';
import '../commonDateRangeFilter.dart';
import 'AddLiveAttendance.dart';
class Attendancelist extends StatefulWidget {
const Attendancelist({super.key});
class AttendanceListScreen extends StatefulWidget {
final mode;
const AttendanceListScreen({super.key,required this.mode});
@override
State<Attendancelist> createState() => _AttendancelistState();
State<AttendanceListScreen> createState() => _AttendanceListScreenState();
}
class _AttendancelistState extends State<Attendancelist> {
class _AttendanceListScreenState extends State<AttendanceListScreen> {
// @override
// void initState() {
// super.initState();
......@@ -32,19 +36,25 @@ class _AttendancelistState extends State<Attendancelist> {
@override
Widget build(BuildContext context) {
String _truncate(String text, int maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength).trim() + '...';
}
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) {
final provider = Attendancelistprovider();
Future.microtask(() {
provider.fetchAttendanceRequests(context);
provider.fetchAttendanceRequests(context, widget.mode);
});
return provider;
},
builder: (context, child) {
return Consumer<Attendancelistprovider>(
builder: (context, provider, child) {
final requestProvider = Provider.of<AttendanceDetailsProvider>(context, listen: false);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
......@@ -79,6 +89,7 @@ class _AttendancelistState extends State<Attendancelist> {
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
provider.updateFiltersFromSheet(
widget.mode,
context,
type: result['type'] ?? "All",
selectedValue: result['selectedValue'] ?? "This Month",
......@@ -132,9 +143,9 @@ class _AttendancelistState extends State<Attendancelist> {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
// if (provider.errorMessage != null) {
// return Center(child: Text(provider.errorMessage!));
// }
if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) {
return const Center(
......@@ -152,104 +163,221 @@ class _AttendancelistState extends State<Attendancelist> {
itemBuilder: (context, index) {
final item = list[index];
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
/// navigation flow
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttendanceRequestDetailScreen(
attendanceListId: item.id,
),
final canSwipe = widget.mode == "apr_lvl1" &&
item.status != "Level 1 Approved" &&
item.status != "Rejected";
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
return Slidable(
key: ValueKey(item.id),
// Left swipe (Reject)
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(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
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(
color: _getTextColor(item.status),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
)
: null,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttendanceRequestDetailScreen(
attendanceListId: item.id,
mode: widget.mode,
),
),
const SizedBox(width: 10),
/// Middle Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.type ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
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(
fontFamily: "JakartaRegular",
color: _getTextColor(item.status),
fontSize: 14,
color: AppColors.semi_black,
fontWeight: FontWeight.bold,
),
),
Text(
item.date ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
const SizedBox(width: 10),
/// Middle Section
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)
Text(
item.attendanceType ?? "-",
textAlign: TextAlign.right,
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: (item.attendanceType ?? "").toLowerCase() == "live"
? Colors.green
: Colors.orange,
/// Right Status (Live / Manual)
Text(
item.attendanceType ?? "-",
textAlign: TextAlign.right,
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: (item.attendanceType ?? "").toLowerCase() == "live"
? Colors.green
: Colors.orange,
),
),
),
],
],
),
),
),
);
},
);
},
),
)
],
),
bottomNavigationBar: Container(
bottomNavigationBar: widget.mode == "apr_lvl1"
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
alignment: Alignment.bottomCenter,
height: 54,
decoration: const BoxDecoration(color: Colors.white),
height: 61,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffFFFFFF),
Color(0x00FFFFFF),
],
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
......@@ -266,7 +394,7 @@ class _AttendancelistState extends State<Attendancelist> {
),
),
).then((_) {
provider.fetchAttendanceRequests(context);
provider.fetchAttendanceRequests(context, widget.mode);
});
},
child: Row(
......@@ -294,7 +422,7 @@ class _AttendancelistState extends State<Attendancelist> {
name: 'AddManualAttendanceScreen'),
),
).then((_) {
provider.fetchAttendanceRequests(context);
provider.fetchAttendanceRequests(context, widget.mode);
});
},
child: Row(
......@@ -311,6 +439,7 @@ class _AttendancelistState extends State<Attendancelist> {
],
),
),
);
},
......@@ -320,6 +449,190 @@ class _AttendancelistState extends State<Attendancelist> {
);
}
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
Color _getAvatarColor(value) {
......
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:generp/screens/hrm/OrganizationStructureScreen.dart';
import 'package:generp/screens/hrm/RewardListScreen.dart';
import 'package:generp/screens/hrm/Attendancelist.dart';
import 'package:provider/provider.dart';
import '../../Utils/app_colors.dart';
import 'AttendanceRequestDetail.dart';
import 'LeaveApplicationScreen.dart';
import 'TourExpensesListScreen.dart';
import 'attendancelist.dart';
import 'RewardListScreen.dart';
import 'OrganizationStructureScreen.dart';
import '../../Notifiers/hrmProvider/hrmAccessiblePagesProvider.dart';
import 'oggchart.dart';
class HrmdashboardScreen extends StatefulWidget {
......@@ -17,17 +19,32 @@ class HrmdashboardScreen extends StatefulWidget {
}
class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
final allowedPages = [
"Team Leave Request Approval",
"Team Attendance Approval",
"Leave Request List",
"Tour Bill List",
"Rewards List",
"Attendance Request List",
];
@override
void initState() {
super.initState();
Future.microtask(() =>
Provider.of<HrmAccessiblePagesProvider>(context, listen: false)
.fetchAccessiblePages(context));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFCEEDFF),
// elevation: 2.0,
title: SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
return SafeArea(
top: false,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFCEEDFF),
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
......@@ -37,298 +54,368 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
),
),
const SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"HRM",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
Text(
"HRM",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
],
),
),
),
backgroundColor: Color(0xffF6F6F8),
body: SingleChildScrollView(
child: Column(
children: [
/// Background elements
Stack(
children: [
Container(
width: double.infinity,
height: 490,
color: const Color(0xffF6F6F8),
),
Container(
width: double.infinity,
height: 490,
padding: const EdgeInsets.only(top: 1, bottom: 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFFCEEDFF),
Color(0xFFf9f9fb),
Color(0xffF6F6F8)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 1, bottom: 30),
child: Image.asset(
"assets/images/vector.png",
height: 230,
backgroundColor: const Color(0xffF6F6F8),
body: SingleChildScrollView(
child: Column(
children: [
/// Background
Stack(
children: [
Container(
width: double.infinity,
fit: BoxFit.fitWidth,
height: 490,
color: const Color(0xffF6F6F8),
),
),
Column(
children: [
/// Top Section with Gradient
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 60, bottom: 30),
child: Column(
children: [
/// Illustration
SvgPicture.asset(
"assets/images/capa.svg",
height: 146,
width: 400,
fit: BoxFit.contain,
),
const SizedBox(height: 32),
/// Organization Structure Button
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),
],
),
),
),
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,
),
),
/// Bottom Grid Section
// Bottom Grid Section
LayoutBuilder(
builder: (context, constraints) {
final itemWidth = 180.0; // Fixed desired width for each item
final availableWidth = constraints.maxWidth;
final crossAxisCount = (availableWidth / itemWidth).floor().clamp(2, 4);
return Padding(
padding: const EdgeInsets.all(14),
child: GridView.count(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 8.5,
mainAxisSpacing: 16,
childAspectRatio: 1.7,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildTile(
label: "Attendance List",
subtitle: "Real-time request",
assetIcon: "assets/svg/hrm/attendanceList.svg",
txtColor: const Color(0xff1487C9),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Attendancelist(),
),
);
},
),
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 1, bottom: 30),
child: Image.asset(
"assets/images/vector.png",
height: 230,
width: double.infinity,
fit: BoxFit.fitWidth,
),
),
/// Content
Column(
children: [
/// Top Illustration & Button
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 60, bottom: 30),
child: Column(
children: [
SvgPicture.asset(
"assets/images/capa.svg",
height: 146,
width: 400,
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFF1487C9), // border color
width: 1.2, // thickness of the border
),
color: const Color(0xffEDF8FF),
borderRadius: BorderRadius.circular(30),
),
_buildTile(
label: "Leave Application",
subtitle: "Apply & Track",
assetIcon: "assets/svg/hrm/leaveApplication.svg",
txtColor: const Color(0xff1487C9),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LeaveApplicationListScreen(),
builder: (context) => OrgChartt(),
),
);
},
),
_buildTile(
label: "Rewards List",
subtitle: "Track earned rewards",
assetIcon: "assets/svg/hrm/rewardList.svg",
txtColor: const Color(0xff1487C9),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RewardListScreen(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
"assets/svg/hrm/groupIc.svg",
height: 29,
width: 29,
fit: BoxFit.contain,
),
);
},
),
_buildTile(
label: "Tour Expenses",
subtitle: "Submit and manage claims",
assetIcon: "assets/svg/hrm/tourExp.svg",
txtColor: const Color(0xff1487C9),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TourExpensesListScreen(),
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 ?? "",
),
);
},
);
},
),
);
},
),
],
),
],
),
],
),
),
),
);
}
/// Reusable Tile Widget (Row style)
/// Reusable Tile Widget (Row style) - Updated to match design
/// Card builder
Widget _buildTile({
required String label,
required String subtitle,
required String assetIcon, // SVG/PNG asset
required String assetIcon,
required Color txtColor,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 15,
),
margin: EdgeInsets.symmetric(
vertical: 7,
horizontal: 5,
),
decoration: BoxDecoration(
color: Colors.white,
return LayoutBuilder(
builder: (context, constraints) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// Left side text
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: AppColors.grey_semi,
fontFamily: "JakartaMedium",
),
),
],
),
child: Container(
padding: EdgeInsets.symmetric(
vertical: constraints.maxHeight * 0.05,
horizontal: constraints.maxWidth * 0.05,
),
margin: const EdgeInsets.symmetric(vertical: 7, horizontal: 5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
const SizedBox(height: 4),
Flexible(
child: Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: AppColors.grey_semi,
fontFamily: "JakartaMedium",
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
],
SizedBox(width: 10),
/// Right side icon (SVG/PNG)
Expanded(
flex: 1,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg
),
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
assetIcon,
fit: BoxFit.contain,
Expanded(
flex: 1,
child: Container(
height: constraints.maxHeight * 0.39,
width: constraints.maxHeight * 0.39,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF),
),
child: Center(
child: SvgPicture.asset(
assetIcon,
height: constraints.maxHeight * 0.19,
width: constraints.maxHeight * 0.19,
),
),
),
),
),
],
),
],
),
),
),
);
},
);
}
}
\ No newline at end of file
/// Mapping subtitles
String _getSubtitle(String pageName) {
switch (pageName) {
case "Attendance Request List":
return "Real-time request";
case "Leave Request List":
return "Apply & Track";
case "Rewards List":
return "Track earned rewards";
case "Tour Bill List":
return "Submit and manage claims";
case "Team Leave Request Approval":
return "";
case "Team Attendance Approval":
return "";
default:
return "";
}
}
/// Mapping icons
String _getIcon(String pageName) {
switch (pageName) {
case "Attendance Request List":
return "assets/svg/hrm/attendanceList.svg";
case "Leave Request List":
return "assets/svg/hrm/leaveApplication.svg";
case "Rewards List":
return "assets/svg/hrm/rewardList.svg";
case "Tour Bill List":
return "assets/svg/hrm/tourExp.svg";
case "Team Leave Request Approval":
return "assets/svg/hrm/leaveApplication.svg";
case "Team Attendance Approval":
return "assets/svg/hrm/attendanceList.svg";
default:
return "assets/svg/hrm/groupIc.svg";
}
}
/// Navigation mapping
void _handleNavigation(
BuildContext context,
String pageName,
String mode,
) {
switch (pageName) {
case "Attendance Request List":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttendanceListScreen(mode: mode),
),
);
break;
case "Leave Request List":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationListScreen(
mode: mode,
),
),
);
break;
case "Rewards List":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RewardListScreen(),
),
);
break;
case "Tour Bill List":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TourExpensesListScreen(),
),
);
break;
case "Team Leave Request Approval":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationListScreen(
mode: mode,
),
),
);
break;
case "Team Attendance Approval":
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttendanceListScreen(mode: mode),
),
);
break;
}
}
}
......@@ -12,235 +12,575 @@ import '../finance/FileViewer.dart';
/// Screen for leave application details
class LeaveApplicationDetailScreen extends StatefulWidget {
final String leaveRequestId;
const LeaveApplicationDetailScreen({super.key, required this.leaveRequestId});
final String mode;
const LeaveApplicationDetailScreen({super.key, required this.leaveRequestId, required this.mode});
@override
State<LeaveApplicationDetailScreen> createState() => _LeaveApplicationDetailScreenState();
}
class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScreen> {
bool _actionSubmitted = false;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LeaveApplicationDetailsProvider()..fetchLeaveApplicationDetails(context, widget.leaveRequestId),
child: Consumer<LeaveApplicationDetailsProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) => LeaveApplicationDetailsProvider()..fetchLeaveApplicationDetails(context, widget.leaveRequestId),
child: Consumer<LeaveApplicationDetailsProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
const SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Leave Application Details",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
const SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Leave Application Details",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
),
),
],
],
),
),
),
backgroundColor: const Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestDetails == null) {
return const Center(child: Text("No details found"));
}
final details = provider.response!.requestDetails!;
/// Screen content
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header with status
Container(
margin: const EdgeInsets.only(bottom: 0.5),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 2),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
/// Left Avatar
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 28,
width: 28,
"assets/svg/hrm/leaveApplication.svg", // Use appropriate icon
fit: BoxFit.contain,
backgroundColor: const Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.response?.requestDetails == null) {
return const Center(child: Text("No details found"));
}
// Get screen dimensions for responsive scaling
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
// Scale factors based on screen size
final scaleFactor = screenWidth / 360; // Base width for scaling
final details = provider.response!.requestDetails!;
/// Screen content
return SingleChildScrollView(
padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scaleFactor),
),
elevation: 0,
child: Padding(
padding: EdgeInsets.all(10.0 * scaleFactor),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header with status
Container(
margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
padding: EdgeInsets.symmetric(
vertical: 10 * scaleFactor,
horizontal: 2 * scaleFactor,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12 * scaleFactor),
),
child: Row(
children: [
/// Left Avatar
Container(
height: 48 * scaleFactor,
width: 48 * scaleFactor,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF),
),
child: Center(
child: SvgPicture.asset(
"assets/svg/hrm/leaveApplication.svg",
height: 28 * scaleFactor,
width: 28 * scaleFactor,
fit: BoxFit.contain,
),
),
),
),
const SizedBox(width: 12),
/// Middle text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
details.leaveType ?? "-",
style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle:
TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
SizedBox(width: 12 * scaleFactor),
/// Middle text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
details.leaveType ?? "-",
style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 12 * scaleFactor,
color: AppColors.semi_black,
),
),
),
const SizedBox(height: 2),
Text(
"Applied: ${details.appliedDate ?? "-"}",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.app_blue,
SizedBox(height: 2 * scaleFactor),
Text(
"Applied: ${details.appliedDate ?? "-"}",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 12 * scaleFactor,
color: AppColors.app_blue,
),
),
),
],
],
),
),
),
/// Right side status badge
Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: _getStatusBackgroundColor(details.status),
),
child: Center(
child: Text(
details.status ?? "-",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 12,
color: _getStatusTextColor(details.status),
/// Right side status badge
Container(
height: 28 * scaleFactor,
padding: EdgeInsets.symmetric(
horizontal: 5 * scaleFactor,
vertical: 1 * scaleFactor,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8 * scaleFactor),
color: _getStatusBackgroundColor(details.status),
),
child: Center(
child: Text(
details.status ?? "-",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 10 * scaleFactor,
color: _getStatusTextColor(details.status),
),
),
),
),
],
),
),
/// Leave Details
Padding(
padding: EdgeInsets.all(8.0 * scaleFactor),
child: Column(
children: [
_buildSectionHeader("Leave Details", ),
_buildDetailTile("Application ID", details.id, scaleFactor),
_buildDetailTile("Applied Date", details.appliedDate, scaleFactor),
_buildDetailTile("Leave Type", details.leaveType, scaleFactor),
_buildDateRangeTile("Leave Period", details.fromDate, details.toDate, scaleFactor),
_buildTimeRangeTile("Time Period", details.fromTime, details.toTime, scaleFactor),
_buildDetailTile("Reason", details.reason, scaleFactor),
/// Approval Details
_buildSectionHeader("Approval Details", ),
_buildDetailTile("Requested To", details.requestedTo, scaleFactor),
_buildDetailTile("Approved By", details.approvedBy, scaleFactor),
_buildDetailTile("Approved Date", details.approvedDate, scaleFactor),
_buildDetailTile("Approval Remarks", details.approvalRemarks, scaleFactor),
/// Additional Information
_buildSectionHeader("Additional Information", ),
_buildDetailTile("Status", details.status, scaleFactor),
_buildDetailTile("From Time", details.fromTime, scaleFactor),
_buildDetailTile("To Time", details.toTime, scaleFactor),
],
),
),
],
),
),
),
SizedBox(height: 30 * scaleFactor),
],
),
);
},
),
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,
child: Column(
children: [
Row(
children: [
/// Reject Button
Expanded(
child: InkWell(
onTap: () {
showRemarkSheet(
context: context,
actionType: "Reject",
onSubmit: (remark) {
provider.leaveRequestRejectApprove(
context,
mode: widget.mode,
type: "Rejected",
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_reject_ic.svg"),
const SizedBox(width: 6),
const Text(
"Reject",
style: TextStyle(
color: Colors.black87,
fontSize: 14,
fontFamily: "JakartaMedium",
),
],
),
],
),
),
),
),
/// Vertical Divider
Container(
width: 1,
height: 45,
color: Colors.grey.shade300,
),
/// Approve Button
Expanded(
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,
);
},
),
),
);
}
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,
style: TextStyle(
color: Colors.black, // Entered text color
fontSize: 14, // Optional: adjust font size
),
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: 5),
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();
await onSubmit(remark);
Navigator.pop(context);
if (mounted) {
setState(() {
_actionSubmitted = true;
});
}
// Show snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Request submitted successfully"),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
},
/// Leave Details
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_buildSectionHeader("Leave Details"),
_buildDetailTile("Application ID", details.id),
_buildDetailTile("Applied Date", details.appliedDate),
_buildDetailTile("Leave Type", details.leaveType),
_buildDateRangeTile("Leave Period", details.fromDate, details.toDate),
_buildTimeRangeTile("Time Period", details.fromTime, details.toTime),
_buildDetailTile("Reason", details.reason),
/// Approval Details
_buildSectionHeader("Approval Details"),
_buildDetailTile("Requested To", details.requestedTo),
_buildDetailTile("Approved By", details.approvedBy),
_buildDetailTile("Approved Date", details.approvedDate),
_buildDetailTile("Approval Remarks", details.approvalRemarks),
/// Additional Information
_buildSectionHeader("Additional Information"),
_buildDetailTile("Status", details.status),
_buildDetailTile("From Time", details.fromTime),
_buildDetailTile("To Time", details.toTime),
],
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",
),
),
),
),
],
),
),
),
],
),
const SizedBox(height: 30),
SizedBox(height: 2,)
],
),
);
},
),
);
},
),
),
),
);
},
);
},
);
}
/// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value) {
Widget _buildDetailTile(String label, String? value, double scaleFactor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // Align top if value wraps
children: [
// Label
Expanded(
flex: 6,
flex: 5,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
fontSize: 12 * scaleFactor,
color: AppColors.semi_black,
),
),
),
const SizedBox(width: 4),
// Value
Expanded(
flex: 0,
flex: 5,
child: Text(
value ?? "-",
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
style: TextStyle(
fontSize: 12 * scaleFactor,
color: const Color(0xFF818181),
fontWeight: FontWeight.w400,
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
],
......@@ -248,32 +588,40 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
);
}
/// For date range display
Widget _buildDateRangeTile(String label, String? fromDate, String? toDate) {
Widget _buildDateRangeTile(String label, String? fromDate, String? toDate, double scaleFactor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Expanded(
flex: 6,
flex: 5,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
fontSize: 12 * scaleFactor,
color: AppColors.semi_black,
),
),
),
const SizedBox(width: 4),
// Value
Expanded(
flex: 0,
flex: 5,
child: Text(
'${fromDate ?? "-"} to ${toDate ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
style: TextStyle(
fontSize: 12 * scaleFactor,
color: const Color(0xFF818181),
fontWeight: FontWeight.w400,
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
],
......@@ -281,38 +629,46 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
);
}
/// 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)) {
return const SizedBox.shrink(); // Hide if no time data
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Expanded(
flex: 6,
flex: 5,
child: Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
style: TextStyle(
fontSize: 12 * scaleFactor,
color: const Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(width: 4),
// Value
Expanded(
flex: 0,
flex: 5,
child: Text(
'${fromTime ?? "-"} to ${toTime ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
style: TextStyle(
fontSize: 12 * scaleFactor,
color: const Color(0xff818181),
fontWeight: FontWeight.w400,
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
],
......@@ -320,6 +676,7 @@ class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScr
);
}
/// Section header with dotted line
Widget _buildSectionHeader(String title) {
return Padding(
......
......@@ -11,7 +11,8 @@ import 'AddLeaveRequestScreen.dart';
import 'LeaveApplicationDetailScreen.dart';
class LeaveApplicationListScreen extends StatefulWidget {
const LeaveApplicationListScreen({super.key});
final mode;
const LeaveApplicationListScreen({super.key, required this.mode});
@override
State<LeaveApplicationListScreen> createState() => _LeaveApplicationListScreenState();
......@@ -30,276 +31,292 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen>
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) {
final provider = LeaveApplicationListProvider();
Future.microtask(() {
provider.fetchLeaveApplications(context);
});
return provider;
},
builder: (context, child) {
return Consumer<LeaveApplicationListProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: appbar2New(
context,
"Leave Application List",
provider.resetForm,
Row(
children: [
InkResponse(
onTap: () async {
var cf = Commondaterangefilter();
var result = await cf.showFilterBottomSheet(context);
if (result != null) {
var dateRange = result['dateRange'] as DateTimeRange?;
var formatted = result['formatted'] as List<String>;
if (formatted.isNotEmpty) {
provider.setDateRangeFilter("Custom", customRange: dateRange);
provider.fetchLeaveApplications(
context,
dateRange: "Custom",
customRange: dateRange,
);
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) {
final provider = LeaveApplicationListProvider();
Future.microtask(() {
provider.fetchLeaveApplications(context, widget.mode);
});
return provider;
},
builder: (context, child) {
return Consumer<LeaveApplicationListProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: appbar2New(
context,
"Leave Application List",
provider.resetForm,
Row(
children: [
InkResponse(
onTap: () async {
var cf = Commondaterangefilter();
var result = await cf.showFilterBottomSheet(context);
if (result != null) {
var dateRange = result['dateRange'] as DateTimeRange?;
var formatted = result['formatted'] as List<String>;
if (formatted.isNotEmpty) {
provider.setDateRangeFilter("Custom", customRange: dateRange);
provider.fetchLeaveApplications(
context,
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(
children: [
/// Filter chips (if you want visible filter indicators)
// if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Row(
// children: [
// if (provider.selectedStatus != "All")
// Chip(
// label: Text('Status: ${provider.selectedStatus}'),
// onDeleted: () {
// provider.setStatusFilter("All");
// provider.fetchLeaveApplications(context);
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter("This Month");
// provider.fetchLeaveApplications(context);
// },
// ),
// ],
// ),
// ),
backgroundColor: const Color(0xFFF6F6F8),
body: Column(
children: [
/// Filter chips (if you want visible filter indicators)
// if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Row(
// children: [
// if (provider.selectedStatus != "All")
// Chip(
// label: Text('Status: ${provider.selectedStatus}'),
// onDeleted: () {
// provider.setStatusFilter("All");
// provider.fetchLeaveApplications(context);
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter("This Month");
// provider.fetchLeaveApplications(context);
// },
// ),
// ],
// ),
// ),
/// Leave application list
Expanded(
child: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) {
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());
/// Leave application list
Expanded(
child: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) {
return const Center(child: Text("No leave applications found"));
}
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(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationDetailScreen(
leaveRequestId: item.id.toString(),
),
),
).then((_) {
provider.fetchLeaveApplications(context);
});
},
String dateToMonth = DateFormat("dd MMM yyyy").format(parsedToDate);
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
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(
color: _getStatusTextColor(item.status),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationDetailScreen(
leaveRequestId: item.id.toString(),
mode: widget.mode,
),
),
const SizedBox(width: 12),
).then((_) {
provider.fetchLeaveApplications(context,widget.mode);
});
},
/// Middle Section - Leave Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.leaveType ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,//Color(int.parse(item.rowColor!.replaceFirst('#', '0xff'))),
borderRadius: BorderRadius.circular(16),
),
child: Row(
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(
fontFamily: "JakartaRegular",
color: _getStatusTextColor(item.status),
fontSize: 14,
color: AppColors.semi_black,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
dateFromMonth ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
),
const SizedBox(width: 12),
/// Middle Section - Leave Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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}" ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
const SizedBox(height: 4),
Row(
children: [
Text(
dateFromMonth ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
),
],
),
// const SizedBox(height: 2),
// Text(
// "Period: ${item.fromPeriod ?? "-"} to ${item.toPeriod ?? "-"}",
// style: const TextStyle(
// fontSize: 12.5,
// color: Color(0xff818181),
// fontFamily: "Plus Jakarta Sans",
// ),
// ),
],
Text(
" - ${dateToMonth}" ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
],
),
// 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
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// decoration: BoxDecoration(
// color: _getStatusBackgroundColor(item.status),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Text(
// item.status ?? "-",
// style: TextStyle(
// fontFamily: "JakartaMedium",
// fontSize: 13,
// color: _getStatusTextColor(item.status),
// ),
// ),
// ),
],
/// Right Status
if (widget.mode == "teamleader")
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(0x00FFFFFF),
borderRadius: BorderRadius.circular(10),
),
child: Text(
item.leaveType ?? "-",
style: TextStyle(
fontFamily: "JakartaMedium",
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((_) {
provider.fetchLeaveApplications(context);
});
SizedBox(height: 28,)
],
),
// 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(
"Add Leave Request",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white,
bottomNavigationBar: widget.mode == "teamleader"
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
color: Colors.white,
child: InkResponse(
onTap: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider(
create: (_) => LeaveApplicationListProvider(),
child: AddLeaveRequest(pageTitleName: "Add Leave Request"),
),
),
).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
Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) {
......@@ -313,13 +330,14 @@ class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen>
}
}
/// Get status text color
Color _getStatusTextColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_text_color;
case 'rejected':
return AppColors.rejected_text_color;
return Colors.redAccent.shade200;
case 'requested':
default:
return AppColors.requested_text_color;
......
......@@ -17,285 +17,288 @@ class RewardListScreen extends StatefulWidget {
class _RewardListScreenState extends State<RewardListScreen> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) =>
RewardListProvider()..fetchRewardList(context),
child: Consumer<RewardListProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) =>
RewardListProvider()..fetchRewardList(context),
child: Consumer<RewardListProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
const SizedBox(width: 10),
Text(
"Reward List",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
const SizedBox(width: 10),
Text(
"Reward List",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
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),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(
color: Colors.blue,));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response == null) {
return const Center(child: Text("No details found"));
}
final rewardDetail = provider.response!;
final rewardResponse = provider.response!;
final rewards = rewardResponse.rewardsList; // main list object
final achieved = rewardResponse.achievedAmount ?? "0";
final disbursed = rewardResponse.disbursedAmount ?? "0";
final balance = rewardResponse.balanceAmount ?? "0";
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
backgroundColor: Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(
color: Colors.blue,));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response == null) {
return const Center(child: Text("No details found"));
}
final rewardDetail = provider.response!;
final rewardResponse = provider.response!;
final rewards = rewardResponse.rewardsList; // main list object
final achieved = rewardResponse.achievedAmount ?? "0";
final disbursed = rewardResponse.disbursedAmount ?? "0";
final balance = rewardResponse.balanceAmount ?? "0";
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
/// --- Top Summary Cards ---
Stack(
children: [
Container(
height: 110,
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xffd9ffd6),
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"₹${achieved}", // Achieved Amount from response
style: const TextStyle(
fontSize: 20,
color: Color(0xff0D9C00),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
/// --- Top Summary Cards ---
Stack(
children: [
Container(
height: 110,
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xffd9ffd6),
borderRadius: BorderRadius.circular(18),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"₹${achieved}", // Achieved Amount from response
style: const TextStyle(
fontSize: 20,
color: Color(0xff0D9C00),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 10),
const Text(
"Achievement Amount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
const SizedBox(height: 10),
const Text(
"Achievement Amount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400,
),
),
),
],
],
),
),
),
// Positioned SVG Icon
Positioned(
bottom: 8,
right: 12,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/achievement_ic.svg",
fit: BoxFit.contain,
// Positioned SVG Icon
Positioned(
bottom: 8,
right: 12,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/achievement_ic.svg",
fit: BoxFit.contain,
),
),
),
),
),
],
),
const SizedBox(height: 12),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Container(
height: 110,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffe8ddff),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"₹${disbursed}", // Disbursed Amount
style: const TextStyle(
fontSize: 20,
color: Color(0xff493272),
fontWeight: FontWeight.w500,
Row(
children: [
Expanded(
child: Container(
height: 110,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffe8ddff),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"₹${disbursed}", // Disbursed Amount
style: const TextStyle(
fontSize: 20,
color: Color(0xff493272),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
const Text(
"Disbursed \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontWeight: FontWeight.w400,
const SizedBox(height: 8),
const Text(
"Disbursed \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontWeight: FontWeight.w400,
),
),
),
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/location_ic.svg",
fit: BoxFit.contain,
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/location_ic.svg",
fit: BoxFit.contain,
),
),
),
),
),
],
],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 110,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xfffffbc3),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"₹${balance}", // Balance Amount
style: const TextStyle(
fontSize: 18,
color: Color(0xff605C00),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 110,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xfffffbc3),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"₹${balance}", // Balance Amount
style: const TextStyle(
fontSize: 18,
color: Color(0xff605C00),
),
),
),
const SizedBox(height: 8),
const Text(
"Balance \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontWeight: FontWeight.w400,
const SizedBox(height: 8),
const Text(
"Balance \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontWeight: FontWeight.w400,
),
),
),
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/ballance_ic.svg",
fit: BoxFit.contain,
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/ballance_ic.svg",
fit: BoxFit.contain,
),
),
),
),
),
],
],
),
),
),
),
],
),
],
),
const SizedBox(height: 20),
const SizedBox(height: 20),
/// --- Reward List Card ---
if (rewards != null)
_rewardListCard(
title: rewards.description ?? "-", // rewardsList fields
dateTime: rewards.dateTime ?? "-",
achieved: achieved,
disbursed: disbursed,
balance: balance,
enteredBy: rewards.enteredBy ?? "-",
)
else
const Text("No rewards available"),
],
),
);
}
),
);
}
)
/// --- Reward List Card ---
if (rewards != null)
_rewardListCard(
title: rewards.description ?? "-", // rewardsList fields
dateTime: rewards.dateTime ?? "-",
achieved: achieved,
disbursed: disbursed,
balance: balance,
enteredBy: rewards.enteredBy ?? "-",
)
else
const Text("No rewards available"),
],
),
);
}
),
);
}
)
),
);
}
......@@ -315,13 +318,13 @@ class _RewardListScreenState extends State<RewardListScreen> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// blurRadius: 6,
// offset: const Offset(0, 3),
// )
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......
......@@ -19,299 +19,316 @@ class TourExpensesDetailsScreen extends StatefulWidget {
class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TourExpensesDetailsProvider()
..fetchTourExpensesDetails(context, widget.tourBillId),
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) => TourExpensesDetailsProvider()
..fetchTourExpensesDetails(context, widget.tourBillId),
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
),
SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Tour Expenses",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Tour Expenses",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
),
),
],
],
),
),
),
backgroundColor: AppColors.scaffold_bg_color,
body: Consumer<TourExpensesDetailsProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
final response = provider.response;
if (response == null) {
return const Center(child: Text("No data available"));
}
debugPrint("==================requestDetails: ${response.requestDetails?.approvalStatus}");
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
backgroundColor: AppColors.scaffold_bg_color,
body: Consumer<TourExpensesDetailsProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
final response = provider.response;
if (response == null) {
return const Center(child: Text("No data available"));
}
debugPrint("==================requestDetails: ${response.requestDetails?.approvalStatus}");
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header Card at the very top
_expenseHeaderCard(
title: response.requestDetails?.placeOfVisit ?? "Tour",
date: response.tourExpenses?.fromDate ?? "-",
status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false)
? response.requestDetails!.approvalStatus!
: "No Status",
details: [
{"key": "TL Pending Approval Amount", "value": "-"},
{"key": "Total Approved Amount", "value": response.tourExpenses?.appliedAmount ?? "-"},
{"key": "Total Balance Amount", "value": "-"},
{"key": "HR Expiring Amount (Within 24Hrs)", "value": "-"},
{"key": "HR Pending Approval 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,
),
),
/// Header Card at the very top
_expenseHeaderCard(
title: response.requestDetails?.placeOfVisit ?? "Tour",
date: response.tourExpenses?.fromDate ?? "-",
status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false)
? response.requestDetails!.approvalStatus!
: "No Status",
details: [
{"key": "TL Pending Approval Amount", "value": "-"},
{"key": "Total Approved Amount", "value": response.tourExpenses?.appliedAmount ?? "-"},
{"key": "Total Balance Amount", "value": "-"},
{"key": "HR Expiring Amount (Within 24Hrs)", "value": "-"},
{"key": "HR Pending Approval Amount", "value": "-"},
{"key": "Total Disbursed Amount", "value": "-"},
],
),
const SizedBox(height: 8),
SizedBox(
height: 220, // adjust height to match your card
child: ListView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric( // horizontal margin for centering
horizontal: MediaQuery.of(context).size.width * 0.05,
),
children: [
SizedBox(
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: 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: 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),
SizedBox(
height: 216,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.04,
),
itemCount: response.travelExpenses!.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final t = response.travelExpenses![index];
return SizedBox(
width: MediaQuery.of(context).size.width * 0.90, // card width
child: _travelExpenseCard(
travelType: t.travelType ?? "-",
amount: t.fare ?? "0",
from: t.froma ?? "-",
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: 8),
SizedBox(
height: 220, // adjust height to match your card
child: ListView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric( // horizontal margin for centering
horizontal: MediaQuery.of(context).size.width * 0.05,
),
children: [
SizedBox(
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),
],
/// 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,
/// 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),
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}");
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
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.travelExpenses!.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final t = response.travelExpenses![index];
return SizedBox(
width: MediaQuery.of(context).size.width * 0.90, // card width
child: _travelExpenseCard(
travelType: t.travelType ?? "-",
amount: t.fare ?? "0",
from: t.froma ?? "-",
to: t.toa ?? "-",
onViewTap: () {
debugPrint("Open: ${t.imageDirFilePath}");
//Fileviewer(fileName: "", fileUrl: t.imageDirFilePath.toString())
Navigator.push(
context,
MaterialPageRoute(
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),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Text("Other Expenses",
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
color: AppColors.grey_thick,
/// Other Expenses Cards
if (response.otherExpenses != null &&
response.otherExpenses!.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
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),
SizedBox(
height: 216,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.04,
),
itemCount: response.otherExpenses!.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final o = response.otherExpenses![index];
return SizedBox(
width: MediaQuery.of(context).size.width * 0.90,
child: _otherExpenseCard(
description: o.otherDesc ?? "-",
amount: o.otherAmount ?? "0",
date: o.otherDate ?? "-",
onViewTap: () {
debugPrint("Open: ${o.imageDirFilePath}");
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(o.imageDirFilePath.toString())
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.otherExpenses!.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final o = response.otherExpenses![index];
return SizedBox(
width: MediaQuery.of(context).size.width * 0.90,
child: _otherExpenseCard(
description: o.otherDesc ?? "-",
amount: o.otherAmount ?? "0",
date: o.otherDate ?? "-",
onViewTap: () {
debugPrint("Open: ${o.imageDirFilePath}");
Navigator.push(
context,
MaterialPageRoute(
builder:
(
context,
) => Fileviewer(
fileName:
o.imageDirFilePath ??
"",
fileUrl:
o.imageDirFilePath ??
"",
),
),
),
);
},
),
);
},
);
},
),
);
},
),
),
),
],
],
const SizedBox(height: 25),
],
),
);
},
const SizedBox(height: 25),
],
),
);
},
),
),
),
);
......@@ -334,13 +351,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// blurRadius: 6,
// offset: const Offset(0, 3),
// )
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -407,13 +424,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// blurRadius: 6,
// offset: const Offset(0, 3),
// )
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -477,13 +494,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// blurRadius: 6,
// offset: const Offset(0, 3),
// )
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -548,13 +565,13 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// blurRadius: 6,
// offset: const Offset(0, 3),
// )
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -681,7 +698,7 @@ class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
bottomRight: Radius.circular(30),
),
),
elevation: 2,
elevation: 0,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
......
......@@ -168,38 +168,36 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
],
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: InkResponse(
onTap: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const AddBillScreen(pageTitleName: "Add Bill",),
settings: const RouteSettings(
name: 'AddTourExpBillScreen'),
bottomNavigationBar: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
color: Colors.white,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xff1487c9), // App blue
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
).then((_) {
provider.fetchTourExpenses(context, "1");
});
// show add bill screen here
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0, // Optional: remove shadow
),
child: Text(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddBillScreen(pageTitleName: "Add Bill"),
settings: const RouteSettings(name: 'AddTourExpBillScreen'),
),
).then((_) {
provider.fetchTourExpenses(context, "1");
});
},
child: const Text(
"Add Bill",
style: TextStyle(
fontSize: 15,
fontSize: 16,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
......
......@@ -57,6 +57,7 @@ export 'package:generp/Notifiers/crmProvider/followUpUpdateProvider.dart';
export 'package:generp/Notifiers/crmProvider/appointmentCalendarProvider.dart';
export 'package:generp/Notifiers/crmProvider/addNewLeadsandProspectsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/hrmAccessiblePagesProvider.dart';
export 'package:generp/Notifiers/hrmProvider/attendanceListProvider.dart';
export 'package:generp/Notifiers/hrmProvider/AttendanceDetailsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/tourExpensesProvider.dart';
......
......@@ -4995,11 +4995,13 @@ class ApiCalling {
type,
from,
to,
mode
) async {
try {
Map<String, String> data = {
'emp_id': (empId).toString(),
'session_id': (session).toString(),
'mode': (mode),
'type': (type),
'from': (from),
'to': (to),
......@@ -5019,6 +5021,7 @@ class ApiCalling {
}
}
static Future<attendanceRequestDetailsResponse?> attendanceRequestDetailAPI(
empId,
session,
......@@ -5045,6 +5048,38 @@ class ApiCalling {
}
}
static Future<CommonResponse?> attendanceRequestApproveRejectAPI(
session,
empId,
mode,
type,
remarks,
id,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'mode': (mode).toString(),
'type': (type).toString(),
'remarks': (remarks).toString(),
'id': (id).toString(),
};
final res = await post(data, AttendanceRequestRejectUrl, {});
if (res != null) {
print("Attendance App Reje:${data}");
debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
static Future<CommonResponse?> addAttendanceRequestAPI({
required String sessionId,
required String empId,
......@@ -5317,12 +5352,14 @@ class ApiCalling {
// Leave Application api
// Leave Application api
static Future<leaveApplicationLIstResponse?> leaveApplicationListAPI(
session,
empId,
dateFrom,
dateTo
dateTo,
mode
) async {
try {
Map<String, String> data = {
......@@ -5330,6 +5367,8 @@ class ApiCalling {
'emp_id': (empId).toString(),
'requested_date_from': (dateFrom),
'requested_date_to': (dateTo),
'mode': (mode),
};
final res = await post(data, LeaveApplicationListUrl, {});
if (res != null) {
......@@ -5408,6 +5447,38 @@ class ApiCalling {
}
}
static Future<CommonResponse?> leaveRequestRejectApproveAPI(
session,
empId,
mode,
type,
remarks,
id,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'mode': (mode).toString(),
'type': (type).toString(),
'remarks': (remarks).toString(),
'id': (id).toString(),
};
final res = await post(data, LeaveRequestRejectAprroveUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
// static Future<CommonResponse?> TpcIssueListApprovalAPI(
// empId,
......
......@@ -180,13 +180,16 @@ const crmDashboardQuotationsUrl = "${baseUrl_test}crm_dashboard_quotations_list"
const ogcharturl = "${baseUrl_test}organisation_structures";
const JobDesciptionUrl ="${baseUrl_test}job_description";
///HRM
//Attendance
const HrmAccessiblePagesUrl ="${baseUrl_test}hrm_accessible_pages";
const AttendanceRequestListUrl ="${baseUrl_test}attendance_request_list";
const AttendanceRequestDetailsUrl ="${baseUrl_test}attendance_request_details";
const AddAttendanceRequestUrl ="${baseUrl_test}add_attendance_request";
const AttendanceRequestRejectUrl ="${baseUrl_test}attendance_approve_reject";
const AttendanceRequestAproveUrl ="${baseUrl_test}attendance_approve_reject";
// reward list
const RewardListUrl ="${baseUrl_test}hrm_emp_self_rewards";
// Tour Expenses hrm_emp_self_rewards
......@@ -198,6 +201,7 @@ const AddTourExpensesUrl ="${baseUrl_test}add_tour_bill";
const LeaveApplicationListUrl ="${baseUrl_test}leave_request_list";
const LeaveApplicationDetailsUrl ="${baseUrl_test}leave_request_details";
const LeaveRequestAdditionUrl ="${baseUrl_test}add_leave_request";
const LeaveRequestRejectAprroveUrl ="${baseUrl_test}leaves_approve_reject";
......