......@@ -16,6 +16,7 @@ class HomescreenNotifier extends ChangeNotifier {
String _curdate = "";
String _empId = "";
String _session = "";
String _requestId = "";
String _webPageUrl = "";
String _whizzdomPageUrl = "";
String _roleStatus = "";
......@@ -34,6 +35,8 @@ class HomescreenNotifier extends ChangeNotifier {
String get session => _session;
String get requestId => _requestId;
String get webPageUrl => _webPageUrl;
String get whizzdomPageUrl => _whizzdomPageUrl;
......@@ -68,6 +71,7 @@ class HomescreenNotifier extends ChangeNotifier {
_email = await SharedpreferencesService().getString("UserEmail") ?? "";
_session = await SharedpreferencesService().getString("Session_id") ?? "";
_roleStatus = await SharedpreferencesService().getString("roles") ?? "";
_requestId = await SharedpreferencesService().getString("attendRequestId") ?? "";
var lastLocationTime = await SharedpreferencesService().getString(
"lastLocationTime",
);
......
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/leaveApplicationDetailsResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class LeaveApplicationDetailsProvider extends ChangeNotifier {
leaveApplicationDetailsResponse? _response;
bool _isLoading = false;
String? _errorMessage;
leaveApplicationDetailsResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
/// Fetch leave application details
Future<void> fetchLeaveApplicationDetails(BuildContext context, String leaveRequestId) async {
_isLoading = true;
_errorMessage = null;
_response = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.leaveApplicationDetailAPI(
provider.session,
provider.empId,
leaveRequestId,
);
if (result != null) {
_response = result;
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Something went wrong: $e";
debugPrint('Error fetching leave application details: $e');
}
_isLoading = false;
notifyListeners();
}
/// Clear the current response data
void clearData() {
_response = null;
_errorMessage = null;
notifyListeners();
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:generp/Models/hrmModels/attendanceRequestDetailsResponse.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/attendanceRequestListResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class AttendanceDetailsProvider extends ChangeNotifier {
attendanceRequestDetailsResponse? _response;
bool _isLoading = false;
String? _errorMessage;
attendanceRequestDetailsResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
Future<void> fetchAttendanceRequestDetail(context,String requestId) async {
_isLoading = true;
_errorMessage = null;
_response = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.attendanceRequestDetailAPI(provider.empId, provider.session, requestId);
if (result != null) {
_response = result;
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Something went wrong: $e";
}
_isLoading = false;
notifyListeners();
}
}
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/attendanceRequestListResponse.dart';
import '../../Models/ordersModels/commonResponse.dart';
import '../../Utils/app_colors.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class Attendancelistprovider extends ChangeNotifier {
attendanceRequestListResponse? _response;
bool _isLoading = false;
String? _errorMessage;
attendanceRequestListResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// Filter states
String _selectedType = "All";
String _selectedDateRange = "This Month";
DateTimeRange? _customDateRange;
String get selectedType => _selectedType;
String get selectedDateRange => _selectedDateRange;
DateTimeRange? get customDateRange => _customDateRange;
// Addition of attendance
CommonResponse? _addResponse;
CommonResponse? get addResponse => _addResponse;
bool _isSubmitting = false;
bool get isSubmitting => _isSubmitting;
// Date controllers for filter UI
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
// For manual attendance date field
final TextEditingController dateController = TextEditingController();
DateTime? _selectedDate;
DateTime? get selectedDate => _selectedDate;
// Type options for filter
final List<String> typeOptions = ["All", "Check In", "Check Out", "Check In/Out"];
// Date range options for filter
final List<String> dateRangeOptions = [
"All",
"Today",
"Yesterday",
"This Month",
"Past 7 days",
"Last Month",
"Custom",
];
bool isDateValid() {
if (_selectedDate == null) return false;
DateTime today = DateTime.now();
DateTime yesterday = today.subtract(const Duration(days: 1));
// normalize (remove time part)
DateTime normalizedSelected = DateTime(_selectedDate!.year, _selectedDate!.month, _selectedDate!.day);
DateTime normalizedToday = DateTime(today.year, today.month, today.day);
DateTime normalizedYesterday = DateTime(yesterday.year, yesterday.month, yesterday.day);
return normalizedSelected == normalizedToday || normalizedSelected == normalizedYesterday;
}
String? validateManualAttendance({//its working or not
required String type,
required String? checkInTime,
required String? checkInLoc,
required File? checkInProof,
required String? checkOutTime,
required String? checkOutLoc,
required File? checkOutProof,
required String? checkInDesc,
required String? checkOutDesc,
}) {
if (!isDateValid()) {
return "Date must be today or yesterday";
}
if (type.isEmpty) return "Please select type";
if (type == "Check In") {
if ((checkInTime ?? "").isEmpty ||
(checkInLoc ?? "").isEmpty ||
(checkInDesc ?? "").isEmpty ||
checkInProof == null) {
return "Please fill all Check In fields";
}
}
if (type == "Check Out") {
if ((checkOutTime ?? "").isEmpty ||
(checkOutLoc ?? "").isEmpty ||
(checkOutDesc ?? "").isEmpty ||
checkOutProof == null) {
return "Please fill all Check Out fields";
}
}
if (type == "Check In/Out") {
if ((checkInTime ?? "").isEmpty ||
(checkInLoc ?? "").isEmpty ||
(checkInDesc ?? "").isEmpty ||
checkInProof == null ||
(checkOutTime ?? "").isEmpty ||
(checkOutLoc ?? "").isEmpty ||
(checkOutDesc ?? "").isEmpty ||
checkOutProof == null) {
return "Please fill all Check In & Check Out fields";
}
}
return null; // everything ok
}
/// Fetch attendance request list with filters
Future<void> fetchAttendanceRequests(BuildContext context,
{String? type, String? dateRange, DateTimeRange? customRange}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
// Update filter states if provided
if (type != null) _selectedType = type;
if (dateRange != null) _selectedDateRange = dateRange;
if (customRange != null) _customDateRange = customRange;
// Calculate date range based on selection
final dateParams = _getDateRangeParams(_selectedDateRange, _customDateRange);
// Convert "All" type to empty string for API
final apiType = _selectedType == "All" ? "" : _selectedType;
final result = await ApiCalling.attendanceRequestListAPI(
provider.empId,
provider.session,
apiType,
dateParams['from']!,
dateParams['to']!,
);
debugPrint('Fetching attendance from: ${dateParams['from']} to: ${dateParams['to']}');
if (result != null) {
_response = result;
if (_response?.requestList == null || _response!.requestList!.isEmpty) {
_errorMessage = "No attendance records found!";
}
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Error: $e";
debugPrint('Error fetching attendance: $e');
}
_isLoading = false;
notifyListeners();
}
/// --- Add Attendance Request ---
Future<void> addAttendanceRequest(
BuildContext context, {
required String process,
required String type,
required String loc,
required String checkDate,
String? checkInTime,
String? checkInLoc,
File? checkInProof,
String? checkOutTime,
String? checkOutLoc,
File? checkOutProof,
String? note,
}) async {
_isSubmitting = true;
_errorMessage = null;
_addResponse = null;
notifyListeners();
try {
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.addAttendanceRequestAPI(
sessionId: homeProvider.session,
empId: homeProvider.empId,
process: process,
type: type,
loc: loc,
checkDate: checkDate,
checkInTime: checkInTime,
checkInLoc: checkInLoc,
checkInProof: checkInProof,
checkOutTime: checkOutTime,
checkOutLoc: checkOutLoc,
checkOutProof: checkOutProof,
note: note,
);
if (result != null) {
_addResponse = result;
if (result.error != null && result.error!.isNotEmpty) {
_errorMessage = result.error;
} else {
_addResponse = result;
}
} else {
_errorMessage = "Failed to submit request!";
}
} catch (e) {
_errorMessage = "Error submitting request: $e";
}
_isSubmitting = false;
notifyListeners();
}
/// Apply filters coming from bottom sheet
void updateFiltersFromSheet(
BuildContext context, {
required String type,
required String selectedValue,
DateTimeRange? customRange,
}) {
_selectedType = type;
_selectedDateRange = selectedValue;
_customDateRange = customRange;
fetchAttendanceRequests(
context,
type: _selectedType,
dateRange: _selectedDateRange,
customRange: _customDateRange,
);
}
/// Set type filter and refresh data
void setTypeFilter(BuildContext context, String type) {
_selectedType = type;
fetchAttendanceRequests(context);
}
/// Set date range filter and refresh data
void setDateRangeFilter(BuildContext context, String dateRange,
{DateTimeRange? customRange}) {
_selectedDateRange = dateRange;
if (customRange != null) {
_customDateRange = customRange;
fromDateController.text = _formatDate(customRange.start);
toDateController.text = _formatDate(customRange.end);
}
fetchAttendanceRequests(context);
}
/// Clear all filters and refresh data
void clearFilters(BuildContext context) {
_selectedType = "All";
_selectedDateRange = "This Month";
_customDateRange = null;
fromDateController.clear();
toDateController.clear();
fetchAttendanceRequests(context);
}
/// Reset form and data
void resetForm(BuildContext context) {
_response = null;
_errorMessage = null;
clearFilters(context);
notifyListeners();
}
/// Get date range parameters for API
Map<String, String> _getDateRangeParams(String dateRange, DateTimeRange? customRange) {
final now = DateTime.now();
final formatter = DateFormat("dd MMM yyyy");
late DateTime from;
late DateTime to;
switch (dateRange) {
case "All":
from = DateTime(now.year - 1);
to = now;
break;
case "Today":
from = now;
to = now;
break;
case "Yesterday":
from = now.subtract(const Duration(days: 1));
to = now.subtract(const Duration(days: 1));
break;
case "This Month":
from = DateTime(now.year, now.month, 1);
to = DateTime(now.year, now.month + 1, 0);
break;
case "Past 7 days":
from = now.subtract(const Duration(days: 6));
to = now;
break;
case "Last Month":
from = DateTime(now.year, now.month - 1, 1);
to = DateTime(now.year, now.month, 0);
break;
case "Custom":
if (customRange != null) {
from = customRange.start;
to = customRange.end;
} else {
from = now.subtract(const Duration(days: 30));
to = now;
}
break;
default:
from = now;
to = now;
}
return {
"from": formatter.format(from),
"to": formatter.format(to),
};
}
/// Format date for display
String _formatDate(DateTime date) {
return DateFormat("dd MMM yyyy").format(date);
}
/// Apply filters and refresh data
void applyFilters(BuildContext context) {
fetchAttendanceRequests(
context,
type: _selectedType,
dateRange: _selectedDateRange,
customRange: _customDateRange,
);
}
/// Set Selected Date
void setSelectedDate(DateTime date) {
_selectedDate = date;
dateController.text = DateFormat("dd MMM yyyy").format(date);
notifyListeners();
}
/// Show Cupertino Date Picker
void showDatePickerDialog(BuildContext context) {
if (_selectedDate == null) {
setSelectedDate(DateTime.now());
}
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 250,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: Column(
children: [
// Cancel + Done Buttons
SizedBox(
height: 55,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
child: Text(
'Cancel',
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
onPressed: () => Navigator.pop(context),
),
CupertinoButton(
child: Text(
'Done',
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
onPressed: () {
setSelectedDate(_selectedDate ?? DateTime.now());
Navigator.pop(context);
},
),
],
),
),
// Cupertino Date Picker
Expanded(
child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy,
initialDateTime: _selectedDate ?? DateTime.now(),
mode: CupertinoDatePickerMode.date,
use24hFormat: true,
showDayOfWeek: true,
onDateTimeChanged: (DateTime newDate) {
setSelectedDate(newDate);
},
),
),
],
),
),
),
);
}
}
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:generp/Models/ordersModels/commonResponse.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/leaveApplicationLIstResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class LeaveApplicationListProvider extends ChangeNotifier {
leaveApplicationLIstResponse? _response;
bool _isLoading = false;
String? _errorMessage;
leaveApplicationLIstResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// Filter states
String _selectedStatus = "All";
String _selectedDateRange = "This Month";
DateTimeRange? _customDateRange;
String get selectedStatus => _selectedStatus;
String get selectedDateRange => _selectedDateRange;
DateTimeRange? get customDateRange => _customDateRange;
// Date controllers for filter UI
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
// Controllers for Add Leave form
final TextEditingController fromDateField = TextEditingController();
final TextEditingController toDateField = TextEditingController();
final TextEditingController fromTimeField = TextEditingController();
final TextEditingController toTimeField = TextEditingController();
final TextEditingController reasonController = TextEditingController();
// Status options for filter
final List<String> statusOptions = ["All", "Requested", "Approved", "Rejected"];
// Date range options for filter
final List<String> dateRangeOptions = [
"All",
"Today",
"Yesterday",
"This Month",
"Past 7 days",
"Last Month",
"Custom",
];
// Loading state for Add Leave
bool _isSubmitting = false;
bool get isSubmitting => _isSubmitting;
CommonResponse? _addResponse;
CommonResponse? get addResponse => _addResponse;
DateTime? _selectedDate;
DateTime? get selectedDate => _selectedDate;
/// Fetch leave application list with filters
Future<void> fetchLeaveApplications(BuildContext context,
{String? status, String? dateRange, DateTimeRange? customRange}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final filterStatus = status ?? _selectedStatus;
final filterDateRange = dateRange ?? _selectedDateRange;
final filterCustomRange = customRange ?? _customDateRange;
final dateParams = _getDateRangeParams(filterDateRange, filterCustomRange);
final result = await ApiCalling.leaveApplicationListAPI(
provider.session,
provider.empId,
dateParams['from']!,
dateParams['to']!,
);
debugPrint(
'Fetching leave applications from: ${dateParams['from']} to: ${dateParams['to']}');
if (result != null) {
_response = result;
if (filterStatus != "All" && _response?.requestList != null) {
_response!.requestList = _response!.requestList!
.where((item) =>
item.status?.toLowerCase() == filterStatus.toLowerCase())
.toList();
}
if (_response?.requestList == null ||
_response!.requestList!.isEmpty) {
_errorMessage = "No leave applications found!";
}
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Error: $e";
debugPrint('Error fetching leave applications: $e');
}
_isLoading = false;
notifyListeners();
}
/// --- Add Leave Request ---
Future<void> addLeaveRequest(
BuildContext context, {
required String fromDate,
required String fromTime,
required String toDate,
required String toTime,
required String leaveType,
required String reason,
}) async {
_isSubmitting = true;
_errorMessage = null;
_addResponse = null;
notifyListeners();
try {
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.leaveRequestAddAPI(
homeProvider.session,
homeProvider.empId,
fromDate,
fromTime,
toDate,
toTime,
leaveType,
reason,
);
if (result != null) {
_addResponse = result;
if (result.error != null && result.error!.isNotEmpty) {
_errorMessage = result.error;
}
} else {
_errorMessage = "Failed to submit leave request!";
}
} catch (e) {
_errorMessage = "Error submitting leave request: $e";
}
_isSubmitting = false;
notifyListeners();
}
/// Set status filter
void setStatusFilter(String status) {
_selectedStatus = status;
notifyListeners();
}
/// Set date range filter
void setDateRangeFilter(String dateRange, {DateTimeRange? customRange}) {
_selectedDateRange = dateRange;
if (customRange != null) {
_customDateRange = customRange;
fromDateController.text = _formatDate(customRange.start);
toDateController.text = _formatDate(customRange.end);
}
notifyListeners();
}
/// Clear all filters
void clearFilters() {
_selectedStatus = "All";
_selectedDateRange = "This Month";
_customDateRange = null;
fromDateController.clear();
toDateController.clear();
notifyListeners();
}
/// Reset form and data
void resetForm() {
_response = null;
_errorMessage = null;
clearFilters();
notifyListeners();
}
/// Get date range parameters for API
Map<String, String> _getDateRangeParams(
String dateRange, DateTimeRange? customRange) {
final now = DateTime.now();
final formatter = DateFormat("dd MMM yyyy");
late DateTime from;
late DateTime to;
switch (dateRange) {
case "All":
from = DateTime(now.year - 1);
to = now;
break;
case "Today":
from = now;
to = now;
break;
case "Yesterday":
from = now.subtract(const Duration(days: 1));
to = now.subtract(const Duration(days: 1));
break;
case "This Month":
from = DateTime(now.year, now.month, 1);
to = DateTime(now.year, now.month + 1, 0);
break;
case "Past 7 days":
from = now.subtract(const Duration(days: 6));
to = now;
break;
case "Last Month":
from = DateTime(now.year, now.month - 1, 1);
to = DateTime(now.year, now.month, 0);
break;
case "Custom":
if (customRange != null) {
from = customRange.start;
to = customRange.end;
} else {
from = now.subtract(const Duration(days: 30));
to = now;
}
break;
default:
from = now;
to = now;
}
return {
"from": formatter.format(from),
"to": formatter.format(to),
};
}
/// Format date
String _formatDate(DateTime date) {
return DateFormat("dd MMM yyyy").format(date);
}
/// Show Cupertino DatePicker for leave form
void showDatePickerDialog(BuildContext context,
{bool isFromDate = true}) {
DateTime? currentDate = DateTime.now();
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 250,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: Column(
children: [
SizedBox(
height: 55,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
child: const Text("Cancel",
style: TextStyle(color: Colors.blue)),
onPressed: () => Navigator.pop(context),
),
CupertinoButton(
child: const Text("Done",
style: TextStyle(color: Colors.blue)),
onPressed: () {
if (isFromDate) {
fromDateField.text =
_formatDate(currentDate ?? DateTime.now());
} else {
toDateField.text =
_formatDate(currentDate ?? DateTime.now());
}
Navigator.pop(context);
},
),
],
),
),
Expanded(
child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy,
initialDateTime: currentDate,
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) {
currentDate = newDate;
},
),
),
],
),
),
),
);
}
/// Apply filters
void applyFilters(BuildContext context) {
fetchLeaveApplications(
context,
status: _selectedStatus,
dateRange: _selectedDateRange,
customRange: _customDateRange,
);
}
/// Export
List<List<String>> prepareExportData() {
final headers = [
'ID',
'Applied Date',
'From Date',
'To Date',
'Leave Type',
'Status',
'Reason',
];
if (_response?.requestList == null) {
return [headers];
}
final rows = _response!.requestList!.map((item) => [
item.id ?? '',
item.appliedDate ?? '',
item.fromPeriod ?? '',
item.toPeriod ?? '',
'', // leave type if available
item.status ?? '',
'', // reason if available
]).toList();
return [headers, ...rows];
}
/// Set selected single date (if needed)
void setSelectedDate(DateTime date) {
_selectedDate = date;
notifyListeners();
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/rewardListResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class RewardListProvider extends ChangeNotifier {
rewardListResponse? _response;
bool _isLoading = false;
String? _errorMessage;
/// Getters
rewardListResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
/// Fetch Reward List
Future<void> fetchRewardList(BuildContext context) async {
_isLoading = true;
_errorMessage = null;
_response = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.rewardListAPI(
provider.empId,
provider.session,
);
if (result != null) {
_response = result;
} else {
_errorMessage = "No reward data found!";
}
} catch (e) {
_errorMessage = "Something went wrong: $e";
}
_isLoading = false;
notifyListeners();
}
}
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/tourExpensesDetailsResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class TourExpensesDetailsProvider extends ChangeNotifier {
tourExpensesDetailsResponse? _response;
bool _isLoading = false;
String? _errorMessage;
tourExpensesDetailsResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
Future<void> fetchTourExpensesDetails(BuildContext context, String tourBillId) async {
_isLoading = true;
_errorMessage = null;
_response = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.tourExpensesDetailAPI(
provider.session,
provider.empId,
tourBillId,
);
print("==== Tour Submitted ====");
print("empId: "+provider.empId);
print("Session: ${provider.session}");
print(": $result.");
print("finish");
print("=============================");
if (result != null) {
_response = result;
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Something went wrong: $e";
}
_isLoading = false;
notifyListeners();
}
}
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:generp/Models/hrmModels/tourExpensesAddViewResponse.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../Models/hrmModels/tourExpensesListResponse.dart';
import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart';
class TourExpensesProvider extends ChangeNotifier {
tourExpensesListResponse? _response;
bool _isLoading = false;
String? _errorMessage;
tourExpensesListResponse? get response => _response;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
tourExpensesAddViewResponse? _response2;
tourExpensesAddViewResponse? get response2 => _response2;
List<String> get daAmountList =>
_response2?.daAmount?.map((e) => e.toString()).toList() ?? [];
List<String> get tourTypeList => _response2?.tourType ?? [];
List<String> get travelTypeList => _response2?.travelType ?? [];
// Controllers for Add form
final TextEditingController fromDateField = TextEditingController();
final TextEditingController toDateField = TextEditingController();
final TextEditingController dateController = TextEditingController();
DateTime? _date;
DateTime? _fromDate;
DateTime? _toDate;
/// Format date (yyyy-MM-dd)
String _formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
/// Set single date
void setDate(DateTime newDate) {
_date = newDate;
dateController.text = _formatDate(newDate);
notifyListeners();
}
/// Fetch tour expenses list
Future<void> fetchTourExpenses(BuildContext context, String pageNumber) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.tourExpensesListAPI(
provider.empId,
provider.session,
pageNumber,
);
debugPrint('empId: ${provider.empId}, session: ${provider.session}');
if (result != null) {
_response = result;
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Error: $e";
}
_isLoading = false;
notifyListeners();
}
Future<void> fetchTourExpensesAddView(BuildContext context, String tourBillId) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.tourExpensesAddViewAPI(
provider.empId,
provider.session,
tourBillId,
);
debugPrint('empId: ${provider.empId}, session: ${provider.session}, tourBillId: $tourBillId');
if (result != null) {
_response2 = result;
} else {
_errorMessage = "No data found!";
}
} catch (e) {
_errorMessage = "Error: $e";
}
_isLoading = false;
notifyListeners();
}
Future<bool> addTourBill({
required BuildContext context,
required String placeOfVisit,
required String daAmount,
required String tourType,
required String tourDate,
required List<Map<String, dynamic>> travelExpenses,
required List<Map<String, dynamic>> hotelExpenses,
required List<Map<String, dynamic>> otherExpenses,
List<File>? travelImages,
List<File>? hotelImages,
List<File>? otherImages,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
if ((homeProvider.session ?? "").isEmpty || (homeProvider.empId ?? "").isEmpty) {
_errorMessage = "Invalid session or employee ID";
_isLoading = false;
notifyListeners();
return false;
}
debugPrint("Submitting Tour Bill => "
"place: $placeOfVisit, da: $daAmount, type: $tourType, "
"date: $tourDate, travelExp: ${travelExpenses.length}, "
"hotelExp: ${hotelExpenses.length}, "
"otherExp: ${otherExpenses.length}, "
"travelImages: ${travelImages?.length}, "
"hotelImages: ${hotelImages?.length},"
"otherImages: ${otherImages?.length}");
final result = await ApiCalling.addTourBillAPI(
sessionId: homeProvider.session ?? "",
empId: homeProvider.empId ?? "",
placeOfVisit: placeOfVisit,
daAmount: daAmount,
tourType: tourType,
tourDate: tourDate,
travelExpenses: travelExpenses,
hotelExpenses: hotelExpenses,
otherExpenses: otherExpenses,
travelImages: travelImages,
hotelImages: hotelImages,
otherImages: otherImages,
);
if (result != null) {
debugPrint(" Tour Bill Added Successfully");
_isLoading = false;
notifyListeners();
return true;
} else {
_errorMessage = "Failed to add Tour Bill";
_isLoading = false;
notifyListeners();
return false;
}
} catch (e) {
_errorMessage = "Error: $e";
_isLoading = false;
notifyListeners();
return false;
}
}
/// Show Cupertino DatePicker for leave form
/// Show Cupertino DatePicker for leave form
Future<DateTime?> showDatePickerDialog(BuildContext context,
{bool isFromDate = true}) async {
DateTime currentDate = DateTime.now();
DateTime? pickedDate;
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 250,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: Column(
children: [
SizedBox(
height: 55,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
child: const Text("Cancel",
style: TextStyle(color: Colors.blue)),
onPressed: () => Navigator.pop(context),
),
CupertinoButton(
child: const Text("Done",
style: TextStyle(color: Colors.blue)),
onPressed: () {
pickedDate = currentDate;
if (isFromDate) {
fromDateField.text = _formatDate(pickedDate!);
} else {
toDateField.text = _formatDate(pickedDate!);
}
Navigator.pop(context);
},
),
],
),
),
Expanded(
child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy,
initialDateTime: currentDate,
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) {
currentDate = newDate;
},
),
),
],
),
),
),
);
return pickedDate;
}
}
......@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
import 'package:generp/Models/hrmModels/leaveApplicationLIstResponse.dart';
import 'package:generp/Utils/app_colors.dart';
import 'screens/notifierExports.dart';
import 'package:generp/Utils/SharedpreferencesService.dart';
......@@ -226,6 +227,12 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => followUpUpdateProvider()),
ChangeNotifierProvider(create: (_) => Appointmentcalendarprovider()),
ChangeNotifierProvider(create: (_) => Addnewleadsandprospectsprovider()),
ChangeNotifierProvider(create: (_) => Attendancelistprovider()),
ChangeNotifierProvider(create: (_) => AttendanceDetailsProvider()),
ChangeNotifierProvider(create: (_) => TourExpensesProvider()),
ChangeNotifierProvider(create: (_) => TourExpensesDetailsProvider()),
ChangeNotifierProvider(create: (_) => RewardListProvider()),
ChangeNotifierProvider(create: (_) => LeaveApplicationListProvider()),
],
child: Builder(
builder: (BuildContext context) {
......
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../Utils/app_colors.dart';
import '../Utils/dropdownTheme.dart';
class CommonFilter2 {
Dropdowntheme ddtheme = Dropdowntheme();
final List<String> filterItems = [
'Today',
'Yesterday',
'This Month',
'Past 7 days',
'Last Month',
'Custom',
];
final List<String> typeItems = [
"All",
"Check In",
"Check Out",
"Check In/Out",
];
String? selectedValue; // Date range value
DateTimeRange? selectedDateRange;
String? selectedType; // Type value
// Get DateTimeRange based on filter selection
DateTimeRange? getDateRange(String filter) {
DateTime now = DateTime.now();
switch (filter) {
case 'Today':
return DateTimeRange(
start: DateTime(now.year, now.month, now.day),
end: DateTime(now.year, now.month, now.day),
);
case 'Yesterday':
DateTime yesterday = now.subtract(const Duration(days: 1));
return DateTimeRange(
start: DateTime(yesterday.year, yesterday.month, yesterday.day),
end: DateTime(yesterday.year, yesterday.month, yesterday.day),
);
case 'This Month':
return DateTimeRange(
start: DateTime(now.year, now.month, 1),
end: DateTime(now.year, now.month + 1, 0),
);
case 'Past 7 days':
return DateTimeRange(
start: now.subtract(const Duration(days: 6)),
end: DateTime(now.year, now.month, now.day),
);
case 'Last Month':
return DateTimeRange(
start: DateTime(now.year, now.month - 1, 1),
end: DateTime(now.year, now.month, 0),
);
case 'Custom':
return null;
default:
return null;
}
}
// Format a single DateTime to string
String formatDate(DateTime date) {
return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
}
// Get formatted date range as a list of strings
List<String> getFormattedDateRange(DateTimeRange? dateRange) {
if (dateRange != null) {
return [
formatDate(dateRange.start),
formatDate(dateRange.end),
];
}
return [];
}
Future<Map<String, dynamic>?> showFilterBottomSheet(BuildContext context) async {
String? tempSelectedValue = selectedValue;
DateTimeRange? tempSelectedDateRange = selectedDateRange;
DateTime? tempStartDate;
DateTime? tempEndDate;
DateTime displayedMonth = DateTime.now();
String? tempSelectedType = selectedType ?? "All";
Widget buildCalendar(StateSetter setState) {
final firstDayOfMonth = DateTime(displayedMonth.year, displayedMonth.month, 1);
final lastDayOfMonth = DateTime(displayedMonth.year, displayedMonth.month + 1, 0);
final firstDayOfWeek = firstDayOfMonth.weekday;
final daysInMonth = lastDayOfMonth.day;
final daysBefore = (firstDayOfWeek - 1) % 7;
List<Widget> dayWidgets = [];
final weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
dayWidgets.addAll(weekdays.map((day) => Center(
child: Text(
day,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
)));
for (int i = 0; i < daysBefore; i++) {
dayWidgets.add(Container());
}
for (int day = 1; day <= daysInMonth; day++) {
final currentDate = DateTime(displayedMonth.year, displayedMonth.month, day);
bool isSelected = false;
bool isInRange = false;
bool isOutsideRange =
currentDate.isBefore(DateTime(2020)) || currentDate.isAfter(DateTime(2100));
if (tempStartDate != null && tempEndDate != null) {
isSelected = currentDate.isAtSameMomentAs(tempStartDate!) ||
currentDate.isAtSameMomentAs(tempEndDate!);
isInRange = currentDate.isAfter(tempStartDate!) &&
currentDate.isBefore(tempEndDate!) &&
!isSelected;
} else if (tempStartDate != null) {
isSelected = currentDate.isAtSameMomentAs(tempStartDate!);
}
dayWidgets.add(
GestureDetector(
onTap: isOutsideRange
? null
: () {
setState(() {
if (tempStartDate == null) {
tempStartDate = currentDate;
tempSelectedDateRange = null;
} else if (tempEndDate == null) {
if (currentDate.isBefore(tempStartDate!)) {
tempEndDate = tempStartDate;
tempStartDate = currentDate;
} else {
tempEndDate = currentDate;
}
tempSelectedDateRange =
DateTimeRange(start: tempStartDate!, end: tempEndDate!);
} else {
tempStartDate = currentDate;
tempEndDate = null;
tempSelectedDateRange = null;
}
});
},
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isSelected
? Colors.blue[600]
: isInRange
? Colors.blue[100]
: null,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$day',
style: TextStyle(
color: isOutsideRange
? Colors.grey[400]
: isSelected
? Colors.white
: Colors.black,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
),
),
);
}
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: displayedMonth.isAfter(DateTime(2020))
? () {
setState(() {
displayedMonth =
DateTime(displayedMonth.year, displayedMonth.month - 1);
});
}
: null,
child: SvgPicture.asset("assets/svg/arrow_left.svg"),
),
Text(
'${_monthName(displayedMonth.month)} ${displayedMonth.year}',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
GestureDetector(
onTap: displayedMonth.isBefore(DateTime(2100))
? () {
setState(() {
displayedMonth =
DateTime(displayedMonth.year, displayedMonth.month + 1);
});
}
: null,
child: SvgPicture.asset("assets/svg/arrow_right_new.svg"),
),
],
),
SizedBox(
height: 280,
child: GridView.count(
crossAxisCount: 7,
childAspectRatio: 1.2,
children: dayWidgets,
physics: const NeverScrollableScrollPhysics(),
),
),
],
);
}
return await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filter Attendance',
style: TextStyle(
color: AppColors.app_blue,
fontSize: 18,
fontFamily: "JakartaSemiBold",
),
),
const SizedBox(height: 20),
/// Type filter
const Text("Type",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>(
isExpanded: true,
value: tempSelectedType,
items: typeItems
.map((type) =>
DropdownMenuItem<String>(value: type, child: Text(type)))
.toList(),
onChanged: (value) {
setState(() {
tempSelectedType = value;
});
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData,
menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
],
),
),
const SizedBox(height: 20),
/// Date range filter
const Text("Date Range",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>(
isExpanded: true,
hint: Text(
tempSelectedValue ?? 'Select Item',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black,
),
),
items: filterItems
.map((String item) =>
DropdownMenuItem<String>(
value: item, child: Text(
item,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black,
),
)
)
)
.toList(),
value: tempSelectedValue,
onChanged: (String? value) {
if (value == null) return;
setState(() {
tempSelectedValue = value;
if (value != 'Custom') {
tempSelectedDateRange = getDateRange(value);
tempStartDate = null;
tempEndDate = null;
} else {
tempSelectedDateRange = null;
tempStartDate = null;
tempEndDate = null;
}
});
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData,
menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
],
),
),
if (tempSelectedValue == 'Custom') ...[
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: buildCalendar(setState),
),
if (tempSelectedDateRange != null)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Text(
'Selected: ${formatDate(tempSelectedDateRange!.start)} to ${formatDate(tempSelectedDateRange!.end)}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
),
],
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel',
style: TextStyle(color: Colors.grey[600])),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
Navigator.pop(context, {
'type': tempSelectedType, // if you store type separately
'selectedValue': tempSelectedValue, // could be null
'dateRange': tempSelectedDateRange, // could be null
'formatted': tempSelectedDateRange != null
? getFormattedDateRange(tempSelectedDateRange)
: null,
});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Apply'),
),
],
),
],
),
),
),
);
},
);
},
);
}
String _monthName(int month) {
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return months[month - 1];
}
}
......@@ -10,6 +10,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:generp/Utils/commonWidgets.dart';
import '../Utils/commonServices.dart';
import 'genTracker/ScanEnterGeneratorIDScreen.dart';
import 'hrm/HrmDashboardScreen.dart';
import 'notifierExports.dart';
import 'screensExports.dart';
import 'package:geolocator/geolocator.dart';
......@@ -124,6 +125,8 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
double screenHeight = MediaQuery.of(context).size.height;
switch (_source.keys.toList()[0]) {
case ConnectivityResult.mobile:
connection = 'Online';
......@@ -138,10 +141,10 @@ class _MyHomePageState extends State<MyHomePage> {
return (connection == 'Online')
? Consumer2<HomescreenNotifier, ProfileNotifer>(
builder: (context, homescreen, profile, child) {
final coreRequiredRoles = ["12", "540","433", "434", ];
final coreRequiredRoles = ["12", "540","433", "434", "430"];
final requiredRoles = ["430", "430", "431", "431"];
final coreNames = ["CRM", "Orders","Service", "Gen Tracker", ];
final coreNames = ["CRM", "Orders","Service", "Gen Tracker","HRM" ];
final names = ["Attendance", "Finance", "ERP", "Whizzdom"];
final subtitles = [
"Check-in,Check-out",
......@@ -154,6 +157,7 @@ class _MyHomePageState extends State<MyHomePage> {
"assets/svg/home/home_order_ic.svg",
"assets/svg/home/home_service_ic.svg",
"assets/svg/home/home_gentracker_ic.svg",
"assets/svg/home/home_erp_ic.svg",
];
final icons = [
......@@ -167,6 +171,7 @@ class _MyHomePageState extends State<MyHomePage> {
"Orders, TPC, Dispatch",
"Visits, P.C. Wallet",
"Generator Details",
"Tour Bills, Live Attendance",
];
final coreFilteredItems = <Map<String, String>>[];
final filteredItems = <Map<String, String>>[];
......@@ -211,23 +216,23 @@ class _MyHomePageState extends State<MyHomePage> {
toolbarHeight: 0,
backgroundColor: Colors.white,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.scaffold_bg_color,
AppColors.scaffold_bg_color,
Color(0xFFCEEDFF),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
body: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.scaffold_bg_color,
AppColors.scaffold_bg_color,
Color(0xFFCEEDFF),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
child: Column(
children: [
Expanded(
flex: 4,
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkResponse(
onTap: () {
HapticFeedback.selectionClick();
_showProfileBottomSheet(
......@@ -275,9 +280,9 @@ class _MyHomePageState extends State<MyHomePage> {
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// "${profile.employeeName}",
// maxLines: 1,
......@@ -331,7 +336,7 @@ class _MyHomePageState extends State<MyHomePage> {
mainAxisAlignment:
MainAxisAlignment.start,
children: [
Container(
width: 12,
height: 12,
......@@ -353,8 +358,8 @@ class _MyHomePageState extends State<MyHomePage> {
color: Color(0xFF2D2D2D),
),
),
],
),
],
......@@ -396,7 +401,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
],
),
),
......@@ -406,10 +411,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
Expanded(
flex: 13,
child: SizedBox(
SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......@@ -541,8 +543,8 @@ class _MyHomePageState extends State<MyHomePage> {
// ),
// ),
// ],
if (homescreen.roleStatus.contains(
"432",
)) ...[
......@@ -556,7 +558,7 @@ class _MyHomePageState extends State<MyHomePage> {
left: 10,
bottom: 5,
top: 10
),
child: Text(
"Workforce & Operations",
......@@ -723,7 +725,7 @@ class _MyHomePageState extends State<MyHomePage> {
left: 10,
bottom: 5,
top: 10
),
padding: const EdgeInsets.only(
top: 10,
......@@ -950,7 +952,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 5,
......@@ -1027,7 +1029,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
],
],
// if (filteredItems.isNotEmpty) ...[
// Container(
// margin: EdgeInsets.symmetric(
......@@ -1235,7 +1237,7 @@ class _MyHomePageState extends State<MyHomePage> {
// ),
// ),
// ],
if (coreFilteredItems.isNotEmpty) ...[
Container(
margin: EdgeInsets.only(
......@@ -1263,6 +1265,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: GridView.builder(
shrinkWrap: true,
itemCount: coreFilteredItems.length,
physics: NeverScrollableScrollPhysics(),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
......@@ -1332,6 +1335,19 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
);
case "HRM":
res = await Navigator.push(
context,
MaterialPageRoute(
builder:
(context) =>
HrmdashboardScreen(),
settings: RouteSettings(
name:
'CrmdashboardScreen',
),
),
);
default:
print("111");
break;
......@@ -1342,7 +1358,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 5,
......@@ -1406,7 +1422,7 @@ class _MyHomePageState extends State<MyHomePage> {
Expanded(
flex: 1,
child: SvgPicture.asset(
filteredItems[ci]['icon'] ??
coreFilteredItems[ci]['icon'] ??
"-",
),
),
......@@ -1418,241 +1434,241 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
],
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 40,
Align(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(bottom: 20),
child: Image.asset(
fit: BoxFit.scaleDown,
"assets/images/horizontal_logo.png",
child: Container(
height: 40,
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(bottom: 20),
child: Image.asset(
fit: BoxFit.scaleDown,
"assets/images/horizontal_logo.png",
),
),
),
),
// Expanded(
// flex: 10,
// child: Container(
// padding: EdgeInsets.only(
// left: 20,
// right: 20,
// top: 0,
// bottom: 10,
// ),
// margin: EdgeInsets.only(top: 10),
// child: GridView.builder(
// itemCount: filteredItems.length,
// gridDelegate:
// SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 2,
// crossAxisSpacing: 10,
// mainAxisSpacing: 10,
// ),
// itemBuilder: (context, index) {
// final item = filteredItems[index];
// return InkResponse(
// onTap: () async {
// var res;
// switch (item['name']) {
// case "Attendance":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// AttendanceScreen(),
// settings: RouteSettings(
// arguments: 'AttendanceScreen',
// ),
// ),
// );
// break;
// case "ERP":
// bool isGpsEnabled =
// await Geolocator.isLocationServiceEnabled();
// if (isGpsEnabled) {
// if (Platform.isAndroid) {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) => WebErpScreen(
// erp_url:
// homescreen
// .webPageUrl,
// ),
// ),
// );
// } else {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) => WebERPIOS(
// url:
// homescreen
// .webPageUrl,
// ),
// ),
// );
// }
// } else {
// requestGpsPermission();
// }
//
// break;
// case "Gen Tracker":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Gentrackerdashboard(),
// settings: RouteSettings(
// arguments:
// 'Gentrackerdashboard',
// ),
// ),
// );
// break;
// case "Service Engineer":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Serviceengineerdashboard(),
// ),
// );
// break;
// case "Nearby":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Nearbygenerators(),
// ),
// );
//
// break;
// case "Inventory":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// InventoryScreen(),
// ),
// );
// break;
// case "Whizzdom":
// bool isGpsEnabled =
// await Geolocator.isLocationServiceEnabled();
// if (isGpsEnabled) {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (
// context,
// ) => WebWhizzdomScreen(
// whizzdom_url:
// homescreen
// .whizzdomPageUrl,
// ),
// ),
// );
// } else {
// requestGpsPermission();
// }
// break;
// case "Common":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Commondashboard(),
// ),
// );
// break;
// case "Finance":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Financedashboard(),
// settings: RouteSettings(
// arguments: 'Financedashboard',
// ),
// ),
// );
// break;
// case "Orders":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Ordermoduledashboard(),
// ),
// );
// case "CRM":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// CrmdashboardScreen(),
// settings: RouteSettings(
// name: 'CrmdashboardScreen',
// ),
// ),
// );
// default:
// print("111");
// break;
// }
// if (res == true) {
// homescreen.DashboardApiFunction(
// context,
// );
// }
// },
// child: Container(
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(30),
// ),
// child: Column(
// crossAxisAlignment:
// CrossAxisAlignment.center,
// mainAxisAlignment:
// MainAxisAlignment.center,
// children: [
// SvgPicture.asset(
// item['icon']!,
// height: 45,
// ),
// SizedBox(height: 10),
// Text(item['name']!),
// ],
// ),
// ),
// );
// },
// ),
// ),
// ),
],
// Expanded(
// flex: 10,
// child: Container(
// padding: EdgeInsets.only(
// left: 20,
// right: 20,
// top: 0,
// bottom: 10,
// ),
// margin: EdgeInsets.only(top: 10),
// child: GridView.builder(
// itemCount: filteredItems.length,
// gridDelegate:
// SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: 2,
// crossAxisSpacing: 10,
// mainAxisSpacing: 10,
// ),
// itemBuilder: (context, index) {
// final item = filteredItems[index];
// return InkResponse(
// onTap: () async {
// var res;
// switch (item['name']) {
// case "Attendance":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// AttendanceScreen(),
// settings: RouteSettings(
// arguments: 'AttendanceScreen',
// ),
// ),
// );
// break;
// case "ERP":
// bool isGpsEnabled =
// await Geolocator.isLocationServiceEnabled();
// if (isGpsEnabled) {
// if (Platform.isAndroid) {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) => WebErpScreen(
// erp_url:
// homescreen
// .webPageUrl,
// ),
// ),
// );
// } else {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) => WebERPIOS(
// url:
// homescreen
// .webPageUrl,
// ),
// ),
// );
// }
// } else {
// requestGpsPermission();
// }
//
// break;
// case "Gen Tracker":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Gentrackerdashboard(),
// settings: RouteSettings(
// arguments:
// 'Gentrackerdashboard',
// ),
// ),
// );
// break;
// case "Service Engineer":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Serviceengineerdashboard(),
// ),
// );
// break;
// case "Nearby":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Nearbygenerators(),
// ),
// );
//
// break;
// case "Inventory":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// InventoryScreen(),
// ),
// );
// break;
// case "Whizzdom":
// bool isGpsEnabled =
// await Geolocator.isLocationServiceEnabled();
// if (isGpsEnabled) {
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (
// context,
// ) => WebWhizzdomScreen(
// whizzdom_url:
// homescreen
// .whizzdomPageUrl,
// ),
// ),
// );
// } else {
// requestGpsPermission();
// }
// break;
// case "Common":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Commondashboard(),
// ),
// );
// break;
// case "Finance":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Financedashboard(),
// settings: RouteSettings(
// arguments: 'Financedashboard',
// ),
// ),
// );
// break;
// case "Orders":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// Ordermoduledashboard(),
// ),
// );
// case "CRM":
// res = await Navigator.push(
// context,
// MaterialPageRoute(
// builder:
// (context) =>
// CrmdashboardScreen(),
// settings: RouteSettings(
// name: 'CrmdashboardScreen',
// ),
// ),
// );
// default:
// print("111");
// break;
// }
// if (res == true) {
// homescreen.DashboardApiFunction(
// context,
// );
// }
// },
// child: Container(
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(30),
// ),
// child: Column(
// crossAxisAlignment:
// CrossAxisAlignment.center,
// mainAxisAlignment:
// MainAxisAlignment.center,
// children: [
// SvgPicture.asset(
// item['icon']!,
// height: 45,
// ),
// SizedBox(height: 10),
// Text(item['name']!),
// ],
// ),
// ),
// );
// },
// ),
// ),
// ),
],
),
),
),
// floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
......
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonServices.dart';
import '../../Utils/commonWidgets.dart';
import '../../Utils/dropdownTheme.dart';
import '../../Notifiers/hrmProvider/leaveApplicationListProvider.dart';
class AddLeaveRequest extends StatefulWidget {
final String pageTitleName;
const AddLeaveRequest({super.key, required this.pageTitleName});
@override
State<AddLeaveRequest> createState() => _AddLeaveRequestState();
}
class _AddLeaveRequestState extends State<AddLeaveRequest> {
Dropdowntheme ddtheme = Dropdowntheme();
List<FocusNode> focusNodes = List.generate(6, (index) => FocusNode());
Map _source = {ConnectivityResult.mobile: true};
final MyConnectivity _connectivity = MyConnectivity.instance;
String? leaveType;
List<String> leaveTypes = ["Normal", "Medical"];
// Validation error messages
String? fromDateError;
String? fromTimeError;
String? toDateError;
String? toTimeError;
String? leaveTypeError;
String? reasonError;
@override
void initState() {
super.initState();
_connectivity.initialise();
_connectivity.myStream.listen((source) {
setState(() => _source = source);
});
// Add listener to reason controller to clear error when user starts typing
final provider = Provider.of<LeaveApplicationListProvider>(context, listen: false);
provider.reasonController.addListener(() {
if (reasonError != null && provider.reasonController.text.isNotEmpty) {
setState(() => reasonError = null);
}
});
}
@override
void dispose() {
focusNodes.map((e) => e.dispose());
_connectivity.disposeStream();
super.dispose();
}
Future<bool> _onBackPressed(BuildContext context) async {
Navigator.pop(context, true);
return true;
}
// Function to validate all fields
bool validateForm(LeaveApplicationListProvider provider) {
String? newFromDateError = provider.fromDateField.text.isEmpty ? "From date is required" : null;
String? newFromTimeError = provider.fromTimeField.text.isEmpty ? "From time is required" : null;
String? newToDateError = provider.toDateField.text.isEmpty ? "To date is required" : null;
String? newToTimeError = provider.toTimeField.text.isEmpty ? "To time is required" : null;
String? newLeaveTypeError = leaveType == null ? "Leave type is required" : null;
String? newReasonError = provider.reasonController.text.isEmpty ? "Reason is required" : null;
// Only update if there are actual changes to avoid unnecessary rebuilds
if (fromDateError != newFromDateError ||
fromTimeError != newFromTimeError ||
toDateError != newToDateError ||
toTimeError != newToTimeError ||
leaveTypeError != newLeaveTypeError ||
reasonError != newReasonError) {
setState(() {
fromDateError = newFromDateError;
fromTimeError = newFromTimeError;
toDateError = newToDateError;
toTimeError = newToTimeError;
leaveTypeError = newLeaveTypeError;
reasonError = newReasonError;
});
}
return newFromDateError == null &&
newFromTimeError == null &&
newToDateError == null &&
newToTimeError == null &&
newLeaveTypeError == null &&
newReasonError == null;
}
@override
Widget build(BuildContext context) {
final leaveProvider = Provider.of<LeaveApplicationListProvider>(context);
switch (_source.keys.toList()[0]) {
case ConnectivityResult.mobile:
case ConnectivityResult.wifi:
connection = 'Online';
break;
case ConnectivityResult.none:
default:
connection = 'Offline';
}
return (connection == "Online")
? Platform.isAndroid
? WillPopScope(
onWillPop: () => _onBackPressed(context),
child: SafeArea(
top: false,
bottom: true,
child: _scaffold(context, leaveProvider)),
)
: _scaffold(context, leaveProvider)
: NoNetwork(context);
}
Widget _scaffold(BuildContext context, LeaveApplicationListProvider provider) {
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: AppColors.scaffold_bg_color,
appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF),
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// From Date
TextWidget(context, "From Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: true);
if (fromDateError != null) {
setState(() => fromDateError = null);
}
},
child: textFieldNew(context, provider.fromDateField,
"Select Date", enabled: false),
),
errorWidget(context, fromDateError),
/// From Time
TextWidget(context, "From Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.fromTimeField.text = picked.format(context);
if (fromTimeError != null) {
setState(() => fromTimeError = null);
}
}
},
child: textFieldNew(context, provider.fromTimeField,
"Select Time", enabled: false),
),
errorWidget(context, fromTimeError),
/// To Date
TextWidget(context, "To Date"),
GestureDetector(
onTap: () {
provider.showDatePickerDialog(context, isFromDate: false);
if (toDateError != null) {
setState(() => toDateError = null);
}
},
child: textFieldNew(context, provider.toDateField, "Select Date",
enabled: false),
),
errorWidget(context, toDateError),
/// To Time
TextWidget(context, "To Time"),
GestureDetector(
onTap: () async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
provider.toTimeField.text = picked.format(context);
if (toTimeError != null) {
setState(() => toTimeError = null);
}
}
},
child: textFieldNew(context, provider.toTimeField, "Select Time",
enabled: false),
),
errorWidget(context, toTimeError),
/// Leave Type
TextWidget(context, "Leave Type"),
Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint: const Text("Select Leave Type",
style: TextStyle(fontSize: 14, color: Colors.grey)),
value: leaveType,
items: leaveTypes
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) {
setState(() {
leaveType = val;
if (leaveTypeError != null) {
leaveTypeError = null;
}
});
},
),
),
),
errorWidget(context, leaveTypeError),
/// Reason
TextWidget(context, "Reason"),
textFieldNew(context, provider.reasonController, "Enter Reason",
maxLines: 2),
errorWidget(context, reasonError),
const SizedBox(height: 70),
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: InkResponse(
onTap: () {
if (validateForm(provider)) {
provider.addLeaveRequest(
context,
fromDate: provider.fromDateField.text,
fromTime: provider.fromTimeField.text,
toDate: provider.toDateField.text,
toTime: provider.toTimeField.text,
leaveType: leaveType!,
reason: provider.reasonController.text,
);
// Reset after submit
setState(() {
provider.fromDateField.clear();
provider.fromTimeField.clear();
provider.toDateField.clear();
provider.toTimeField.clear();
provider.reasonController.clear();
leaveType = null;
fromDateError = null;
fromTimeError = null;
toDateError = null;
toTimeError = null;
leaveTypeError = null;
reasonError = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Leave request submitted successfully!"),
backgroundColor: Colors.black87,
),
);
}
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: provider.isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: const Text(
"Submit",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white),
),
),
),
);
}
Widget textFieldNew(
BuildContext context,
TextEditingController controller,
String hint, {
bool enabled = true,
int maxLines = 1,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: TextFormField(
controller: controller,
enabled: enabled,
maxLines: maxLines,
style: TextStyle(
color: Colors.black, // Entered text color
fontSize: 14, // Optional: adjust font size
),
decoration: InputDecoration(
hintText: hint,
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),
),
),
);
}
Widget errorText(String msg) {
return Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Text(
msg,
style: const TextStyle(color: Colors.red, fontSize: 13),
),
);
}
bool _validateForm(LeaveApplicationListProvider provider) {
bool isValid = true;
setState(() {
fromDateError =
provider.fromDateField.text.isEmpty ? "From date is required" : null;
fromTimeError =
provider.fromTimeField.text.isEmpty ? "From time is required" : null;
toDateError =
provider.toDateField.text.isEmpty ? "To date is required" : null;
toTimeError =
provider.toTimeField.text.isEmpty ? "To time is required" : null;
leaveTypeError = leaveType == null ? "Please select leave type" : null;
reasonError = provider.reasonController.text.isEmpty
? "Reason is required"
: null;
if (fromDateError != null ||
fromTimeError != null ||
toDateError != null ||
toTimeError != null ||
leaveTypeError != null ||
reasonError != null) {
isValid = false;
}
});
return isValid;
}
}
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/dropdownTheme.dart';
class AddLiveAttendanceScreen extends StatefulWidget {
const AddLiveAttendanceScreen({Key? key}) : super(key: key);
@override
State<AddLiveAttendanceScreen> createState() =>
_AddLiveAttendanceScreenState();
}
class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
String? typeError;
String? locationError;
String? proofError;
String? selectedType;
Dropdowntheme ddtheme = Dropdowntheme();
final TextEditingController locationController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
final List<String> types = ["Check In", "Check Out"];
final ImagePicker picker = ImagePicker();
XFile? proofFile;
bool isSubmitting = false;
String get locationHeading =>
selectedType == null ? "Location" : "$selectedType Location";
String get descriptionHeading =>
selectedType == null ? "Description" : "$selectedType Description";
String get proofButtonText =>
selectedType == null ? "Attach Proof" : "Attach $selectedType Proof";
bool get isSubmitEnabled =>
selectedType != null &&
locationController.text.trim().isNotEmpty &&
proofFile != null;
@override
void initState() {
super.initState();
_autoFetchLocation();
}
Future<void> _autoFetchLocation() async {
String loc = await getCurrentLocation();
setState(() {
locationController.text = loc;
});
}
Future<String> getCurrentLocation() async {
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return "Location permission denied";
}
}
if (permission == LocationPermission.deniedForever) {
return "Location permissions permanently denied";
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
List<Placemark> placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
if (placemarks.isNotEmpty) {
Placemark place = placemarks.first;
return "${place.name}, ${place.locality}, ${place.administrativeArea}, ${place.country}";
} else {
return "${position.latitude}, ${position.longitude}";
}
} catch (e) {
return "Error: $e";
}
}
void _showPicker(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return SafeArea(
child: Wrap(
children: <Widget>[
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Camera'),
onTap: () async {
Navigator.of(context).pop();
final XFile? image =
await picker.pickImage(source: ImageSource.camera);
if (image != null) {
setState(() => proofFile = image);
}
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Gallery'),
onTap: () async {
Navigator.of(context).pop();
final XFile? image =
await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() => proofFile = image);
}
},
),
],
),
);
},
);
}
void submitAttendance(BuildContext context) async {
setState(() {
typeError = null;
locationError = null;
proofError = null;
});
bool hasError = false;
if (selectedType == null) {
typeError = "Please select a type";
hasError = true;
}
if (locationController.text.trim().isEmpty) {
locationError = "Please enter a location";
hasError = true;
}
if (proofFile == null) {
proofError = "Please attach proof";
hasError = true;
}
if (hasError) {
setState(() {});
return;
}
setState(() => isSubmitting = true);
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
await provider.addAttendanceRequest(
context,
process: "Live",
type: selectedType ?? "",
loc: locationController.text,
checkDate: DateTime.now().toString().split(" ").first,
checkInTime:
selectedType == "Check In" ? TimeOfDay.now().format(context) : null,
checkInLoc: selectedType == "Check In" ? locationController.text : null,
checkInProof: selectedType == "Check In" ? File(proofFile!.path) : null,
checkOutTime:
selectedType == "Check Out" ? TimeOfDay.now().format(context) : null,
checkOutLoc: selectedType == "Check Out" ? locationController.text : null,
checkOutProof:
selectedType == "Check Out" ? File(proofFile!.path) : null,
note: descriptionController.text,
);
setState(() {
isSubmitting = false;
selectedType = null;
locationController.clear();
descriptionController.clear();
proofFile = null;
});
_showSnack(context, "Attendance Submitted Successfully!");
_autoFetchLocation();
}
void _showSnack(BuildContext context, String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), backgroundColor: Colors.black87),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
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,
),
),
const SizedBox(width: 10),
Text(
"Add Live Attendance",
style: TextStyle(
fontSize: 18,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
],
),
),
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),
),
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!,
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!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
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),
),
child: Center(
child: Text(
proofButtonText,
style: const TextStyle(
fontSize: 16,
color: Colors.blue,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
),
),
),
),
),
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,
),
),
),
),
],
),
),
);
}
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(
fontSize: 14,
fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400,
color: Colors.grey,
),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppColors.app_blue),
),
);
}
}
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/cupertino.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter_svg/svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
import '../../Models/ordersModels/commonResponse.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonServices.dart';
import '../../Utils/commonWidgets.dart';
import '../../Utils/dropdownTheme.dart';
class AddManualAttendanceScreen extends StatefulWidget {
const AddManualAttendanceScreen({super.key});
@override
State<AddManualAttendanceScreen> createState() =>
_AddManualAttendanceScreenState();
}
class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
Dropdowntheme ddtheme = Dropdowntheme();
final ImagePicker picker = ImagePicker();
// Connectivity
Map _source = {ConnectivityResult.mobile: true};
final MyConnectivity _connectivity = MyConnectivity.instance;
String connection = "Online";
// Controllers
final checkInTime = TextEditingController();
final checkInLocation = TextEditingController();
final checkInDescription = TextEditingController();
XFile? checkInProof;
final checkOutTime = TextEditingController();
final checkOutLocation = TextEditingController();
final checkOutDescription = TextEditingController();
XFile? checkOutProof;
String? selectedType;
final List<String> types = ["Check In", "Check Out", "Check In/Out"];
// Errors
String? dateError, typeError;
String? checkInTimeError, checkInLocError, checkInDescError, checkInProofError;
String? checkOutTimeError, checkOutLocError, checkOutDescError, checkOutProofError;
// In your Attendancelistprovider class
CommonResponse? get addResponse => addResponse;
String? get errorMessage => errorMessage;
@override
void initState() {
super.initState();
_connectivity.initialise();
_connectivity.myStream.listen((src) {
setState(() => _source = src);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchInitialLocation();
});
}
@override
void dispose() {
_connectivity.disposeStream();
super.dispose();
}
Future<void> _fetchInitialLocation() async {
String loc = await getCurrentLocation();
setState(() {
checkInLocation.text = loc;
checkOutLocation.text = loc;
});
}
Future<String> getCurrentLocation() async {
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) return "Permission denied";
}
if (permission == LocationPermission.deniedForever) {
return "Permission permanently denied";
}
Position pos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
List<Placemark> placemarks =
await placemarkFromCoordinates(pos.latitude, pos.longitude);
if (placemarks.isNotEmpty) {
Placemark p = placemarks.first;
return "${p.locality}, ${p.administrativeArea}, ${p.country}";
}
return "${pos.latitude}, ${pos.longitude}";
} catch (e) {
return "Error: $e";
}
}
Future<void> _pickTime(TextEditingController controller) async {
final TimeOfDay? picked =
await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (picked != null) controller.text = picked.format(context);
}
Future<void> _pickFile(bool isCheckIn) async {
showModalBottomSheet(
useSafeArea: true,
isDismissible: true,
showDragHandle: true,
backgroundColor: Colors.white,
context: context,
builder: (_) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text("Capture photo from camera"),
onTap: () async {
final XFile? file =
await picker.pickImage(source: ImageSource.camera);
if (file != null) {
setState(() {
if (isCheckIn) checkInProof = file;
else checkOutProof = file;
});
}
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text("Select photo from gallery"),
onTap: () async {
final XFile? file =
await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
setState(() {
if (isCheckIn) checkInProof = file;
else checkOutProof = file;
});
}
Navigator.pop(context);
},
),
],
),
);
},
);
}
void _submitForm(BuildContext context) async {
// reset errors first
dateError = null;
typeError = null;
checkInTimeError = checkInLocError = checkInDescError = checkInProofError = null;
checkOutTimeError = checkOutLocError = checkOutDescError = checkOutProofError = null;
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// --- Date Validation ---
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";
}
} catch (e) {
dateError = "Invalid date format (use dd MMM yyyy)";
}
}
// --- Type Validation ---
if (selectedType == null) {
typeError = "Please select type";
}
// --- Check In Validations ---
if (selectedType == "Check In" || selectedType == "Check In/Out") {
if (checkInTime.text.isEmpty) checkInTimeError = "Please select check-in time";
if (checkInLocation.text.isEmpty) checkInLocError = "Please enter check-in location";
if (checkInDescription.text.isEmpty) checkInDescError = "Please enter description";
if (checkInProof == null) checkInProofError = "Please attach check-in proof";
}
// --- Check Out Validations ---
if (selectedType == "Check Out" || selectedType == "Check In/Out") {
if (checkOutTime.text.isEmpty) checkOutTimeError = "Please select check-out time";
if (checkOutLocation.text.isEmpty) checkOutLocError = "Please enter check-out location";
if (checkOutDescription.text.isEmpty) checkOutDescError = "Please enter description";
if (checkOutProof == null) checkOutProofError = "Please attach check-out proof";
}
// --- Stop if any error ---
if ([
dateError,
typeError,
checkInTimeError,
checkInLocError,
checkInDescError,
checkInProofError,
checkOutTimeError,
checkOutLocError,
checkOutDescError,
checkOutProofError
].any((e) => e != null)) {
setState(() {});
return;
}
// --- Build data according to type ---
String? finalCheckInTime;
String? finalCheckInLoc;
File? finalCheckInProof;
String? finalCheckOutTime;
String? finalCheckOutLoc;
File? finalCheckOutProof;
String? finalNote;
if (selectedType == "Check In") {
finalCheckInTime = checkInTime.text;
finalCheckInLoc = checkInLocation.text;
finalCheckInProof = File(checkInProof!.path);
finalNote = checkInDescription.text;
} else if (selectedType == "Check Out") {
finalCheckOutTime = checkOutTime.text;
finalCheckOutLoc = checkOutLocation.text;
finalCheckOutProof = File(checkOutProof!.path);
finalNote = checkOutDescription.text;
} else if (selectedType == "Check In/Out") {
finalCheckInTime = checkInTime.text;
finalCheckInLoc = checkInLocation.text;
finalCheckInProof = File(checkInProof!.path);
finalCheckOutTime = checkOutTime.text;
finalCheckOutLoc = checkOutLocation.text;
finalCheckOutProof = File(checkOutProof!.path);
finalNote = "${checkInDescription.text} / ${checkOutDescription.text}";
}
// --- Submit to provider ---
await provider.addAttendanceRequest(
context,
process: "Manual",
type: selectedType!,
loc: selectedType == "Check In"
? checkInLocation.text
: selectedType == "Check Out"
? checkOutLocation.text
: "${checkInLocation.text}, ${checkOutLocation.text}",
checkDate: provider.dateController.text,
checkInTime: finalCheckInTime,
checkInLoc: finalCheckInLoc,
checkInProof: finalCheckInProof,
checkOutTime: finalCheckOutTime,
checkOutLoc: finalCheckOutLoc,
checkOutProof: finalCheckOutProof,
note: finalNote,
);
// Check the response from provider
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 ---
setState(() {
selectedType = null;
provider.dateController.clear();
checkInTime.clear();
checkInLocation.clear();
checkInDescription.clear();
checkInProof = null;
checkOutTime.clear();
checkOutLocation.clear();
checkOutDescription.clear();
checkOutProof = null;
});
_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";
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
);
}
}
Future<bool> _onBackPressed(BuildContext context) async {
Navigator.pop(context, true);
return true;
}
@override
Widget build(BuildContext context) {
switch (_source.keys.toList()[0]) {
case ConnectivityResult.mobile:
case ConnectivityResult.wifi:
connection = 'Online';
break;
default:
connection = 'Offline';
}
return (connection == "Online")
? Platform.isAndroid
? WillPopScope(
onWillPop: () => _onBackPressed(context),
child: SafeArea(
top: false,
bottom: true,
child: _scaffold(context),
),
)
: _scaffold(context)
: NoNetwork(context);
}
Widget _scaffold(BuildContext context) {
return Consumer<Attendancelistprovider>(
builder: (context, provider, child) {
return Scaffold(
backgroundColor: Colors.white,
appBar: appbar2New(
context,
"Add Manual Attendance",
() {},
SizedBox.shrink(),
0xFFFFFFFF,
),
body: Scrollbar(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TextWidget(context, "Date"),
GestureDetector(
onTap: () => provider.showDatePickerDialog(context),
child: AbsorbPointer(
child: textControllerWidget(
context,
provider.dateController,
"Date",
"Select Date",
(v) {},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
),
),
errorWidget(context, dateError),
TextWidget(context, "Type"),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>(
isExpanded: true,
hint: const Text("Select Type"),
items: types
.map((e) => DropdownMenuItem(
value: e,
child: Text(e),
))
.toList(),
value: selectedType,
onChanged: (val) {
setState(() => selectedType = val);
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData,
menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
],
),
),
errorWidget(context, typeError),
if (selectedType == "Check In" || selectedType == "Check In/Out")
_buildSection("Check In"),
if (selectedType == "Check Out" || selectedType == "Check In/Out")
_buildSection("Check Out"),
SizedBox(height: 80),
],
),
),
),
bottomNavigationBar: InkResponse(
onTap: provider.isSubmitting ? null : () => _submitForm(context),
child: Container(
height: 45,
alignment: Alignment.center,
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: provider.isSubmitting
? CircularProgressIndicator.adaptive(
valueColor: AlwaysStoppedAnimation(AppColors.white),
)
: const Text(
"Submit",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
),
),
);
},
);
}
Widget _buildSection(String title) {
final isCheckIn = title == "Check In";
final timeCtrl = isCheckIn ? checkInTime : checkOutTime;
final locCtrl = isCheckIn ? checkInLocation : checkOutLocation;
final descCtrl = isCheckIn ? checkInDescription : checkOutDescription;
final proofFile = isCheckIn ? checkInProof : checkOutProof;
final proofError = isCheckIn ? checkInProofError : checkOutProofError;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TextWidget(context, "$title Time"),
GestureDetector(
onTap: () => _pickTime(timeCtrl), // ⏰ open time picker
child: AbsorbPointer(
child: textControllerWidget(
context,
timeCtrl,
"$title Time",
"Select Time",
(v) {},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
),
),
errorWidget(context, isCheckIn ? checkInTimeError : checkOutTimeError),
textControllerWidget(
context,
locCtrl,
"$title Location",
"Enter Location",
(v) {},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
errorWidget(context, isCheckIn ? checkInLocError : checkOutLocError),
textControllerWidget(
context,
descCtrl,
"$title Description",
"Enter Description",
(v) {},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.done,
),
errorWidget(context, isCheckIn ? checkInDescError : checkOutDescError),
InkResponse(
onTap: () => _pickFile(isCheckIn),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 45,
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.app_blue, width: 0.5),
),
child: Center(
child: Text(
"Attach $title Proof",
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
),
),
),
if (proofFile != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
proofFile.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.red, size: 18),
onPressed: () {
setState(() {
if (isCheckIn) checkInProof = null;
else checkOutProof = null;
});
},
)
],
),
errorWidget(context, proofError),
],
);
}
}
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart';
import '../../Notifiers/hrmProvider/tourExpensesProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonServices.dart';
import '../../Utils/commonWidgets.dart';
import '../../Utils/dropdownTheme.dart';
class AddBillScreen extends StatefulWidget {
final String pageTitleName;
const AddBillScreen({super.key, required this.pageTitleName});
@override
State<AddBillScreen> createState() => _AddBillScreenState();
}
class _AddBillScreenState extends State<AddBillScreen> {
final Dropdowntheme ddtheme = Dropdowntheme();
final List<FocusNode> focusNodes = List.generate(8, (index) => FocusNode());
Map _source = {ConnectivityResult.mobile: true};
final MyConnectivity _connectivity = MyConnectivity.instance;
final TextEditingController placeController = TextEditingController();
final TextEditingController noteController = TextEditingController();
// Validation errors
String? placeError;
String? daAmountError;
String? tourTypeError;
String? tourDateError;
String? noteError;
List<Map<String, String>> travelExpenses = [];
List<Map<String, String>> hotelExpenses = [];
List<Map<String, String>> otherExpenses = [];
List<File> travelImages = [];
List<File> hotelImages = [];
List<File> otherImages = [];
String? selectedDAAmount;
String? selectedTourType;
String? selectedTravelType;
@override
void initState() {
super.initState();
_connectivity.initialise();
_connectivity.myStream.listen((source) {
setState(() => _source = source);
});
// Add listeners to clear errors when user starts typing
placeController.addListener(() {
if (placeError != null && placeController.text.isNotEmpty) {
setState(() => placeError = null);
}
});
noteController.addListener(() {
if (noteError != null && noteController.text.isNotEmpty) {
setState(() => noteError = null);
}
});
Future.microtask(() {
final provider = Provider.of<TourExpensesProvider>(context, listen: false);
provider.fetchTourExpensesAddView(context, "0"); // fresh bill
});
}
@override
void dispose() {
placeController.dispose();
noteController.dispose();
for (var node in focusNodes) {
node.dispose();
}
_connectivity.disposeStream();
super.dispose();
}
Future<bool> _onBackPressed(BuildContext context) async {
Navigator.pop(context, true);
return true;
}
// Function to validate all fields
bool validateFields() {
String? newPlaceError = placeController.text.isEmpty ? "Place of visit is required" : null;
String? newDaAmountError = selectedDAAmount == null ? "DA Amount is required" : null;
String? newTourTypeError = selectedTourType == null ? "Tour Type is required" : null;
String? newTourDateError = Provider.of<TourExpensesProvider>(context, listen: false).dateController.text.isEmpty
? "Tour Date is required"
: null;
String? newNoteError = noteController.text.isEmpty ? "Note is required" : null;
// Only update if there are actual changes to avoid unnecessary rebuilds
if (placeError != newPlaceError ||
daAmountError != newDaAmountError ||
tourTypeError != newTourTypeError ||
tourDateError != newTourDateError ||
noteError != newNoteError) {
setState(() {
placeError = newPlaceError;
daAmountError = newDaAmountError;
tourTypeError = newTourTypeError;
tourDateError = newTourDateError;
noteError = newNoteError;
});
}
return newPlaceError == null &&
newDaAmountError == null &&
newTourTypeError == null &&
newTourDateError == null &&
newNoteError == null;
}
// Format date to "02 Sep 2025" format
String _formatDate(DateTime date) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return '${date.day.toString().padLeft(2, '0')} ${months[date.month - 1]} ${date.year}';
}
@override
Widget build(BuildContext context) {
switch (_source.keys.toList()[0]) {
case ConnectivityResult.mobile:
case ConnectivityResult.wifi:
connection = 'Online';
break;
case ConnectivityResult.none:
default:
connection = 'Offline';
}
return (connection == "Online")
? Platform.isAndroid
? WillPopScope(
onWillPop: () => _onBackPressed(context),
child: SafeArea(
top: false, bottom: true, child: _scaffold(context)),
)
: _scaffold(context)
: NoNetwork(context);
}
Future<File?> pickImage(BuildContext context) async {
final ImagePicker picker = ImagePicker();
return showModalBottomSheet<File?>(
context: context,
builder: (ctx) {
return SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Pick from Gallery'),
onTap: () async {
final picked = await picker.pickImage(source: ImageSource.gallery);
Navigator.pop(ctx, picked != null ? File(picked.path) : null);
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Take a Photo'),
onTap: () async {
final picked = await picker.pickImage(source: ImageSource.camera);
Navigator.pop(ctx, picked != null ? File(picked.path) : null);
},
),
],
),
);
},
);
}
Widget _scaffold(BuildContext context) {
return Consumer<TourExpensesProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return Scaffold(
body: Container(child: Center(child: CircularProgressIndicator(color: Colors.blue)),),
);
}
if (provider.errorMessage != null) {
return Scaffold(body: Center(child: Text(provider.errorMessage!)));
}
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: AppColors.scaffold_bg_color,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
widget.pageTitleName,
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
leading: IconButton(
icon: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
onPressed: () => Navigator.pop(context),
),
),
body: Scrollbar(
thumbVisibility: false,
child: SingleChildScrollView(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(20),
),
margin: EdgeInsets.only(top: 10, left: 10, right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
textControllerWidget(
context,
placeController,
"Place of Visit",
"Enter Place",
(value) {
// Clear error when user types
if (placeError != null && value.isNotEmpty) {
setState(() => placeError = null);
}
},
TextInputType.text,
false,
null,
focusNodes[0],
focusNodes[1],
TextInputAction.next,
),
errorWidget(context, placeError),
TextWidget(context, "DA Amount"),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>(
isExpanded: true,
hint: Text(
'Select DA Amount',
style: TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
items: provider.daAmountList
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
value: selectedDAAmount,
onChanged: (String? value) {
setState(() {
selectedDAAmount = value;
if (daAmountError != null) daAmountError = null;
});
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData,
menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
],
),
),
errorWidget(context, daAmountError),
TextWidget(context, "Tour Type"),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>(
isExpanded: true,
hint: Text(
'Select Tour Type',
style: TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
items: provider.tourTypeList
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
value: selectedTourType,
onChanged: (String? value) {
setState(() {
selectedTourType = value;
if (tourTypeError != null) tourTypeError = null;
});
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData,
menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
],
),
),
errorWidget(context, tourTypeError),
TextWidget(context, "Tour Date"),
GestureDetector(
onTap: () async {
final d = await provider.showDatePickerDialog(context, isFromDate: true);
if (d != null) {
provider.dateController.text = _formatDate(d);
if (tourDateError != null) {
setState(() => tourDateError = null);
}
}
},
child: Container(
height: 50,
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: TextFormField(
controller: provider.dateController,
enabled: false,
decoration: InputDecoration(
hintText: "Select Tour Date",
hintStyle: TextStyle(
fontWeight: FontWeight.w400,
color: Color(0xFFB4BEC0),
fontSize: 14,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
),
),
),
),
errorWidget(context, tourDateError),
textControllerWidget(
context,
noteController,
"Note",
"Enter Note",
(value) {
// Clear error when user types
if (noteError != null && value.isNotEmpty) {
setState(() => noteError = null);
}
},
TextInputType.text,
false,
null,
focusNodes[2],
focusNodes[3],
TextInputAction.next,
300, // Allow up to 300 characters
),
errorWidget(context, noteError),
const SizedBox(height: 16),
/// Travel Expenses Section
sectionHeader("Travel Expenses", onAddTap: () {
showAddTravelExpenseSheet(
context,
travelExpenses,
() => setState(() {}),
provider.travelTypeList,
travelImages,
);
}),
if (travelExpenses.isNotEmpty) travelExpenseList(travelExpenses),
/// Hotel Expenses Section
sectionHeader("Hotel Expenses", onAddTap: () {
showAddHotelExpenseSheet(
context,
hotelExpenses,
() => setState(() {}),
provider,
hotelImages,
);
}),
if (hotelExpenses.isNotEmpty) hotelExpenseList(hotelExpenses),
/// Other Expenses Section
sectionHeader("Other Expenses", onAddTap: () {
showAddOtherExpenseSheet(
context,
otherExpenses,
() => setState(() {}),
provider,
otherImages,
);
}),
if (otherExpenses.isNotEmpty) otherExpenseList(otherExpenses),
const SizedBox(height: 80),
],
),
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: InkResponse(
onTap: () async {
// Validate all fields first
if (!validateFields()) {
return;
}
final provider = Provider.of<TourExpensesProvider>(context, listen: false);
provider.dateController.clear();
tourDateError = null;
final success = await provider.addTourBill(
context: context,
placeOfVisit: placeController.text,
daAmount: selectedDAAmount ?? "",
tourType: selectedTourType ?? "",
tourDate: provider.dateController.text,
travelExpenses: travelExpenses.map((e) => e.map((k, v) => MapEntry(k, v as dynamic))).toList(),
hotelExpenses: hotelExpenses.map((e) => e.map((k, v) => MapEntry(k, v as dynamic))).toList(),
otherExpenses: otherExpenses.map((e) => e.map((k, v) => MapEntry(k, v as dynamic))).toList(),
travelImages: travelImages,
hotelImages: hotelImages,
otherImages: otherImages,
);
print("image================== $travelImages");
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Tour Bill Submitted Successfully")),
);
Navigator.pop(context, true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(provider.errorMessage ?? "Failed to submit bill")),
);
}
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: const Text(
"Submit",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
),
),
);
},
);
}
Widget sectionHeader(String title, {VoidCallback? onAddTap}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
fontFamily: "JakartaMedium",
)),
const SizedBox(height: 6),
Container(
height: 45,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400, width: 0.7),
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onAddTap,
child: const Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add, color: Colors.blue),
SizedBox(width: 6),
Text("Add Expenses", style: TextStyle(
color: Colors.blue,
fontSize: 14,
fontFamily: "JakartaMedium",
)),
],
),
),
),
),
const SizedBox(height: 10),
],
);
}
Widget travelExpenseList(List<Map<String, String>> items) {
return Container(
height: 90,
margin: const EdgeInsets.only(bottom: 12),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
final exp = items[index];
return Container(
width: 200,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
"assets/svg/hrm/travel_ic.svg",
height: 20,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
exp["travel_type"] ?? "Travel",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
fontFamily: "JakartaMedium",
),
overflow: TextOverflow.ellipsis,
),
if (exp["from"] != null && exp["to"] != null) ...[
const SizedBox(height: 2),
Text(
"${exp["from"]}${exp["to"]}",
style: TextStyle(
fontSize: 12,
color: AppColors.grey_semi,
fontFamily: "JakartaMedium",
),
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 4),
Text(
"₹${exp["amount"] ?? "0"}",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
],
),
),
],
),
);
},
),
);
}
Widget hotelExpenseList(List<Map<String, String>> items) {
return Container(
height: 90,
margin: const EdgeInsets.only(bottom: 12),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
final exp = items[index];
return Container(
width: 160,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
"assets/svg/hrm/hotel_ic.svg",
height: 20,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
exp["hotel_name"] ?? "-",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
fontFamily: "JakartaMedium",
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
"₹${exp["amount"] ?? "0"}",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
],
),
),
],
),
);
},
),
);
}
Widget otherExpenseList(List<Map<String, String>> items) {
return Container(
height: 90,
margin: const EdgeInsets.only(bottom: 12),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
final exp = items[index];
return Container(
width: 160,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
"assets/svg/hrm/books_ic.svg",
height: 20,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
exp["description"] ?? "-",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
fontFamily: "JakartaMedium",
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
"₹${exp["amount"] ?? "0"}",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 2),
Text(
exp["date"] != null
? exp["date"]!.split("T").first
: "-",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.grey_semi,
fontFamily: "JakartaMedium",
),
),
],
),
),
],
),
);
},
),
);
}
Future<File?> pickFile() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);
if (result != null && result.files.isNotEmpty) {
return File(result.files.single.path!);
}
return null;
}
// --- Travel Expense BottomSheet ---
Future<void> showAddTravelExpenseSheet(
BuildContext context,
List<Map<String, String>> travelExpenses,
VoidCallback onUpdated,
List<String> travelTypes,
List<File> travelImages,
) {
final fromController = TextEditingController();
final toController = TextEditingController();
final fareController = TextEditingController();
String? selectedTravelType;
File? billFile;
String? fromError, toError, typeError, fareError, billError;
// Listeners to clear errors when user starts typing
fromController.addListener(() {
if (fromError != null && fromController.text.isNotEmpty) {
fromError = null;
}
});
toController.addListener(() {
if (toError != null && toController.text.isNotEmpty) {
toError = null;
}
});
fareController.addListener(() {
if (fareError != null && fareController.text.isNotEmpty) {
fareError = null;
}
});
return showModalBottomSheet(
useSafeArea: true,
isDismissible: true,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: Colors.white,
enableDrag: true,
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
// Function to update state and clear errors when fields change
void updateState(VoidCallback fn) {
setState(() {
fn();
});
}
// Function to validate fields and show errors if needed
bool validateFields() {
String? newFromError = fromController.text.isEmpty ? "From is required" : null;
String? newToError = toController.text.isEmpty ? "To is required" : null;
String? newTypeError = selectedTravelType == null ? "Please select type" : null;
String? newFareError = fareController.text.isEmpty ? "Fare is required" : null;
String? newBillError = billFile == null ? "Attach bill required" : null;
// Only update if there are actual changes to avoid unnecessary rebuilds
if (fromError != newFromError ||
toError != newToError ||
typeError != newTypeError ||
fareError != newFareError ||
billError != newBillError) {
updateState(() {
fromError = newFromError;
toError = newToError;
typeError = newTypeError;
fareError = newFareError;
billError = newBillError;
});
}
return newFromError == null &&
newToError == null &&
newTypeError == null &&
newFareError == null &&
newBillError == null;
}
Widget errorText(String? msg) => msg == null
? const SizedBox()
: Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
msg,
style: 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,
children: [
Text(
"Add Travel Expense",
style: TextStyle(
fontSize: 16,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 16),
textControllerWidget(
context,
fromController,
"From",
"Enter Starting Location",
(value) {
// Clear error when user types
if (fromError != null && value.isNotEmpty) {
updateState(() => fromError = null);
}
},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
errorText(fromError),
const SizedBox(height: 12),
textControllerWidget(
context,
toController,
"To",
"Enter Destination Location",
(value) {
// Clear error when user types
if (toError != null && value.isNotEmpty) {
updateState(() => toError = null);
}
},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
errorText(toError),
const SizedBox(height: 12),
TextWidget(context, "Travel Type"),
DropdownButtonHideUnderline(
child: Container(
width: double.infinity,
child: DropdownButton2<String>(
isExpanded: true,
hint: Text(
"Select Travel Type",
style: TextStyle(
fontSize: 14,
color: Color(0xFFB4BEC0),
)
),
items: travelTypes.map((t) =>
DropdownMenuItem(
value: t,
child: Text(
t,
style: TextStyle(fontSize: 14),
)
)
).toList(),
value: selectedTravelType,
onChanged: (val) {
updateState(() {
selectedTravelType = val;
if (typeError != null) typeError = null;
});
},
buttonStyleData: ButtonStyleData(
height: 50,
width: double.infinity,
padding: const EdgeInsets.only(left: 14, right: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: AppColors.text_field_color,
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: 200,
width: MediaQuery.of(context).size.width - 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: Colors.white,
),
offset: const Offset(0, -5),
scrollbarTheme: ScrollbarThemeData(
radius: const Radius.circular(40),
thickness: MaterialStateProperty.all<double>(6),
thumbVisibility: MaterialStateProperty.all<bool>(true),
),
),
),
),
),
errorText(typeError),
const SizedBox(height: 12),
textControllerWidget(
context,
fareController,
"Fare Amount",
"Enter Amount",
(value) {
// Clear error when user types
if (fareError != null && value.isNotEmpty) {
updateState(() => fareError = null);
}
},
TextInputType.number,
false,
FilteringTextInputFormatter.digitsOnly,
null,
null,
TextInputAction.next,
),
errorText(fareError),
const SizedBox(height: 12),
InkResponse(
onTap: () async {
final f = await pickImage(context);
if (f != null) {
updateState(() {
billFile = f;
if (billError != null) billError = null;
});
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.app_blue, width: 0.5),
),
child: Center(
child: Text(
billFile == null ? "Attach Bill" : "Bill Attached",
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
),
),
),
errorText(billError),
if (billFile != null) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: Text(
"${billFile!.path.split('/').last}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: AppColors.semi_black,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
flex: 1,
child: InkResponse(
onTap: () => updateState(() => billFile = null),
child: SvgPicture.asset(
"assets/svg/ic_close.svg",
width: 15,
height: 15,
),
),
),
],
),
)
],
const SizedBox(height: 20),
InkResponse(
onTap: () {
// Validate all fields
if (validateFields()) {
travelExpenses.add({
"from": fromController.text,
"to": toController.text,
"travel_type": selectedTravelType!,
"amount": fareController.text,
});
travelImages.add(billFile!);
onUpdated();
Navigator.pop(context);
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
"Submit",
style: TextStyle(
color: Colors.white,
fontFamily: "JakartaMedium",
fontSize: 15,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Hotel Expense BottomSheet ---
Future<void> showAddHotelExpenseSheet(
BuildContext context,
List<Map<String, String>> hotelExpenses,
VoidCallback onUpdated,
TourExpensesProvider provider,
List<File> hotelImages,
) {
final hotelController = TextEditingController();
final amountController = TextEditingController();
DateTime? fromDate, toDate;
File? billFile;
String? hotelError, fromDateError, toDateError, amountError, billError;
// Listeners to clear errors when user starts typing
hotelController.addListener(() {
if (hotelError != null && hotelController.text.isNotEmpty) {
hotelError = null;
}
});
amountController.addListener(() {
if (amountError != null && amountController.text.isNotEmpty) {
amountError = null;
}
});
return showModalBottomSheet(
useSafeArea: true,
isDismissible: true,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: Colors.white,
enableDrag: true,
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
// Function to update state and clear errors
void updateState(VoidCallback fn) {
setState(() {
fn();
});
}
// Function to validate fields and show errors
bool validateFields() {
String? newHotelError = hotelController.text.isEmpty ? "Hotel name required" : null;
String? newFromDateError = fromDate == null ? "From date required" : null;
String? newToDateError = toDate == null ? "To date required" : null;
String? newAmountError = amountController.text.isEmpty ? "Amount required" : null;
String? newBillError = billFile == null ? "Attach bill required" : null;
if (hotelError != newHotelError ||
fromDateError != newFromDateError ||
toDateError != newToDateError ||
amountError != newAmountError ||
billError != newBillError) {
updateState(() {
hotelError = newHotelError;
fromDateError = newFromDateError;
toDateError = newToDateError;
amountError = newAmountError;
billError = newBillError;
});
}
return newHotelError == null &&
newFromDateError == null &&
newToDateError == null &&
newAmountError == null &&
newBillError == null;
}
Widget errorText(String? msg) => msg == null
? const SizedBox()
: Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
msg,
style: 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,
children: [
Text(
"Add Hotel Expense",
style: TextStyle(
fontSize: 16,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 16),
textControllerWidget(
context,
hotelController,
"Hotel Name",
"Enter Hotel Name",
(value) {
// Clear error
if (hotelError != null && value.isNotEmpty) {
updateState(() => hotelError = null);
}
},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
errorText(hotelError),
const SizedBox(height: 12),
Text(
"Stay Duration",
style: TextStyle(
fontSize: 14,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () async {
final d = await provider.showDatePickerDialog(context, isFromDate: true);
if (d != null) {
updateState(() {
fromDate = d;
if (fromDateError != null) fromDateError = null;
});
}
},
child: Container(
height: 50,
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
fromDate == null
? "From Date"
: DateFormat("dd MMM yyyy").format(fromDate!),
style: TextStyle(
fontSize: 14,
color: fromDate == null ? Color(0xFFB4BEC0) : Colors.black,
fontFamily: "JakartaMedium",
),
),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
onTap: () async {
final d = await provider.showDatePickerDialog(context, isFromDate: false);
if (d != null) {
updateState(() {
toDate = d;
if (toDateError != null) toDateError = null;
});
}
},
child: Container(
height: 50,
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
toDate == null
? "To Date"
: DateFormat("dd MMM yyyy").format(toDate!),
style: TextStyle(
fontSize: 14,
color: toDate == null ? Color(0xFFB4BEC0) : Colors.black,
fontFamily: "JakartaMedium",
),
),
),
),
),
),
],
),
if (fromDateError != null) errorText(fromDateError),
if (toDateError != null) errorText(toDateError),
const SizedBox(height: 12),
textControllerWidget(
context,
amountController,
"Amount",
"Enter Amount",
(value) {
// Clear error
if (amountError != null && value.isNotEmpty) {
updateState(() => amountError = null);
}
},
TextInputType.number,
false,
FilteringTextInputFormatter.digitsOnly,
null,
null,
TextInputAction.next,
),
errorText(amountError),
const SizedBox(height: 12),
InkResponse(
onTap: () async {
final f = await pickImage(context);
if (f != null) {
updateState(() {
billFile = f;
if (billError != null) billError = null;
});
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.app_blue, width: 0.5),
),
child: Center(
child: Text(
billFile == null ? "Attach Bill" : "Bill Attached",
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
),
),
),
errorText(billError),
if (billFile != null) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text("Attached: ${billFile!.path.split('/').last}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => updateState(() => billFile = null),
),
],
)
],
const SizedBox(height: 20),
InkResponse(
onTap: () {
// Validate all fields
if (validateFields()) {
hotelExpenses.add({
"hotel_name": hotelController.text,
"from_date": fromDate!.toIso8601String(),
"to_date": toDate!.toIso8601String(),
"amount": amountController.text,
});
hotelImages.add(billFile!);
onUpdated();
Navigator.pop(context);
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
"Submit",
style: TextStyle(
color: Colors.white,
fontFamily: "JakartaMedium",
fontSize: 15,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Other Expense BottomSheet ---
Future<void> showAddOtherExpenseSheet(
BuildContext context,
List<Map<String, String>> otherExpenses,
VoidCallback onUpdated,
TourExpensesProvider provider,
List<File> otherImages,
) {
final titleController = TextEditingController();
final amountController = TextEditingController();
File? billFile;
DateTime? date;
String? titleError, amountError, dateError, billError;
// Listeners to clear errors when user starts typing
titleController.addListener(() {
if (titleError != null && titleController.text.isNotEmpty) {
titleError = null;
}
});
amountController.addListener(() {
if (amountError != null && amountController.text.isNotEmpty) {
amountError = null;
}
});
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();
});
}
// Function to validate fields and show errors
bool validateFields() {
String? newDateError = date == null ? "Date required" : null;
String? newTitleError = titleController.text.isEmpty ? "Title required" : null;
String? newAmountError = amountController.text.isEmpty ? "Amount required" : null;
String? newBillError = billFile == null ? "Attach bill required" : null;
if (dateError != newDateError ||
titleError != newTitleError ||
amountError != newAmountError ||
billError != newBillError) {
updateState(() {
dateError = newDateError;
titleError = newTitleError;
amountError = newAmountError;
billError = newBillError;
});
}
return newDateError == null &&
newTitleError == null &&
newAmountError == null &&
newBillError == null;
}
Widget errorText(String? msg) => msg == null
? const SizedBox()
: Padding(
padding: const EdgeInsets.only(top: 4, left: 4),
child: Text(
msg,
style: 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,
children: [
Text(
"Add Other Expense",
style: TextStyle(
fontSize: 16,
color: AppColors.app_blue,
fontFamily: "JakartaMedium",
),
),
const SizedBox(height: 16),
TextWidget(context, "Date"),
GestureDetector(
onTap: () async {
final d = await provider.showDatePickerDialog(context, isFromDate: false);
if (d != null) {
updateState(() {
date = d;
if (dateError != null) dateError = null;
});
}
},
child: Container(
height: 50,
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(16),
),
child: Text(
date == null
? "Select Date"
: DateFormat("dd MMM yyyy").format(date!),
style: TextStyle(
fontSize: 14,
color: date == null ? const Color(0xFFB4BEC0) : Colors.black,
fontFamily: "JakartaMedium",
),
),
),
),
),
errorText(dateError),
const SizedBox(height: 12),
textControllerWidget(
context,
titleController,
"Description",
"Enter Title",
(value) {
// Clear error
if (titleError != null && value.isNotEmpty) {
updateState(() => titleError = null);
}
},
TextInputType.text,
false,
null,
null,
null,
TextInputAction.next,
),
errorText(titleError),
const SizedBox(height: 12),
textControllerWidget(
context,
amountController,
"Amount",
"Enter Amount",
(value) {
// Clear error
if (amountError != null && value.isNotEmpty) {
updateState(() => amountError = null);
}
},
TextInputType.number,
false,
FilteringTextInputFormatter.digitsOnly,
null,
null,
TextInputAction.next,
),
errorText(amountError),
const SizedBox(height: 12),
InkResponse(
onTap: () async {
final f = await pickImage(context);
if (f != null) {
updateState(() {
billFile = f;
if (billError != null) billError = null;
});
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.app_blue, width: 0.5),
),
child: Center(
child: Text(
billFile == null ? "Attach Bill" : "Bill Attached",
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
),
),
),
errorText(billError),
if (billFile != null) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: Text(
"${billFile!.path.split('/').last}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: AppColors.semi_black,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
flex: 1,
child: InkResponse(
onTap: () => updateState(() => billFile = null),
child: SvgPicture.asset(
"assets/svg/ic_close.svg",
width: 15,
height: 15,
),
),
),
],
),
)
],
const SizedBox(height: 20),
InkResponse(
onTap: () {
// Validate all fields
if (validateFields()) {
otherExpenses.add({
"description": titleController.text,
"amount": amountController.text,
"date": date!.toIso8601String(),
});
otherImages.add(billFile!);
onUpdated();
Navigator.pop(context);
}
},
child: Container(
height: 45,
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
"Submit",
style: TextStyle(
color: Colors.white,
fontFamily: "JakartaMedium",
fontSize: 15,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
}
\ No newline at end of file
import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:geocoding/geocoding.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../Notifiers/hrmProvider/attendanceDetailsProvider.dart';
import '../../Notifiers/HomeScreenNotifier.dart';
import '../../Utils/app_colors.dart';
import '../finance/FileViewer.dart';
/// screen for attendance details
class AttendanceRequestDetailScreen extends StatefulWidget {
final attendanceListId;
const AttendanceRequestDetailScreen({super.key, required this.attendanceListId});
@override
State<AttendanceRequestDetailScreen> createState() =>
_AttendanceRequestDetailScreenState();
}
class _AttendanceRequestDetailScreenState
extends State<AttendanceRequestDetailScreen> {
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,
),
),
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,
),
),
),
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,
),
),
],
),
),
/// 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),
],
// 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),
],
),
);
},
),
);
},
)
);
}
/// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value, double scaleFactor) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // Align top if wraps
children: [
// Label
Expanded(
flex: 5, // keep same ratio as other tiles
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
SizedBox(width: 4,),
// Value
Expanded(
flex: 5, // take remaining width
child: Text(
value ?? "-",
style: const TextStyle(
fontSize: 14,
color: Color(0xFF818181),
),
softWrap: true,
overflow: TextOverflow.visible, // wrap instead of clipping
),
),
],
),
);
}
/// for location
Widget buildLocationTile(String label, String? value, double scaleFactor) {
return FutureBuilder<String>(
future: getReadableLocation(value),
builder: (context, snapshot) {
final locationText = snapshot.data ?? "-";
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // aligns top when wrapping
children: [
// Label
Expanded(
flex: 5, // ratio (adjust same as your Date/Time tile)
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
// Value (Clickable Location)
Expanded(
flex: 5, // take remaining space
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);
}
},
child: Text(
locationText,
style: const TextStyle(
fontSize: 14,
color: Colors.blue,
decoration: TextDecoration.underline,
fontWeight: FontWeight.w400,
),
softWrap: true,
overflow: TextOverflow.visible,
),
),
),
],
),
);
},
);
}
Future<String> getReadableLocation(String? value) async {
if (value == null) return "-";
try {
List<Location> locations = await locationFromAddress(value);
List<Placemark> placemarks = await placemarkFromCoordinates(
locations[0].latitude,
locations[0].longitude,
);
return placemarks.first.locality ?? value;
} 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(
padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // align top when wrapped
children: [
// Label
Expanded(
flex: 5, // adjust ratio
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
SizedBox(width: 4,),
// Value (date + time)
Expanded(
flex: 5, // adjust ratio so both fill row
child: Text(
'${date ?? "-"}, ${time ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
softWrap: true, // allow wrapping
overflow: TextOverflow.visible,
),
),
],
),
);
}
Widget _buildSectionHeader(String title, double scaleFactor) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 9 * scaleFactor),
child: Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontFamily: "JakartaSemiBold",
),
),
SizedBox(width: 10 * scaleFactor),
Expanded(
child: DottedLine(
dashGapLength: 4,
dashGapColor: Colors.white,
dashColor: AppColors.grey_semi,
dashLength: 2,
lineThickness: 0.5,
),
),
],
),
);
}
/// Proof section (image/file path)
Widget _buildProofLink(BuildContext context, String label, String? filePath, double scaleFactor) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row(
children: [
Expanded(
flex: 5,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
Expanded(
flex: 0,
child: filePath != null
? InkWell(
onTap: () {
print("++++++++++++++++ImageUrel: $filePath");
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => Image.network(filePath),
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
),
);
},
child: const Text(
"View",
style: TextStyle(
fontSize: 14,
color: Colors.blue,
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
decoration: TextDecoration.underline
),
),
)
: const Text("-"),
),
],
),
);
}
Color getTextColor(value) {
var color = AppColors.approved_text_color;
switch (value) {
case 'Requested':
return AppColors.requested_text_color;
case 'Level 1 Approved':
return AppColors.approved_text_color;
case 'Level 1 Rejected':
return AppColors.rejected_text_color;
case 'Level 2 Approved':
return AppColors.approved_text_color;
case 'Level 2 Rejected':
return AppColors.rejected_text_color;
case 'Processed':
return AppColors.processed_text_color;
case 'Payment Rejected':
return AppColors.rejected_text_color;
}
return color;
}
Color getDecorationColor(value) {
var color = AppColors.approved_bg_color;
switch (value) {
case 'Requested':
return AppColors.requested_bg_color;
case 'Level 1 Approved':
return AppColors.approved_bg_color;
case 'Level 1 Rejected':
return AppColors.rejected_bg_color;
case 'Level 2 Approved':
return AppColors.approved_bg_color;
case 'Level 2 Rejected':
return AppColors.rejected_bg_color;
case 'Processed':
return AppColors.processed_bg_color;
case 'Payment Rejected':
return AppColors.rejected_bg_color;
}
return color;
}
}
\ No newline at end of file
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.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/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart';
import '../CommonFilter2.dart';
import '../commonDateRangeFilter.dart';
import 'AddLiveAttendance.dart';
class Attendancelist extends StatefulWidget {
const Attendancelist({super.key});
@override
State<Attendancelist> createState() => _AttendancelistState();
}
class _AttendancelistState extends State<Attendancelist> {
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// provider.fetchAttendanceRequests(context);
// });
// }
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: ChangeNotifierProvider(
create: (_) {
final provider = Attendancelistprovider();
Future.microtask(() {
provider.fetchAttendanceRequests(context);
});
return provider;
},
builder: (context, child) {
return Consumer<Attendancelistprovider>(
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(
"Attendance List",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
],
),
actions: [
InkResponse(
onTap: () async {
final result = await CommonFilter2().showFilterBottomSheet(context);
if (result != null) {
final provider = Provider.of<Attendancelistprovider>(context, listen: false);
provider.updateFiltersFromSheet(
context,
type: result['type'] ?? "All",
selectedValue: result['selectedValue'] ?? "This Month",
customRange: result['dateRange'],
);
}
},
child: SvgPicture.asset(
"assets/svg/filter_ic.svg",
height: 25,
),
),
const SizedBox(width: 20),
],
),
backgroundColor: const Color(0xFFF6F6F8),
body: Column(
children: [
/// Filter chips - show active filters
// if (provider.selectedType != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Wrap(
// spacing: 8,
// children: [
// if (provider.selectedType != "All")
// Chip(
// label: Text('Type: ${provider.selectedType}'),
// onDeleted: () {
// provider.setTypeFilter(context, "All");
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter(context, "This Month");
// },
// ),
// ],
// ),
// ),
/// Attendance 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 attendance records found",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
);
}
final list = provider.response!.requestList!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: list.length,
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,
),
),
);
},
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,
),
),
),
),
const SizedBox(width: 10),
/// Middle Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.type ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
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,
),
),
],
),
),
);
},
);
},
),
)
],
),
bottomNavigationBar: Container(
alignment: Alignment.bottomCenter,
height: 54,
decoration: const BoxDecoration(color: Colors.white),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: InkResponse(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddLiveAttendanceScreen(),
settings: const RouteSettings(
name: 'AddLiveAttendanceScreen',
),
),
).then((_) {
provider.fetchAttendanceRequests(context);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/hrm/live.svg"),
const SizedBox(width: 10),
Text("Live Request",
style: TextStyle(color: AppColors.semi_black)),
],
),
),
),
const SizedBox(width: 10),
SvgPicture.asset("assets/svg/crm/vertical_line_ic.svg"),
const SizedBox(width: 10),
Expanded(
child: InkResponse(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddManualAttendanceScreen(),
settings: const RouteSettings(
name: 'AddManualAttendanceScreen'),
),
).then((_) {
provider.fetchAttendanceRequests(context);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/hrm/manual.svg"),
const SizedBox(width: 10),
Text("Manual Request",
style: TextStyle(color: AppColors.semi_black)),
],
),
),
),
],
),
),
);
},
);
},
)
);
}
/// Avatar color generator
Color _getAvatarColor(value) {
var color = AppColors.approved_bg_color;
switch (value) {
case 'Requested':
return AppColors.requested_bg_color;
case 'Level 1 Approved':
return AppColors.approved_bg_color;
case 'Level 1 Rejected':
return AppColors.rejected_bg_color;
case 'Level 2 Approved':
return AppColors.approved_bg_color;
case 'Level 2 Rejected':
return AppColors.rejected_bg_color;
case 'Updated':
return AppColors.processed_bg_color;
case 'Payment Rejected':
return AppColors.rejected_bg_color;
}
return color;
}
Color _getTextColor(value) {
var color = AppColors.approved_text_color;
switch (value) {
case 'Requested':
return AppColors.requested_text_color;
case 'Level 1 Approved':
return AppColors.approved_text_color;
case 'Level 1 Rejected':
return AppColors.rejected_text_color;
case 'Level 2 Approved':
return AppColors.approved_text_color;
case 'Level 2 Rejected':
return AppColors.rejected_text_color;
case 'Updated':
return AppColors.processed_text_color;
}
return color;
}
getText(value) {
switch (value) {
case 'Requested':
return "R";
case 'Level 1 Approved':
return "L1A";
case 'Level 1 Rejected':
return "L1R";
case 'Level 2 Approved':
return "L2A";
case 'Level 2 Rejected':
return "L2R";
case 'Updated':
return "U";
default:
return "Requested";
}
}
}
\ No newline at end of file
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 '../../Utils/app_colors.dart';
import 'AttendanceRequestDetail.dart';
import 'LeaveApplicationScreen.dart';
import 'TourExpensesListScreen.dart';
import 'attendancelist.dart';
class HrmdashboardScreen extends StatefulWidget {
const HrmdashboardScreen({super.key});
@override
State<HrmdashboardScreen> createState() => _HrmdashboardScreenState();
}
class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
@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,
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(
"HRM",
style: TextStyle(
fontSize: 18,
height: 1.1,
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,
width: double.infinity,
fit: BoxFit.fitWidth,
),
),
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) => OrganizationStructureScreen(),
),
);
},
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),
],
),
),
),
],
),
),
/// 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(),
),
);
},
),
_buildTile(
label: "Leave Application",
subtitle: "Apply & Track",
assetIcon: "assets/svg/hrm/leaveApplication.svg",
txtColor: const Color(0xff1487C9),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LeaveApplicationListScreen(),
),
);
},
),
_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(),
),
);
},
),
_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(),
),
);
},
),
],
),
);
},
),
],
),
],
),
],
),
),
);
}
/// Reusable Tile Widget (Row style)
/// Reusable Tile Widget (Row style) - Updated to match design
Widget _buildTile({
required String label,
required String subtitle,
required String assetIcon, // SVG/PNG asset
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,
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",
),
),
],
),
),
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,
),
),
),
),
],
),
),
);
}
}
\ No newline at end of file
import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../Notifiers/hrmProvider/leaveApplicationDetailsProvider.dart';
import '../../Notifiers/HomeScreenNotifier.dart';
import '../../Utils/app_colors.dart';
import '../finance/FileViewer.dart';
/// Screen for leave application details
class LeaveApplicationDetailScreen extends StatefulWidget {
final String leaveRequestId;
const LeaveApplicationDetailScreen({super.key, required this.leaveRequestId});
@override
State<LeaveApplicationDetailScreen> createState() => _LeaveApplicationDetailScreenState();
}
class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScreen> {
@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,
),
),
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,
),
),
),
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,
),
),
const SizedBox(height: 2),
Text(
"Applied: ${details.appliedDate ?? "-"}",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
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),
),
),
),
),
],
),
),
/// 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),
],
),
),
],
),
),
),
const SizedBox(height: 30),
],
),
);
},
),
);
},
),
);
}
/// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
Expanded(
flex: 0,
child: Text(
value ?? "-",
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// For date range display
Widget _buildDateRangeTile(String label, String? fromDate, String? toDate) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
Expanded(
flex: 0,
child: Text(
'${fromDate ?? "-"} to ${toDate ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// For time range display
Widget _buildTimeRangeTile(String label, String? fromTime, String? toTime) {
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),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
),
),
),
Expanded(
flex: 0,
child: Text(
'${fromTime ?? "-"} to ${toTime ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// Section header with dotted line
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontFamily: "JakartaSemiBold",
),
),
const SizedBox(width: 10),
Expanded(
child: DottedLine(
dashGapLength: 4,
dashGapColor: Colors.white,
dashColor: AppColors.grey_semi,
dashLength: 2,
lineThickness: 0.5,
),
),
],
),
);
}
/// Status background color
Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_bg_color;
case 'rejected':
return AppColors.rejected_bg_color;
case 'requested':
default:
return AppColors.requested_bg_color;
}
}
/// Status text color
Color _getStatusTextColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_text_color;
case 'rejected':
return AppColors.rejected_text_color;
case 'requested':
default:
return AppColors.requested_text_color;
}
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../Notifiers/hrmProvider/leaveApplicationListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart';
import '../commonDateRangeFilter.dart';
import 'AddLeaveRequestScreen.dart';
import 'LeaveApplicationDetailScreen.dart';
class LeaveApplicationListScreen extends StatefulWidget {
const LeaveApplicationListScreen({super.key});
@override
State<LeaveApplicationListScreen> createState() => _LeaveApplicationListScreenState();
}
class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen> {
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final provider = Provider.of<LeaveApplicationListProvider>(context, listen: false);
// provider.fetchLeaveApplications(context);
// });
// }
@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,
);
}
}
},
child: SvgPicture.asset("assets/svg/filter_ic.svg", height: 25),
),
],
),
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);
// },
// ),
// ],
// ),
// ),
/// 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());
String dateToMonth = DateFormat("dd MMM yyyy").format(parsedToDate);
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationDetailScreen(
leaveRequestId: item.id.toString(),
),
),
).then((_) {
provider.fetchLeaveApplications(context);
});
},
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,
),
),
),
),
const SizedBox(width: 12),
/// Middle Section - Leave Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.leaveType ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
dateFromMonth ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
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),
// ),
// ),
// ),
],
),
),
);
},
);
},
),
)
],
),
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);
});
// 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,
),
),
),
),
);
},
);
},
);
}
/// Get status background color
Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_bg_color;
case 'rejected':
return AppColors.rejected_bg_color;
case 'requested':
default:
return AppColors.requested_bg_color;
}
}
/// 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;
case 'requested':
default:
return AppColors.requested_text_color;
}
}
/// Get status initials
String _getStatusInitials(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return "A";
case 'rejected':
return "R";
case 'requested':
default:
return "P"; // Pending
}
}
}
\ No newline at end of file