Commit f6fbe101 authored by Mohit Kumar's avatar Mohit Kumar
Browse files

AttendanceList

RewardList
TourExpenses
Implementation
parent 6d1deaf2
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_319_5695)">
<path d="M1.07683 7.38789C1.08816 7.31523 1.10027 7.24102 1.11238 7.16367L1.11706 7.13555C1.26472 6.20742 1.75886 5.28945 2.47917 4.53672C3.19949 3.78398 4.11238 3.23789 5.04988 3.04297C5.2741 2.99648 5.52136 2.95078 5.78738 2.90703C5.79805 2.70339 5.80925 2.49987 5.82097 2.29648C5.85105 1.78125 6.26355 1.31992 6.73738 1.27891C7.21121 1.23789 7.57956 1.63633 7.5616 2.16055C7.55535 2.33372 7.54923 2.50664 7.54324 2.6793C8.30066 2.60469 9.11355 2.55352 9.92605 2.53281C9.92605 2.37656 9.92748 2.22148 9.93035 2.06758C9.93659 1.53867 10.3268 1.10156 10.8014 1.10156C11.276 1.10156 11.6671 1.53945 11.6729 2.06758C11.6745 2.22383 11.6759 2.37891 11.6772 2.53281C12.4874 2.55352 13.2968 2.6043 14.0514 2.67852C14.0452 2.50586 14.0391 2.33307 14.0331 2.16016C14.0151 1.63555 14.3846 1.23594 14.8573 1.27813C15.33 1.32031 15.7436 1.78047 15.7737 2.2957C15.7854 2.49883 15.7966 2.70208 15.8073 2.90547C16.0768 2.94961 16.3276 2.99609 16.5542 3.04297C17.4917 3.23828 18.4034 3.78516 19.1249 4.53672C19.8464 5.28828 20.3393 6.20742 20.487 7.13555L20.4917 7.16406C20.5042 7.24219 20.5159 7.31523 20.5272 7.38789C14.0526 6.90977 7.55148 6.90977 1.07683 7.38789ZM20.8011 11.216C20.7686 13.5695 20.6038 14.341 20.4128 15.4801C20.4128 15.4898 20.4096 15.4992 20.4077 15.509C20.2432 16.4418 19.737 17.3734 19.0147 18.1445C18.2925 18.9156 17.3874 19.4797 16.4635 19.6895C15.1038 20.0051 12.8843 20.2672 10.8022 20.2582C8.72019 20.2672 6.50027 20.0051 5.1405 19.6895C4.21589 19.4801 3.31277 18.9137 2.58933 18.1449C1.86589 17.3762 1.36081 16.4422 1.19636 15.5094C1.19636 15.5 1.19285 15.4902 1.19128 15.4809C1.00027 14.341 0.835814 13.5707 0.803002 11.2164C0.794799 10.0418 0.838158 9.31328 0.893236 8.73633C7.48464 8.44414 14.1194 8.44414 20.7108 8.73633C20.7663 9.31289 20.8093 10.0418 20.8011 11.2164V11.216ZM7.19324 13.7902C7.1823 12.9789 6.54558 12.3148 5.76589 12.3027C5.58217 12.2984 5.39949 12.3316 5.22902 12.4002C5.05856 12.4689 4.9039 12.5716 4.7745 12.7021C4.6451 12.8326 4.54368 12.9882 4.47645 13.1592C4.40921 13.3302 4.37757 13.5132 4.38347 13.6969C4.40535 14.4781 5.05535 15.1574 5.8241 15.1977C6.59285 15.2379 7.20378 14.6012 7.19324 13.7902ZM12.2647 13.8266C12.2702 13.0082 11.6397 12.3477 10.8585 12.3473C10.0772 12.3469 9.44753 13.009 9.45222 13.8273C9.45691 14.6457 10.0882 15.3203 10.8585 15.3199C11.6288 15.3195 12.2589 14.6445 12.2647 13.8266ZM17.4155 13.6879C17.4214 13.5045 17.3898 13.3219 17.3226 13.1512C17.2554 12.9805 17.154 12.8253 17.0246 12.6952C16.8953 12.5651 16.7407 12.4627 16.5704 12.3945C16.4001 12.3263 16.2177 12.2936 16.0343 12.2984C15.253 12.3109 14.6178 12.9742 14.6061 13.7828C14.5944 14.5914 15.2046 15.2281 15.9733 15.1855C16.7421 15.143 17.3936 14.4691 17.4155 13.6879Z" fill="url(#paint0_linear_319_5695)"/>
</g>
<defs>
<linearGradient id="paint0_linear_319_5695" x1="12.1655" y1="23.2878" x2="-2.4722" y2="3.87753" gradientUnits="userSpaceOnUse">
<stop stop-color="#498D1E"/>
<stop offset="1" stop-color="#81C320"/>
</linearGradient>
<clipPath id="clip0_319_5695">
<rect width="20" height="20" fill="white" transform="translate(0.77832 0.679688)"/>
</clipPath>
</defs>
</svg>
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_319_5711)">
<path d="M22.595 9.39164C22.3853 7.75582 20.9282 6.3718 19.2232 6.14492C16.2184 5.84758 11.7789 5.82266 11.778 5.8334C9.85776 5.81363 5.36237 5.99668 4.33284 6.14492C3.29041 6.27941 2.34811 6.85434 1.71733 7.64582V7.64668C1.31471 8.15371 1.04143 8.75012 0.961078 9.39164C0.838617 10.3804 0.742367 11.7317 0.791351 13.5798C0.843343 15.4283 1.01307 16.7823 1.18666 17.7744C1.4853 19.4163 2.96256 20.8583 4.60655 21.1346C5.60256 21.3245 9.9308 21.5883 11.7785 21.5548C13.6261 21.5888 17.9544 21.3249 18.9504 21.1346C20.5944 20.8583 22.0716 19.4163 22.3703 17.7744C22.5434 16.7818 22.7131 15.4279 22.7656 13.5798C22.8146 11.7313 22.7179 10.3804 22.5958 9.39164H22.595ZM18.9388 15.2182C18.0974 15.2182 17.4151 14.5363 17.4151 13.6945C17.4151 12.8528 18.097 12.1709 18.9388 12.1709C19.7805 12.1709 20.4624 12.8528 20.4624 13.6945C20.4624 14.5363 19.7805 15.2182 18.9388 15.2182ZM4.2976 5.06555C5.42381 4.88852 10.0451 4.68914 11.7776 4.71492C13.2678 4.70074 16.8939 4.82664 18.5967 4.99465C18.0532 3.45809 16.6683 2.23477 15.0914 2.03281C13.9823 1.89445 12.469 1.78102 10.3996 1.80895C8.33022 1.83988 6.81428 1.99629 5.70182 2.16387C3.86448 2.45348 2.22307 4.06996 1.90768 5.92063C1.89994 5.96703 1.89221 6.01988 1.88362 6.07746C2.58787 5.55367 3.41588 5.18672 4.2976 5.06512V5.06555Z" fill="url(#paint0_linear_319_5711)"/>
</g>
<defs>
<linearGradient id="paint0_linear_319_5711" x1="0.790113" y1="11.5706" x2="27.2783" y2="27.6814" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC80B"/>
<stop offset="1" stop-color="#E89318"/>
</linearGradient>
<clipPath id="clip0_319_5711">
<rect width="22" height="22" fill="white" transform="translate(0.77832 0.679688)"/>
</clipPath>
</defs>
</svg>
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_319_5704)">
<path d="M10.4369 20.6078C10.6568 20.7035 10.908 20.7035 11.1279 20.6078C13.7295 19.475 19.0666 14.0719 19.0666 8.95938C19.0666 4.38672 15.3596 0.679688 10.7869 0.679688H10.7693C6.19629 0.679688 2.48926 4.38672 2.48926 8.95938C2.48926 14.0723 7.83457 19.4754 10.4365 20.6078H10.4369ZM10.7291 4.36875C12.0232 4.36875 13.0725 5.41797 13.0725 6.71211C13.0725 8.00625 12.0232 9.05547 10.7291 9.05547C9.43496 9.05547 8.38574 8.00625 8.38574 6.71211C8.38574 5.41797 9.43496 4.36875 10.7291 4.36875ZM6.90762 12.3672C6.86582 11.9605 6.88418 11.6184 6.88418 11.618C6.88418 11.618 6.86582 11.3973 6.90762 10.9973C6.90762 10.9938 6.9084 10.9906 6.90879 10.9871C6.94316 10.659 7.09082 10.3535 7.32519 10.1187C7.55957 9.88477 7.87012 9.73281 8.20019 9.69727C8.68496 9.64609 10.1596 9.61797 10.9092 9.61797C11.6588 9.61797 12.8619 9.6457 13.3467 9.69727C13.6764 9.73242 13.9873 9.88477 14.2217 10.1187C14.4561 10.3535 14.6037 10.659 14.6381 10.9871C14.6381 10.9906 14.6389 10.9938 14.6393 10.9973C14.6811 11.3977 14.6756 11.6172 14.6756 11.6176C14.6756 11.6176 14.6811 11.9609 14.6396 12.368C14.6396 12.3715 14.6389 12.3746 14.6385 12.3781C14.6041 12.7117 14.4572 13.041 14.2236 13.3082C13.9904 13.575 13.6807 13.7664 13.3514 13.8359C12.8678 13.9453 11.6623 14.0531 10.91 14.048C10.1576 14.0668 8.67949 13.95 8.1959 13.8352C7.8666 13.7656 7.55684 13.5742 7.32363 13.3074C7.09043 13.0402 6.94316 12.7109 6.90879 12.3777C6.90879 12.3742 6.90801 12.3707 6.90762 12.3676V12.3672Z" fill="url(#paint0_linear_319_5704)"/>
<path d="M10.4369 20.6078C10.6568 20.7035 10.908 20.7035 11.1279 20.6078C13.7295 19.475 19.0666 14.0719 19.0666 8.95938C19.0666 4.38672 15.3596 0.679688 10.7869 0.679688H10.7693C6.19629 0.679688 2.48926 4.38672 2.48926 8.95938C2.48926 14.0723 7.83457 19.4754 10.4365 20.6078H10.4369ZM10.7291 4.36875C12.0232 4.36875 13.0725 5.41797 13.0725 6.71211C13.0725 8.00625 12.0232 9.05547 10.7291 9.05547C9.43496 9.05547 8.38574 8.00625 8.38574 6.71211C8.38574 5.41797 9.43496 4.36875 10.7291 4.36875ZM6.90762 12.3672C6.86582 11.9605 6.88418 11.6184 6.88418 11.618C6.88418 11.618 6.86582 11.3973 6.90762 10.9973C6.90762 10.9938 6.9084 10.9906 6.90879 10.9871C6.94316 10.659 7.09082 10.3535 7.32519 10.1187C7.55957 9.88477 7.87012 9.73281 8.20019 9.69727C8.68496 9.64609 10.1596 9.61797 10.9092 9.61797C11.6588 9.61797 12.8619 9.6457 13.3467 9.69727C13.6764 9.73242 13.9873 9.88477 14.2217 10.1187C14.4561 10.3535 14.6037 10.659 14.6381 10.9871C14.6381 10.9906 14.6389 10.9938 14.6393 10.9973C14.6811 11.3977 14.6756 11.6172 14.6756 11.6176C14.6756 11.6176 14.6811 11.9609 14.6396 12.368C14.6396 12.3715 14.6389 12.3746 14.6385 12.3781C14.6041 12.7117 14.4572 13.041 14.2236 13.3082C13.9904 13.575 13.6807 13.7664 13.3514 13.8359C12.8678 13.9453 11.6623 14.0531 10.91 14.048C10.1576 14.0668 8.67949 13.95 8.1959 13.8352C7.8666 13.7656 7.55684 13.5742 7.32363 13.3074C7.09043 13.0402 6.94316 12.7109 6.90879 12.3777C6.90879 12.3742 6.90801 12.3707 6.90762 12.3676V12.3672Z" fill="url(#paint1_linear_319_5704)"/>
</g>
<defs>
<linearGradient id="paint0_linear_319_5704" x1="2.48926" y1="10.6797" x2="19.0666" y2="10.6797" gradientUnits="userSpaceOnUse">
<stop stop-color="#0080DE"/>
<stop offset="0.6" stop-color="#49BCFF"/>
<stop offset="1" stop-color="#61CAFF"/>
</linearGradient>
<linearGradient id="paint1_linear_319_5704" x1="-6.72168" y1="-0.320313" x2="20.7783" y2="12.6797" gradientUnits="userSpaceOnUse">
<stop stop-color="#6949C6"/>
<stop offset="0.6" stop-color="#7C6EE0"/>
<stop offset="1" stop-color="#A28EEF"/>
</linearGradient>
<clipPath id="clip0_319_5704">
<rect width="20" height="20" fill="white" transform="translate(0.77832 0.679688)"/>
</clipPath>
</defs>
</svg>
class hrmAccessiblePagesResponse {
String? error;
List<PagesAccessible>? pagesAccessible;
String? message;
int? sessionExists;
hrmAccessiblePagesResponse(
{this.error, this.pagesAccessible, this.message, this.sessionExists});
hrmAccessiblePagesResponse.fromJson(Map<String, dynamic> json) {
error = json['error'];
if (json['pages_accessible'] != null) {
pagesAccessible = <PagesAccessible>[];
json['pages_accessible'].forEach((v) {
pagesAccessible!.add(new PagesAccessible.fromJson(v));
});
}
message = json['message'];
sessionExists = json['session_exists'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['error'] = this.error;
if (this.pagesAccessible != null) {
data['pages_accessible'] =
this.pagesAccessible!.map((v) => v.toJson()).toList();
}
data['message'] = this.message;
data['session_exists'] = this.sessionExists;
return data;
}
}
class PagesAccessible {
String? id;
String? pageName;
String? mode;
PagesAccessible({this.id, this.pageName, this.mode});
PagesAccessible.fromJson(Map<String, dynamic> json) {
id = json['id'];
pageName = json['page_name'];
mode = json['mode'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['page_name'] = this.pageName;
data['mode'] = this.mode;
return data;
}
}
class leaveApplicationDetailsResponse {
RequestDetails? requestDetails;
String? error;
String? message;
int? sessionExists;
leaveApplicationDetailsResponse(
{this.requestDetails, this.error, this.message, this.sessionExists});
leaveApplicationDetailsResponse.fromJson(Map<String, dynamic> json) {
requestDetails = json['request_details'] != null
? new RequestDetails.fromJson(json['request_details'])
: null;
error = json['error'];
message = json['message'];
sessionExists = json['session_exists'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.requestDetails != null) {
data['request_details'] = this.requestDetails!.toJson();
}
data['error'] = this.error;
data['message'] = this.message;
data['session_exists'] = this.sessionExists;
return data;
}
}
class RequestDetails {
String? id;
String? appliedDate;
String? fromDate;
String? toDate;
String? fromTime;
String? toTime;
String? reason;
String? leaveType;
String? status;
String? requestedTo;
String? approvedBy;
String? approvedDate;
String? approvalRemarks;
RequestDetails(
{this.id,
this.appliedDate,
this.fromDate,
this.toDate,
this.fromTime,
this.toTime,
this.reason,
this.leaveType,
this.status,
this.requestedTo,
this.approvedBy,
this.approvedDate,
this.approvalRemarks});
RequestDetails.fromJson(Map<String, dynamic> json) {
id = json['id'];
appliedDate = json['applied_date'];
fromDate = json['from_date'];
toDate = json['to_date'];
fromTime = json['from_time'];
toTime = json['to_time'];
reason = json['reason'];
leaveType = json['leave_type'];
status = json['status'];
requestedTo = json['requested_to'];
approvedBy = json['approved_by'];
approvedDate = json['approved_date'];
approvalRemarks = json['approval_remarks'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['applied_date'] = this.appliedDate;
data['from_date'] = this.fromDate;
data['to_date'] = this.toDate;
data['from_time'] = this.fromTime;
data['to_time'] = this.toTime;
data['reason'] = this.reason;
data['leave_type'] = this.leaveType;
data['status'] = this.status;
data['requested_to'] = this.requestedTo;
data['approved_by'] = this.approvedBy;
data['approved_date'] = this.approvedDate;
data['approval_remarks'] = this.approvalRemarks;
return data;
}
}
class leaveApplicationLIstResponse {
List<RequestList>? requestList;
String? error;
String? message;
int? sessionExists;
leaveApplicationLIstResponse(
{this.requestList, this.error, this.message, this.sessionExists});
leaveApplicationLIstResponse.fromJson(Map<String, dynamic> json) {
if (json['request_list'] != null) {
requestList = <RequestList>[];
json['request_list'].forEach((v) {
requestList!.add(new RequestList.fromJson(v));
});
}
error = json['error'];
message = json['message'];
sessionExists = json['session_exists'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.requestList != null) {
data['request_list'] = this.requestList!.map((v) => v.toJson()).toList();
}
data['error'] = this.error;
data['message'] = this.message;
data['session_exists'] = this.sessionExists;
return data;
}
}
class RequestList {
String? id;
String? appliedDate;
String? fromPeriod;
String? toPeriod;
String? status;
String? leaveType;
RequestList(
{this.id, this.appliedDate, this.fromPeriod, this.toPeriod, this.status});
RequestList.fromJson(Map<String, dynamic> json) {
id = json['id'];
appliedDate = json['applied_date'];
fromPeriod = json['from_period'];
toPeriod = json['to_period'];
status = json['status'];
leaveType = json["leave_type"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['applied_date'] = this.appliedDate;
data['from_period'] = this.fromPeriod;
data['to_period'] = this.toPeriod;
data['status'] = this.status;
data["leave_type"] = this.leaveType;
return data;
}
}
class tourExpensesAddViewResponse {
List<int>? daAmount;
List<String>? tourType;
List<String>? travelType;
String? error;
String? message;
int? sessionExists;
tourExpensesAddViewResponse(
{this.daAmount,
this.tourType,
this.travelType,
this.error,
this.message,
this.sessionExists});
tourExpensesAddViewResponse.fromJson(Map<String, dynamic> json) {
daAmount = json['da_amount'].cast<int>();
tourType = json['tour_type'].cast<String>();
travelType = json['travel_type'].cast<String>();
error = json['error'];
message = json['message'];
sessionExists = json['session_exists'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['da_amount'] = this.daAmount;
data['tour_type'] = this.tourType;
data['travel_type'] = this.travelType;
data['error'] = this.error;
data['message'] = this.message;
data['session_exists'] = this.sessionExists;
return data;
}
}
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 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../Models/hrmModels/attendanceRequestListResponse.dart'; import '../../Models/hrmModels/attendanceRequestListResponse.dart';
import '../../Utils/SharedpreferencesService.dart'; import '../../Models/ordersModels/commonResponse.dart';
import '../../Utils/app_colors.dart';
import '../../services/api_calling.dart'; import '../../services/api_calling.dart';
import '../HomeScreenNotifier.dart'; import '../HomeScreenNotifier.dart';
class Attendancelistprovider extends ChangeNotifier { class Attendancelistprovider extends ChangeNotifier {
attendanceRequestListResponse? _response; attendanceRequestListResponse? _response;
bool _isLoading = false; bool _isLoading = false;
...@@ -18,8 +20,114 @@ class Attendancelistprovider extends ChangeNotifier { ...@@ -18,8 +20,114 @@ class Attendancelistprovider extends ChangeNotifier {
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
/// fetch aattendance request list // Filter states
Future<void> fetchAttendanceRequests(context,String type, String from, String to) async { 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; _isLoading = true;
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
...@@ -27,36 +135,234 @@ class Attendancelistprovider extends ChangeNotifier { ...@@ -27,36 +135,234 @@ class Attendancelistprovider extends ChangeNotifier {
try { try {
final provider = Provider.of<HomescreenNotifier>(context, listen: false); final provider = Provider.of<HomescreenNotifier>(context, listen: false);
final result = await ApiCalling.attendanceRequestListAPI(provider.empId, provider.session, type, from, to); // Update filter states if provided
debugPrint('empId: ${provider.empId}, session: ${provider.requestId}'); 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) { if (result != null) {
_response = result; _response = result;
if (_response?.requestList == null || _response!.requestList!.isEmpty) {
_errorMessage = "No attendance records found!";
}
} else { } else {
_errorMessage = "No data found!"; _errorMessage = "No data found!";
} }
} catch (e) { } catch (e) {
_errorMessage = "Error: $e"; _errorMessage = "Error: $e";
debugPrint('Error fetching attendance: $e');
} }
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
DateTime? _date; /// --- Add Attendance Request ---
final TextEditingController dateController = TextEditingController(); 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();
void setDate(DateTime newDate) { try {
_date = newDate; final homeProvider = Provider.of<HomescreenNotifier>(context, listen: false);
dateController.text =
"${newDate.day}-${newDate.month}-${newDate.year}"; 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(); 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) { void showDatePickerDialog(BuildContext context) {
if (_date == null) { if (_selectedDate == null) {
setDate(DateTime.now()); setSelectedDate(DateTime.now());
} }
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
...@@ -72,36 +378,48 @@ class Attendancelistprovider extends ChangeNotifier { ...@@ -72,36 +378,48 @@ class Attendancelistprovider extends ChangeNotifier {
top: false, top: false,
child: Column( child: Column(
children: [ children: [
// Cancel + Done Buttons
SizedBox( SizedBox(
height: 40, height: 55,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CupertinoButton( CupertinoButton(
child: const Text("Cancel", child: Text(
style: TextStyle(color: Colors.blue)), 'Cancel',
onPressed: () { style: TextStyle(
Navigator.pop(context); fontFamily: "JakartaMedium",
}, color: AppColors.app_blue,
),
),
onPressed: () => Navigator.pop(context),
), ),
CupertinoButton( CupertinoButton(
child: const Text("Done", child: Text(
style: TextStyle(color: Colors.blue)), 'Done',
style: TextStyle(
fontFamily: "JakartaMedium",
color: AppColors.app_blue,
),
),
onPressed: () { onPressed: () {
setDate(_date ?? DateTime.now()); setSelectedDate(_selectedDate ?? DateTime.now());
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
], ],
), ),
), ),
// Cupertino Date Picker
Expanded( Expanded(
child: CupertinoDatePicker( child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy, dateOrder: DatePickerDateOrder.dmy,
initialDateTime: _date ?? DateTime.now(), initialDateTime: _selectedDate ?? DateTime.now(),
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
use24hFormat: true,
showDayOfWeek: true,
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
_date = newDate; // temp update 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 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:generp/Models/hrmModels/tourExpensesAddViewResponse.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../Models/hrmModels/tourExpensesListResponse.dart'; import '../../Models/hrmModels/tourExpensesListResponse.dart';
...@@ -15,6 +19,37 @@ class TourExpensesProvider extends ChangeNotifier { ...@@ -15,6 +19,37 @@ class TourExpensesProvider extends ChangeNotifier {
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage; 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 /// Fetch tour expenses list
Future<void> fetchTourExpenses(BuildContext context, String pageNumber) async { Future<void> fetchTourExpenses(BuildContext context, String pageNumber) async {
_isLoading = true; _isLoading = true;
...@@ -45,21 +80,115 @@ class TourExpensesProvider extends ChangeNotifier { ...@@ -45,21 +80,115 @@ class TourExpensesProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
DateTime? _date; Future<void> fetchTourExpensesAddView(BuildContext context, String tourBillId) async {
final TextEditingController dateController = TextEditingController(); _isLoading = true;
_errorMessage = null;
notifyListeners();
void setDate(DateTime newDate) { try {
_date = newDate; final provider = Provider.of<HomescreenNotifier>(context, listen: false);
dateController.text = "${newDate.day}-${newDate.month}-${newDate.year}";
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(); notifyListeners();
} }
void showDatePickerDialog(BuildContext context) { Future<bool> addTourBill({
if (_date == null) { required BuildContext context,
setDate(DateTime.now()); 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;
} }
showCupertinoModalPopup<void>( 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, context: context,
builder: (BuildContext context) => Container( builder: (BuildContext context) => Container(
height: 250, height: 250,
...@@ -73,18 +202,25 @@ class TourExpensesProvider extends ChangeNotifier { ...@@ -73,18 +202,25 @@ class TourExpensesProvider extends ChangeNotifier {
child: Column( child: Column(
children: [ children: [
SizedBox( SizedBox(
height: 40, height: 55,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
CupertinoButton( CupertinoButton(
child: const Text("Cancel", style: TextStyle(color: Colors.blue)), child: const Text("Cancel",
style: TextStyle(color: Colors.blue)),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
CupertinoButton( CupertinoButton(
child: const Text("Done", style: TextStyle(color: Colors.blue)), child: const Text("Done",
style: TextStyle(color: Colors.blue)),
onPressed: () { onPressed: () {
setDate(_date ?? DateTime.now()); pickedDate = currentDate;
if (isFromDate) {
fromDateField.text = _formatDate(pickedDate!);
} else {
toDateField.text = _formatDate(pickedDate!);
}
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
...@@ -94,10 +230,10 @@ class TourExpensesProvider extends ChangeNotifier { ...@@ -94,10 +230,10 @@ class TourExpensesProvider extends ChangeNotifier {
Expanded( Expanded(
child: CupertinoDatePicker( child: CupertinoDatePicker(
dateOrder: DatePickerDateOrder.dmy, dateOrder: DatePickerDateOrder.dmy,
initialDateTime: _date ?? DateTime.now(), initialDateTime: currentDate,
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
_date = newDate; currentDate = newDate;
}, },
), ),
), ),
...@@ -106,5 +242,9 @@ class TourExpensesProvider extends ChangeNotifier { ...@@ -106,5 +242,9 @@ class TourExpensesProvider extends ChangeNotifier {
), ),
), ),
); );
return pickedDate;
} }
} }
...@@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.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 'package:generp/Utils/app_colors.dart';
import 'screens/notifierExports.dart'; import 'screens/notifierExports.dart';
import 'package:generp/Utils/SharedpreferencesService.dart'; import 'package:generp/Utils/SharedpreferencesService.dart';
...@@ -231,6 +232,7 @@ class MyApp extends StatelessWidget { ...@@ -231,6 +232,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => TourExpensesProvider()), ChangeNotifierProvider(create: (_) => TourExpensesProvider()),
ChangeNotifierProvider(create: (_) => TourExpensesDetailsProvider()), ChangeNotifierProvider(create: (_) => TourExpensesDetailsProvider()),
ChangeNotifierProvider(create: (_) => RewardListProvider()), ChangeNotifierProvider(create: (_) => RewardListProvider()),
ChangeNotifierProvider(create: (_) => LeaveApplicationListProvider()),
], ],
child: Builder( child: Builder(
builder: (BuildContext context) { 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];
}
}
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;
}
}
...@@ -2,9 +2,11 @@ import 'dart:io'; ...@@ -2,9 +2,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Utils/app_colors.dart'; import '../../Utils/app_colors.dart';
import '../../Utils/dropdownTheme.dart'; import '../../Utils/dropdownTheme.dart';
...@@ -17,6 +19,10 @@ class AddLiveAttendanceScreen extends StatefulWidget { ...@@ -17,6 +19,10 @@ class AddLiveAttendanceScreen extends StatefulWidget {
} }
class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
String? typeError;
String? locationError;
String? proofError;
String? selectedType; String? selectedType;
Dropdowntheme ddtheme = Dropdowntheme(); Dropdowntheme ddtheme = Dropdowntheme();
final TextEditingController locationController = TextEditingController(); final TextEditingController locationController = TextEditingController();
...@@ -25,9 +31,9 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -25,9 +31,9 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
final List<String> types = ["Check In", "Check Out"]; final List<String> types = ["Check In", "Check Out"];
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
XFile? proofFile; // store selected proof XFile? proofFile;
bool isSubmitting = false;
// computed labels
String get locationHeading => String get locationHeading =>
selectedType == null ? "Location" : "$selectedType Location"; selectedType == null ? "Location" : "$selectedType Location";
...@@ -40,21 +46,49 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -40,21 +46,49 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
bool get isSubmitEnabled => bool get isSubmitEnabled =>
selectedType != null && selectedType != null &&
locationController.text.trim().isNotEmpty && locationController.text.trim().isNotEmpty &&
proofFile != null; // proof is required proofFile != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
locationController.addListener(() { _autoFetchLocation();
setState(() {}); }
Future<void> _autoFetchLocation() async {
String loc = await getCurrentLocation();
setState(() {
locationController.text = loc;
}); });
} }
@override Future<String> getCurrentLocation() async {
void dispose() { try {
locationController.dispose(); LocationPermission permission = await Geolocator.checkPermission();
descriptionController.dispose(); if (permission == LocationPermission.denied) {
super.dispose(); 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) { void _showPicker(BuildContext context) {
...@@ -95,26 +129,71 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -95,26 +129,71 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
); );
} }
Future<String> getCurrentLocation() async { void submitAttendance(BuildContext context) async {
var status = await Permission.location.request(); setState(() {
if (status.isGranted) { typeError = null;
Position position = await Geolocator.getCurrentPosition( locationError = null;
desiredAccuracy: LocationAccuracy.high, proofError = null;
); });
return "${position.latitude.toStringAsFixed(7)}, ${position.longitude.toStringAsFixed(7)}";
} else { bool hasError = false;
return "Permission denied";
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;
} }
/// New: submit function if (hasError) {
void submitAttendance() { setState(() {});
print("==== Attendance Submitted ===="); return;
print("Type: $selectedType"); }
print("Location: ${locationController.text}");
print("Description: ${descriptionController.text}"); setState(() => isSubmitting = true);
print("Proof: ${proofFile?.path ?? 'No file'}");
print("============================="); 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 @override
...@@ -123,7 +202,8 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -123,7 +202,8 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
backgroundColor: const Color(0xFEFFFFFF), backgroundColor: Colors.white,
elevation: 0,
title: Row( title: Row(
children: [ children: [
InkResponse( InkResponse(
...@@ -138,7 +218,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -138,7 +218,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
"Add Live Attendance", "Add Live Attendance",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.semi_black, color: AppColors.semi_black,
), ),
...@@ -155,7 +235,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -155,7 +235,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const Text("Type", const Text("Type",
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500)), fontWeight: FontWeight.w500)),
const SizedBox(height: 6), const SizedBox(height: 6),
Container( Container(
...@@ -167,101 +247,113 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -167,101 +247,113 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton2<String>( child: DropdownButton2<String>(
isExpanded: true, isExpanded: true,
hint: const Text("Select Type", hint: const Text(
"Select Type",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontWeight: FontWeight.w400), fontWeight: FontWeight.w400),
), ),
value: selectedType, value: selectedType,
items: types items: types
.map((e) => .map((e) => DropdownMenuItem<String>(
DropdownMenuItem<String>(
value: e, value: e,
child: Text( child: Text(
e, e,
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14, fontFamily: "JakartaMedium"),
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
) ),
)) ))
.toList(), .toList(),
onChanged: (val) => setState(() => selectedType = val), onChanged: (val) => setState(() => selectedType = val),
// buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData, iconStyleData: ddtheme.iconStyleData,
// menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData, 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), const SizedBox(height: 16),
/// Location field /// Location
Text(locationHeading, Text(locationHeading,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontStyle: FontStyle.normal, fontWeight: FontWeight.w500)),
fontWeight: FontWeight.w500
)),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( TextField(
controller: locationController, controller: locationController,
readOnly: true, decoration: _inputDecoration("Enter location"),
onTap: () async {
String loc = await getCurrentLocation();
locationController.text = loc;
},
decoration: _inputDecoration("Tap to get location"),
), ),
if (locationError != null) ...[
const SizedBox(height: 4),
Text(locationError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
const SizedBox(height: 16), const SizedBox(height: 16),
/// Description /// Description
Text(descriptionHeading, Text(descriptionHeading,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontStyle: FontStyle.normal, fontWeight: FontWeight.w500)),
fontWeight: FontWeight.w500
)
),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( TextField(
controller: descriptionController, controller: descriptionController,
maxLines: 3, maxLines: 3,
decoration: _inputDecoration("Write Description"), decoration: _inputDecoration("Write Description"),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
/// Attach Proof /// Attach Proof
SizedBox( InkResponse(
onTap: () => _showPicker(context),
child: Container(
width: double.infinity, width: double.infinity,
child: OutlinedButton(
onPressed: () => _showPicker(context),
style: OutlinedButton.styleFrom(
backgroundColor: Colors.blue.shade50,
side: BorderSide(color: Colors.blue.shade200),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(14),
), ),
child: Text(proofButtonText, child: Center(
child: Text(
proofButtonText,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.blue, color: Colors.blue,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontStyle: FontStyle.normal, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500 ),
)),
), ),
), ),
),
),
if (proofError != null) ...[
const SizedBox(height: 4),
Text(proofError!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: "JakartaMedium")),
],
/// Show proof preview
if (proofFile != null) ...[ if (proofFile != null) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
...@@ -269,9 +361,10 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -269,9 +361,10 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const Icon(Icons.check_circle, color: Colors.green), const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text("Attached: ${proofFile!.name}",
"Attached: ${proofFile!.name}", overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis)), style: const TextStyle(
fontFamily: "JakartaMedium", fontSize: 14))),
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.red), icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => setState(() => proofFile = null), onPressed: () => setState(() => proofFile = null),
...@@ -282,23 +375,30 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -282,23 +375,30 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
/// Submit button /// Submit Button
SizedBox( InkResponse(
width: double.infinity, onTap:
child: ElevatedButton( isSubmitEnabled && !isSubmitting ? () => submitAttendance(context) : null,
onPressed: isSubmitEnabled ? submitAttendance : null, child: Container(
style: ElevatedButton.styleFrom( height: 48,
backgroundColor: alignment: Alignment.center,
isSubmitEnabled ? Colors.blue : Colors.grey.shade400, decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(vertical: 14), color: isSubmitEnabled
shape: RoundedRectangleBorder( ? AppColors.app_blue
borderRadius: BorderRadius.circular(8)), : Colors.grey.shade400,
borderRadius: BorderRadius.circular(12),
), ),
child: const Text("Submit", child: isSubmitting
? const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2)
: const Text(
"Submit",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontFamily: "JakartaMedium",
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w500)), ),
),
), ),
), ),
], ],
...@@ -306,13 +406,13 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -306,13 +406,13 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
), ),
); );
} }
InputDecoration _inputDecoration(String hint) { InputDecoration _inputDecoration(String hint) {
return InputDecoration( return InputDecoration(
hintText: hint, hintText: hint,
hintStyle: const TextStyle( hintStyle: const TextStyle(
fontSize: 14, fontSize: 14,
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Colors.grey, color: Colors.grey,
), ),
...@@ -324,7 +424,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> { ...@@ -324,7 +424,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue), borderSide: BorderSide(color: AppColors.app_blue),
), ),
); );
} }
......
import 'package:flutter/cupertino.dart'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter/services.dart';
import 'package:flutter/cupertino.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter_svg/svg.dart';
import 'package:image_picker/image_picker.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/app_colors.dart';
import '../../Utils/commonServices.dart';
import '../../Utils/commonWidgets.dart';
import '../../Utils/dropdownTheme.dart'; import '../../Utils/dropdownTheme.dart';
class AddManualAttendanceScreen extends StatefulWidget { class AddManualAttendanceScreen extends StatefulWidget {
...@@ -15,10 +27,15 @@ class AddManualAttendanceScreen extends StatefulWidget { ...@@ -15,10 +27,15 @@ class AddManualAttendanceScreen extends StatefulWidget {
} }
class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final TextEditingController dateController = TextEditingController();
Dropdowntheme ddtheme = Dropdowntheme(); Dropdowntheme ddtheme = Dropdowntheme();
final ImagePicker picker = ImagePicker();
// Connectivity
Map _source = {ConnectivityResult.mobile: true};
final MyConnectivity _connectivity = MyConnectivity.instance;
String connection = "Online";
// Separate controllers for each section // Controllers
final checkInTime = TextEditingController(); final checkInTime = TextEditingController();
final checkInLocation = TextEditingController(); final checkInLocation = TextEditingController();
final checkInDescription = TextEditingController(); final checkInDescription = TextEditingController();
...@@ -29,325 +46,524 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> { ...@@ -29,325 +46,524 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final checkOutDescription = TextEditingController(); final checkOutDescription = TextEditingController();
XFile? checkOutProof; XFile? checkOutProof;
final ImagePicker picker = ImagePicker();
String? selectedType; String? selectedType;
final List<String> types = ["Check In", "Check Out", "Check In/Out"]; final List<String> types = ["Check In", "Check Out", "Check In/Out"];
DateTime? _date;
bool get isSubmitEnabled { // Errors
if (selectedType == "Check In") { String? dateError, typeError;
return checkInLocation.text.trim().isNotEmpty; String? checkInTimeError, checkInLocError, checkInDescError, checkInProofError;
} else if (selectedType == "Check Out") { String? checkOutTimeError, checkOutLocError, checkOutDescError, checkOutProofError;
return checkOutLocation.text.trim().isNotEmpty;
} else if (selectedType == "Check In/Out") {
return checkInLocation.text.trim().isNotEmpty &&
checkOutLocation.text.trim().isNotEmpty;
}
return false;
}
// In your Attendancelistprovider class
CommonResponse? get addResponse => addResponse;
String? get errorMessage => errorMessage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
checkInLocation.addListener(() => setState(() {})); _connectivity.initialise();
checkOutLocation.addListener(() => setState(() {})); _connectivity.myStream.listen((src) {
setState(() => _source = src);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchInitialLocation();
});
} }
// ===== Print all submitted values =====
void _submitForm() { @override
print("===== Manual Attendance Submitted ====="); void dispose() {
print("Date: ${dateController.text}"); _connectivity.disposeStream();
print("Type: $selectedType"); super.dispose();
print("Time: ${checkInTime.text}");
print("Location: ${checkInLocation.text}");
print("Description: ${checkInDescription.text}");
print("Proof: ${checkInProof != null ? checkOutProof!.path : 'No file attached'}");
print("=======================================");
print("Date: ${dateController.text}");
print("Type: $selectedType");
print("Time: ${checkOutTime.text}");
print("Location: ${checkOutLocation.text}");
print("Description: ${checkOutDescription.text}");
print("Proof: ${checkInProof != null ? checkOutProof!.path : 'No file attached'}");
} }
// ===== Pick File ===== Future<void> _fetchInitialLocation() async {
Future<void> _pickFile(bool isCheckIn) async { String loc = await getCurrentLocation();
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
setState(() { setState(() {
if (isCheckIn) { checkInLocation.text = loc;
checkInProof = file; checkOutLocation.text = loc;
} else {
checkOutProof = file;
}
}); });
} }
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";
} }
void setDate(DateTime newDate) { Position pos = await Geolocator.getCurrentPosition(
_date = newDate; desiredAccuracy: LocationAccuracy.high);
dateController.text = "${newDate.day}-${newDate.month}-${newDate.year}";
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 { Future<void> _pickTime(TextEditingController controller) async {
final TimeOfDay? picked = final TimeOfDay? picked =
await showTimePicker(context: context, initialTime: TimeOfDay.now()); await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (picked != null) { if (picked != null) controller.text = picked.format(context);
controller.text = picked.format(context);
}
} }
@override Future<void> _pickFile(bool isCheckIn) async {
Widget build(BuildContext context) { showModalBottomSheet(
return Scaffold( useSafeArea: true,
isDismissible: true,
showDragHandle: true,
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( context: context,
automaticallyImplyLeading: false, builder: (_) {
backgroundColor: const Color(0xFEFFFFFF), return SafeArea(
title: Row( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
InkResponse( ListTile(
onTap: () => Navigator.pop(context, true), leading: const Icon(Icons.camera_alt),
child: SvgPicture.asset("assets/svg/appbar_back_button.svg", title: const Text("Capture photo from camera"),
height: 25), 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);
},
), ),
const SizedBox(width: 10),
Text("Add Manual Attendance",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
)),
], ],
), ),
);
},
);
}
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: Padding( body: Scrollbar(
padding: const EdgeInsets.all(18),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
/// Date // TextWidget(context, "Date"),
_buildLabel("Date"), GestureDetector(
const SizedBox(height: 6), onTap: () => provider.showDatePickerDialog(context),
TextField( child: AbsorbPointer(
controller: dateController, child: textControllerWidget(
readOnly: true, context,
onTap: () => setDate(DateTime.now()), provider.dateController,
decoration: _inputDecoration("Select Date") "Date",
.copyWith(suffixIcon: const Icon(Icons.calendar_today)), "Select Date",
), (v) {},
const SizedBox(height: 16), TextInputType.text,
false,
/// Type Dropdown null,
_buildLabel("Type"), null,
const SizedBox(height: 6), null,
Container( TextInputAction.next,
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
), ),
child: DropdownButtonHideUnderline( ),
),
errorWidget(context, dateError),
TextWidget(context, "Type"),
DropdownButtonHideUnderline(
child: Row(
children: [
Expanded(
child: DropdownButton2<String>( child: DropdownButton2<String>(
isExpanded: true, isExpanded: true,
hint: const Text("Select Type"), hint: const Text("Select Type"),
value: selectedType,
items: types items: types
.map((e) => DropdownMenuItem<String>( .map((e) => DropdownMenuItem(
value: e, value: e,
child: Text(e), child: Text(e),
)) ))
.toList(), .toList(),
onChanged: (val) => setState(() => selectedType = val), value: selectedType,
// buttonStyleData: ddtheme.buttonStyleData, onChanged: (val) {
setState(() => selectedType = val);
},
buttonStyleData: ddtheme.buttonStyleData,
iconStyleData: ddtheme.iconStyleData, iconStyleData: ddtheme.iconStyleData,
// menuItemStyleData: ddtheme.menuItemStyleData, menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData, dropdownStyleData: ddtheme.dropdownStyleData,
), ),
), ),
],
), ),
const SizedBox(height: 20),
/// Conditional Sections
if (selectedType == "Check In")
_buildSection(
title: "Check In",
timeController: checkInTime,
locationController: checkInLocation,
descriptionController: checkInDescription,
proofFile: checkInProof,
onPickProof: () => _pickFile(true),
),
if (selectedType == "Check Out")
_buildSection(
title: "Check Out",
timeController: checkOutTime,
locationController: checkOutLocation,
descriptionController: checkOutDescription,
proofFile: checkOutProof,
onPickProof: () => _pickFile(false),
),
if (selectedType == "Check In/Out") ...[
_buildSection(
title: "Check In",
timeController: checkInTime,
locationController: checkInLocation,
descriptionController: checkInDescription,
proofFile: checkInProof,
onPickProof: () => _pickFile(true),
),
_buildSection(
title: "Check Out",
timeController: checkOutTime,
locationController: checkOutLocation,
descriptionController: checkOutDescription,
proofFile: checkOutProof,
onPickProof: () => _pickFile(false),
), ),
], errorWidget(context, typeError),
const SizedBox(height: 24), if (selectedType == "Check In" || selectedType == "Check In/Out")
_buildSection("Check In"),
if (selectedType == "Check Out" || selectedType == "Check In/Out")
_buildSection("Check Out"),
/// Submit Button SizedBox(height: 80),
SizedBox( ],
width: double.infinity, ),
child: ElevatedButton( ),
style: ElevatedButton.styleFrom( ),
padding: const EdgeInsets.symmetric(vertical: 14), bottomNavigationBar: InkResponse(
backgroundColor: onTap: provider.isSubmitting ? null : () => _submitForm(context),
isSubmitEnabled ? Colors.blue : Colors.grey.shade400, child: Container(
shape: RoundedRectangleBorder( height: 45,
borderRadius: BorderRadius.circular(8)), alignment: Alignment.center,
), margin: const EdgeInsets.all(12),
onPressed: isSubmitEnabled decoration: BoxDecoration(
? () { color: AppColors.app_blue,
_submitForm(); borderRadius: BorderRadius.circular(15),
print("Submit pressed for $selectedType"); ),
} child: provider.isSubmitting
: null, ? CircularProgressIndicator.adaptive(
child: const Text("Submit", valueColor: AlwaysStoppedAnimation(AppColors.white),
)
: const Text(
"Submit",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white, color: Colors.white,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500,
)),
), ),
), ),
],
),
), ),
), ),
); );
},
);
} }
/// Reusable Section Widget _buildSection(String title) {
Widget _buildSection({ final isCheckIn = title == "Check In";
required String title, final timeCtrl = isCheckIn ? checkInTime : checkOutTime;
required TextEditingController timeController, final locCtrl = isCheckIn ? checkInLocation : checkOutLocation;
required TextEditingController locationController, final descCtrl = isCheckIn ? checkInDescription : checkOutDescription;
required TextEditingController descriptionController, final proofFile = isCheckIn ? checkInProof : checkOutProof;
required XFile? proofFile, final proofError = isCheckIn ? checkInProofError : checkOutProofError;
required VoidCallback onPickProof,
}) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildLabel("$title Time"), // TextWidget(context, "$title Time"),
const SizedBox(height: 6), GestureDetector(
TextField( onTap: () => _pickTime(timeCtrl), // ⏰ open time picker
controller: timeController, child: AbsorbPointer(
readOnly: true, child: textControllerWidget(
onTap: () => _pickTime(timeController), context,
decoration: _inputDecoration("Select Time") timeCtrl,
.copyWith(suffixIcon: const Icon(Icons.access_time)), "$title Time",
), "Select Time",
const SizedBox(height: 16), (v) {},
TextInputType.text,
_buildLabel("$title Location"), false,
const SizedBox(height: 6), null,
TextField( null,
controller: locationController, null,
decoration: _inputDecoration("Enter Location"), TextInputAction.next,
), ),
const SizedBox(height: 16), ),
),
_buildLabel("$title Description"), errorWidget(context, isCheckIn ? checkInTimeError : checkOutTimeError),
const SizedBox(height: 6),
TextField( textControllerWidget(
controller: descriptionController, context,
maxLines: 3, locCtrl,
decoration: _inputDecoration("Write Description"), "$title Location",
), "Enter Location",
const SizedBox(height: 18), (v) {},
TextInputType.text,
/// Proof false,
SizedBox( 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, width: double.infinity,
child: OutlinedButton( decoration: BoxDecoration(
onPressed: onPickProof, color: const Color(0xFFE6F6FF),
style: OutlinedButton.styleFrom( borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.symmetric(vertical: 14), border: Border.all(color: AppColors.app_blue, width: 0.5),
backgroundColor: Colors.blue.shade50, ),
side: BorderSide(color: Colors.blue.shade200), child: Center(
shape: child: Text(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), "Attach $title Proof",
), style: TextStyle(
child: Text("Attach $title Proof", fontFamily: "JakartaMedium",
style: const TextStyle( color: AppColors.app_blue,
fontSize: 16, ),
color: Colors.blue, ),
fontFamily: "Plus Jakarta Sans", ),
fontWeight: FontWeight.w500)), ),
), ),
), if (proofFile != null)
// Show proof preview
if (proofFile != null) ...[
const SizedBox(height: 10),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
"Attached: ${proofFile!.name}", proofFile.name,
overflow: TextOverflow.ellipsis)), maxLines: 2,
IconButton( overflow: TextOverflow.ellipsis,
icon: const Icon(Icons.close, color: Colors.red), style: const TextStyle(fontSize: 12),
onPressed: () => setState(() => proofFile = null),
), ),
], ),
IconButton(
icon: const Icon(Icons.close, color: Colors.red, size: 18),
onPressed: () {
setState(() {
if (isCheckIn) checkInProof = null;
else checkOutProof = null;
});
},
) )
], ],
const SizedBox(height: 24), ),
errorWidget(context, proofError),
], ],
); );
} }
Widget _buildLabel(String text) => Text(text,
style: const TextStyle(
fontSize: 15,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500));
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
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: const BorderSide(color: Colors.blue)),
);
}
} }
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.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/app_colors.dart';
import '../../Utils/commonServices.dart'; import '../../Utils/commonServices.dart';
import '../../Utils/commonWidgets.dart'; import '../../Utils/commonWidgets.dart';
...@@ -22,32 +25,32 @@ class AddBillScreen extends StatefulWidget { ...@@ -22,32 +25,32 @@ class AddBillScreen extends StatefulWidget {
} }
class _AddBillScreenState extends State<AddBillScreen> { class _AddBillScreenState extends State<AddBillScreen> {
Dropdowntheme ddtheme = Dropdowntheme(); final Dropdowntheme ddtheme = Dropdowntheme();
List<FocusNode> focusNodes = List.generate(8, (index) => FocusNode()); final List<FocusNode> focusNodes = List.generate(8, (index) => FocusNode());
Map _source = {ConnectivityResult.mobile: true}; Map _source = {ConnectivityResult.mobile: true};
final MyConnectivity _connectivity = MyConnectivity.instance; final MyConnectivity _connectivity = MyConnectivity.instance;
TextEditingController placeController = TextEditingController(); final TextEditingController placeController = TextEditingController();
TextEditingController dateController = TextEditingController(); final TextEditingController noteController = TextEditingController();
TextEditingController noteController = TextEditingController();
String? selectedDAAmount;
String? selectedTourType;
List<Map<String, String>> travelExpenses = [ // Validation errors
{"title": "Bike", "amount": "1800", "icon": "assets/svg/ic_bike.svg"}, String? placeError;
{"title": "Taxi", "amount": "300", "icon": "assets/svg/ic_taxi.svg"}, String? daAmountError;
]; String? tourTypeError;
String? tourDateError;
String? noteError;
List<Map<String, String>> hotelExpenses = [ List<Map<String, String>> travelExpenses = [];
{"title": "Hotel Sharada", "amount": "1800", "icon": "assets/svg/ic_hotel.svg"}, List<Map<String, String>> hotelExpenses = [];
{"title": "Hotel Nikitan", "amount": "1800", "icon": "assets/svg/ic_hotel.svg"}, List<Map<String, String>> otherExpenses = [];
]; List<File> travelImages = [];
List<File> hotelImages = [];
List<File> otherImages = [];
List<Map<String, String>> otherExpenses = [ String? selectedDAAmount;
{"title": "Book", "amount": "1800", "icon": "assets/svg/ic_book.svg"}, String? selectedTourType;
]; String? selectedTravelType;
@override @override
void initState() { void initState() {
...@@ -56,14 +59,33 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -56,14 +59,33 @@ class _AddBillScreenState extends State<AddBillScreen> {
_connectivity.myStream.listen((source) { _connectivity.myStream.listen((source) {
setState(() => _source = 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 @override
void dispose() { void dispose() {
placeController.dispose(); placeController.dispose();
dateController.dispose();
noteController.dispose(); noteController.dispose();
focusNodes.map((e) => e.dispose()); for (var node in focusNodes) {
node.dispose();
}
_connectivity.disposeStream(); _connectivity.disposeStream();
super.dispose(); super.dispose();
} }
...@@ -73,6 +95,44 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -73,6 +95,44 @@ class _AddBillScreenState extends State<AddBillScreen> {
return 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (_source.keys.toList()[0]) { switch (_source.keys.toList()[0]) {
...@@ -90,99 +150,329 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -90,99 +150,329 @@ class _AddBillScreenState extends State<AddBillScreen> {
? WillPopScope( ? WillPopScope(
onWillPop: () => _onBackPressed(context), onWillPop: () => _onBackPressed(context),
child: SafeArea( child: SafeArea(
top: false, top: false, bottom: true, child: _scaffold(context)),
bottom: true,
child: _scaffold(context),
),
) )
: _scaffold(context) : _scaffold(context)
: NoNetwork(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) { 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( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
backgroundColor: AppColors.scaffold_bg_color, backgroundColor: AppColors.scaffold_bg_color,
appBar: appbarNew(context, widget.pageTitleName, 0xFFFFFFFF), appBar: AppBar(
body: SingleChildScrollView( 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( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: EdgeInsets.symmetric(horizontal: 10),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppColors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
margin: EdgeInsets.only(top: 10, left: 10, right: 10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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),
/// Place of Visit
TextWidget(context, "Place of Visit"),
textFieldNew(context, placeController, "Enter Place"),
/// DA Amount
TextWidget(context, "DA Amount"), TextWidget(context, "DA Amount"),
dropDownField(context, "Select DA Amount", ["100", "200", "300"], selectedDAAmount, (val) { DropdownButtonHideUnderline(
setState(() => selectedDAAmount = val); 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),
/// Tour Type
TextWidget(context, "Tour Type"), TextWidget(context, "Tour Type"),
dropDownField(context, "Select Tour", ["Business", "Personal"], selectedTourType, (val) { DropdownButtonHideUnderline(
setState(() => selectedTourType = val); 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),
/// Tour Date
TextWidget(context, "Tour Date"), TextWidget(context, "Tour Date"),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
DateTime? picked = await showDatePicker( final d = await provider.showDatePickerDialog(context, isFromDate: true);
context: context, if (d != null) {
initialDate: DateTime.now(), provider.dateController.text = _formatDate(d);
firstDate: DateTime(2022), if (tourDateError != null) {
lastDate: DateTime(2100), setState(() => tourDateError = null);
); }
if (picked != null) {
dateController.text = DateFormat("dd MMM yyyy").format(picked);
setState(() {});
} }
}, },
child: textFieldNew(context, dateController, "Enter Date", enabled: false), 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),
/// Note textControllerWidget(
TextWidget(context, "Note"), context,
textFieldNew(context, noteController, "Write Note", maxLines: 3), 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), const SizedBox(height: 16),
/// Travel Expenses /// Travel Expenses Section
sectionHeader("Travel Expenses", onAddTap: () { sectionHeader("Travel Expenses", onAddTap: () {
// TODO: Add Travel Expense showAddTravelExpenseSheet(
context,
travelExpenses,
() => setState(() {}),
provider.travelTypeList,
travelImages,
);
}), }),
expenseList(travelExpenses), if (travelExpenses.isNotEmpty) travelExpenseList(travelExpenses),
/// Hotel Expenses /// Hotel Expenses Section
sectionHeader("Hotel Expenses", onAddTap: () { sectionHeader("Hotel Expenses", onAddTap: () {
// TODO: Add Hotel Expense showAddHotelExpenseSheet(
context,
hotelExpenses,
() => setState(() {}),
provider,
hotelImages,
);
}), }),
expenseList(hotelExpenses), if (hotelExpenses.isNotEmpty) hotelExpenseList(hotelExpenses),
/// Other Expenses /// Other Expenses Section
sectionHeader("Other Expenses", onAddTap: () { sectionHeader("Other Expenses", onAddTap: () {
// TODO: Add Other Expense showAddOtherExpenseSheet(
context,
otherExpenses,
() => setState(() {}),
provider,
otherImages,
);
}), }),
expenseList(otherExpenses), if (otherExpenses.isNotEmpty) otherExpenseList(otherExpenses),
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ),
), ),
), ),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
bottomNavigationBar: InkResponse( bottomNavigationBar: InkResponse(
onTap: () { onTap: () async {
// TODO: Submit API Call // 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( child: Container(
height: 45, height: 45,
...@@ -204,47 +494,7 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -204,47 +494,7 @@ class _AddBillScreenState extends State<AddBillScreen> {
), ),
), ),
); );
} },
/// --- Custom Widgets Below ---
Widget textFieldNew(BuildContext context, TextEditingController controller, String hint,
{bool enabled = true, int maxLines = 1}) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: TextFormField(
controller: controller,
enabled: enabled,
maxLines: maxLines,
decoration: _inputDecoration(hint)
),
);
}
Widget dropDownField(BuildContext context, String hint, List<String> items, String? value, Function(String?) onChanged) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: AppColors.text_field_color,
borderRadius: BorderRadius.circular(14),
),
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
hint: Text(hint, style: const TextStyle(fontSize: 14, color: Colors.grey)),
value: value,
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
onChanged: onChanged,
iconStyleData: ddtheme.iconStyleData,
// menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData: ddtheme.dropdownStyleData,
),
),
); );
} }
...@@ -252,23 +502,31 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -252,23 +502,31 @@ class _AddBillScreenState extends State<AddBillScreen> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), Text(title, style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
fontFamily: "JakartaMedium",
)),
const SizedBox(height: 6), const SizedBox(height: 6),
Container( Container(
height: 45, height: 45,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400, width: 0.7, style: BorderStyle.solid), border: Border.all(color: Colors.grey.shade400, width: 0.7),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: InkWell( child: InkWell(
onTap: onAddTap, onTap: onAddTap,
child: Center( child: const Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: [
Icon(Icons.add, color: Colors.blue), Icon(Icons.add, color: Colors.blue),
SizedBox(width: 6), SizedBox(width: 6),
Text("Add Expenses", style: TextStyle(color: Colors.blue, fontSize: 14)), Text("Add Expenses", style: TextStyle(
color: Colors.blue,
fontSize: 14,
fontFamily: "JakartaMedium",
)),
], ],
), ),
), ),
...@@ -279,9 +537,9 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -279,9 +537,9 @@ class _AddBillScreenState extends State<AddBillScreen> {
); );
} }
Widget expenseList(List<Map<String, String>> items) { Widget travelExpenseList(List<Map<String, String>> items) {
return Container( return Container(
height: 84, height: 90,
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
...@@ -289,20 +547,70 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -289,20 +547,70 @@ class _AddBillScreenState extends State<AddBillScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final exp = items[index]; final exp = items[index];
return Container( return Container(
width: 120, width: 200,
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE6F6FF), color: const Color(0xFFE6F6FF),
borderRadius: BorderRadius.circular(12), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SvgPicture.asset(exp["icon"] ?? "assets/svg/ic_default.svg", height: 22), Text(
const SizedBox(height: 6), exp["travel_type"] ?? "Travel",
Text(exp["title"] ?? "-", style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), style: TextStyle(
Text("₹${exp["amount"]}", style: const TextStyle(fontSize: 12, color: Colors.blue)), 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",
),
),
],
),
),
], ],
), ),
); );
...@@ -310,21 +618,1134 @@ class _AddBillScreenState extends State<AddBillScreen> { ...@@ -310,21 +618,1134 @@ class _AddBillScreenState extends State<AddBillScreen> {
), ),
); );
} }
InputDecoration _inputDecoration(String hint) {
return InputDecoration( Widget hotelExpenseList(List<Map<String, String>> items) {
hintText: hint, return Container(
hintStyle: TextStyle( 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, fontSize: 14,
fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal, color: AppColors.semi_black,
fontWeight: FontWeight.w400, fontFamily: "JakartaMedium",
color: Color(0xFFB4BEC0), ),
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,
), ),
filled: true, child: Center(
fillColor: Colors.grey.shade100, child: SvgPicture.asset(
enabledBorder: InputBorder.none, "assets/svg/hrm/books_ic.svg",
disabledBorder: InputBorder.none, height: 20,
focusedBorder: InputBorder.none, ),
),
),
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
...@@ -24,24 +24,6 @@ class _AttendanceRequestDetailScreenState ...@@ -24,24 +24,6 @@ class _AttendanceRequestDetailScreenState
extends State<AttendanceRequestDetailScreen> { extends State<AttendanceRequestDetailScreen> {
late AttendanceDetailsProvider provider; late AttendanceDetailsProvider provider;
// @override
// void initState() {
// super.initState();
//
// /// fetch API after widget is built
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final home = Provider.of<HomescreenNotifier>(context, listen: false);
// provider = Provider.of<AttendanceDetailsProvider>(context, listen: false);
//
// provider.fetchAttendanceRequestDetail(
// context,
// widget.attendanceListId
// );
// });
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return ChangeNotifierProvider(
...@@ -49,6 +31,14 @@ class _AttendanceRequestDetailScreenState ...@@ -49,6 +31,14 @@ class _AttendanceRequestDetailScreenState
AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId), AttendanceDetailsProvider()..fetchAttendanceRequestDetail(context, widget.attendanceListId),
child: Consumer<AttendanceDetailsProvider>( child: Consumer<AttendanceDetailsProvider>(
builder: (context, provider, child) { 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
...@@ -61,10 +51,10 @@ class _AttendanceRequestDetailScreenState ...@@ -61,10 +51,10 @@ class _AttendanceRequestDetailScreenState
onTap: () => Navigator.pop(context, true), onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg", "assets/svg/appbar_back_button.svg",
height: 25, height: 25 * scaleFactor,
), ),
), ),
SizedBox(width: 10), SizedBox(width: 10 * scaleFactor),
InkResponse( InkResponse(
onTap: () => Navigator.pop(context, true), onTap: () => Navigator.pop(context, true),
child: Text( child: Text(
...@@ -98,47 +88,46 @@ class _AttendanceRequestDetailScreenState ...@@ -98,47 +88,46 @@ class _AttendanceRequestDetailScreenState
/// scr /// scr
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column( child: Column(
children: [ children: [
Card( Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16 * scaleFactor),
), ),
elevation: 2, elevation: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0 * scaleFactor),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
margin: const EdgeInsets.only(bottom: 0.5), margin: EdgeInsets.only(bottom: 0.5 * scaleFactor),
padding: const EdgeInsets.all(12), padding: EdgeInsets.all(12 * scaleFactor),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12 * scaleFactor),
), ),
child: Row( child: Row(
children: [ children: [
/// Left Avatar /// Left Avatar
Container( Container(
height: 48, height: 48 * scaleFactor,
width: 48, width: 48 * scaleFactor,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg color: const Color(0xFFEDF8FF), // icon bg
), ),
child: Center( child: Center(
child: SvgPicture.asset( child: SvgPicture.asset(
height: 28, height: 28 * scaleFactor,
width: 28, width: 28 * scaleFactor,
"assets/svg/hrm/attendanceList.svg", "assets/svg/hrm/attendanceList.svg",
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
), ),
const SizedBox(width: 12), SizedBox(width: 12 * scaleFactor),
/// Middle text /// Middle text
Expanded( Expanded(
...@@ -148,22 +137,24 @@ class _AttendanceRequestDetailScreenState ...@@ -148,22 +137,24 @@ class _AttendanceRequestDetailScreenState
children: [ children: [
Text( Text(
details.type ?? "-", details.type ?? "-",
style: const TextStyle( style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle:
TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 14,
fontStyle: FontStyle.normal, color: AppColors.semi_black,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
), ),
), ),
const SizedBox(height: 2), SizedBox(height: 2 * scaleFactor),
Text( Text(
details.date ?? "-", details.date ?? "-",
style: const TextStyle( style: TextStyle(
fontSize: 12, fontFamily: "JakartaRegular",
fontStyle: FontStyle.normal, fontSize: 14,
fontFamily: "Plus Jakarta Sans", color: AppColors.app_blue,
fontWeight: FontWeight.w400,
color: Color(0xff818181),
), ),
), ),
], ],
...@@ -172,10 +163,12 @@ class _AttendanceRequestDetailScreenState ...@@ -172,10 +163,12 @@ class _AttendanceRequestDetailScreenState
/// Right side (Live/Manual) /// Right side (Live/Manual)
Container( Container(
height: 30, height: 30 * scaleFactor,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: EdgeInsets.symmetric(
horizontal: 12 * scaleFactor,
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6 * scaleFactor),
color: getDecorationColor(details.status) color: getDecorationColor(details.status)
), ),
child: Center( child: Center(
...@@ -189,97 +182,100 @@ class _AttendanceRequestDetailScreenState ...@@ -189,97 +182,100 @@ class _AttendanceRequestDetailScreenState
), ),
), ),
), ),
], ],
), ),
), ),
// Employee Details // Employee Details
_buildSectionHeader("Employee Details"), _buildSectionHeader("Employee Details", scaleFactor),
_buildDetailTile("Employee Name", details.employeeName), _buildDetailTile("Employee Name", details.employeeName, scaleFactor),
_buildDetailTile("Created Employee", details.createdEmpName), _buildDetailTile("Created Employee", details.createdEmpName, scaleFactor),
// Check In/Out // Check In/Out
_buildSectionHeader("Check In/Out Details"), _buildSectionHeader("Check In/Out Details", scaleFactor),
_buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime), _buildDate_TimeTile("Check In Date & Time", details.date, details.checkInTime, scaleFactor),
_buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime), _buildDate_TimeTile("Check Out Date & Time", details.date, details.checkOutTime, scaleFactor),
_buildDetailTile("Original Check In", details.checkInTime), _buildDetailTile("Original Check In", details.checkInTime, scaleFactor),
_buildDetailTile("Original Check Out", "--"), _buildDetailTile("Original Check Out", "--", scaleFactor),
_buildDetailTile("Original Check In Location", details.checkInLocation), _buildDetailTile("Original Check In Location", details.checkInLocation, scaleFactor),
_buildDetailTile("Original Check Out Location", details.checkOutLocation), _buildDetailTile("Original Check Out Location", details.checkOutLocation, scaleFactor),
buildLocationTile("Location", details.location), buildLocationTile("Location", details.location, scaleFactor),
// Proofs // Proofs
_buildSectionHeader("Proofs"), if ((details.checkInProofDirFilePath != null && details.checkInProofDirFilePath!.isNotEmpty) ||
_buildProofLink(context,"Check In Proof", details.checkInProofDirFilePath), (details.checkOutProofDirFilePath != null && details.checkOutProofDirFilePath!.isNotEmpty)) ...[
_buildProofLink(context,"Check Out Proof", details.checkOutProofDirFilePath), _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 // Remarks & Approvals
_buildSectionHeader("Remarks & Approvals"), _buildSectionHeader("Remarks & Approvals", scaleFactor),
_buildDetailTile("Level 1 Approved By", details.level1EmpName), _buildDetailTile("Level 1 Approved By", details.level1EmpName, scaleFactor),
_buildDetailTile("Level 2 Approved By", details.level2EmpName), _buildDetailTile("Level 2 Approved By", details.level2EmpName, scaleFactor),
_buildDetailTile("Level 1 Remark", details.level1Remarks), _buildDetailTile("Level 1 Remark", details.level1Remarks, scaleFactor),
_buildDetailTile("Level 2 Remark", details.level2Remarks), _buildDetailTile("Level 2 Remark", details.level2Remarks, scaleFactor),
///remain data ///remain data
_buildSectionHeader("Other Details"), _buildSectionHeader("Other Details", scaleFactor),
_buildDetailTile("Check In Type", details.checkInType), _buildDetailTile("Check In Type", details.checkInType, scaleFactor),
_buildDetailTile("Check Out Type", details.chechOutType), _buildDetailTile("Check Out Type", details.chechOutType, scaleFactor),
_buildDetailTile("Check Out Time", details.checkOutTime), _buildDetailTile("Check Out Time", details.checkOutTime, scaleFactor),
// Attendance Info // Attendance Info
_buildDetailTile("ID", details.id), _buildDetailTile("ID", details.id, scaleFactor),
_buildDetailTile("Attendance Type", details.attendanceType), _buildDetailTile("Attendance Type", details.attendanceType, scaleFactor),
_buildDetailTile("Note", details.note), _buildDetailTile("Note", details.note, scaleFactor),
_buildDetailTile("Created Datetime", details.requestedDatetime), _buildDetailTile("Created Datetime", details.requestedDatetime, scaleFactor),
], ],
), ),
), ),
), ),
SizedBox(height: 30,) SizedBox(height: 30 * scaleFactor),
], ],
), ),
); );
}, },
), ),
); );
}, },
) )
); );
} }
/// Reusable Row Widget for details /// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value) { Widget _buildDetailTile(String label, String? value, double scaleFactor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 3 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // Align top if wraps
children: [ children: [
// Label
Expanded( Expanded(
flex: 6, flex: 5, // keep same ratio as other tiles
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 14,
color: Color(0xff2D2D2D), color: AppColors.semi_black,
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
), ),
), ),
), ),
SizedBox(width: 4,),
// Value
Expanded( Expanded(
flex: 0, flex: 5, // take remaining width
child: Text(value ?? "-", child: Text(
value ?? "-",
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xff818181), color: Color(0xFF818181),
fontStyle: FontStyle.normal, ),
fontFamily: "Plus Jakarta Sans", softWrap: true,
fontWeight: FontWeight.w400, overflow: TextOverflow.visible, // wrap instead of clipping
)
), ),
), ),
], ],
...@@ -287,37 +283,42 @@ class _AttendanceRequestDetailScreenState ...@@ -287,37 +283,42 @@ class _AttendanceRequestDetailScreenState
); );
} }
/// for location /// for location
Widget buildLocationTile(String label, String? value) { Widget buildLocationTile(String label, String? value, double scaleFactor) {
return FutureBuilder<String>( return FutureBuilder<String>(
future: getReadableLocation(value), future: getReadableLocation(value),
builder: (context, snapshot) { builder: (context, snapshot) {
final locationText = snapshot.data ?? "-"; final locationText = snapshot.data ?? "-";
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // aligns top when wrapping
children: [ children: [
// Label
Expanded( Expanded(
flex: 6, flex: 5, // ratio (adjust same as your Date/Time tile)
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 14,
color: Color(0xff2D2D2D), color: AppColors.semi_black,
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
), ),
), ),
), ),
// Value (Clickable Location)
Expanded( Expanded(
flex: 0, flex: 5, // take remaining space
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
final uri = Uri.parse("https://www.google.com/maps/search/?api=1&query=$value"); final uri = Uri.parse(
"https://www.google.com/maps/search/?api=1&query=$value");
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(uri,
mode: LaunchMode.externalApplication);
} }
}, },
child: Text( child: Text(
...@@ -326,10 +327,10 @@ class _AttendanceRequestDetailScreenState ...@@ -326,10 +327,10 @@ class _AttendanceRequestDetailScreenState
fontSize: 14, fontSize: 14,
color: Colors.blue, color: Colors.blue,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
softWrap: true,
overflow: TextOverflow.visible,
), ),
), ),
), ),
...@@ -339,6 +340,7 @@ class _AttendanceRequestDetailScreenState ...@@ -339,6 +340,7 @@ class _AttendanceRequestDetailScreenState
}, },
); );
} }
Future<String> getReadableLocation(String? value) async { Future<String> getReadableLocation(String? value) async {
if (value == null) return "-"; if (value == null) return "-";
try { try {
...@@ -353,52 +355,40 @@ class _AttendanceRequestDetailScreenState ...@@ -353,52 +355,40 @@ class _AttendanceRequestDetailScreenState
} }
} }
/// for date and time /// for date and time
Widget _buildDate_TimeTile(String label, String? date, String? time) { Widget _buildDate_TimeTile(String label, String? date, String? time, double scaleFactor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // align top when wrapped
children: [ children: [
// Label
Expanded( Expanded(
flex: 5, flex: 5, // adjust ratio
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14, fontSize: 14,
color: Color(0xff2D2D2D), color: AppColors.semi_black,
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
), ),
), ),
), ),
SizedBox(width: 4,),
// Value (date + time)
Expanded( Expanded(
flex: 0, flex: 5, // adjust ratio so both fill row
child: Row( child: Text(
children: [ '${date ?? "-"}, ${time ?? "-"}',
Text('$date, ' ?? "-",
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xff818181), color: Color(0xff818181),
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
)
), ),
Text(time ?? "-", softWrap: true, // allow wrapping
style: const TextStyle( overflow: TextOverflow.visible,
fontSize: 14,
color: Color(0xff818181),
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
)
), ),
],
)
), ),
], ],
), ),
...@@ -406,51 +396,47 @@ class _AttendanceRequestDetailScreenState ...@@ -406,51 +396,47 @@ class _AttendanceRequestDetailScreenState
} }
Widget _buildSectionHeader(String title, double scaleFactor) {
///////////////////////
Widget _buildSectionHeader(String title) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 9 * scaleFactor),
child: Row( child: Row(
children: [ children: [
Text( Text(
title, title,
style: const TextStyle( style: TextStyle(
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
fontSize: 14, fontSize: 14,
fontFamily: "JakartaSemiBold",
), ),
), ),
const SizedBox(width: 10), SizedBox(width: 10 * scaleFactor),
Expanded( Expanded(
child: DottedLine( child: DottedLine(
dashLength: 4, dashGapLength: 4,
dashGapLength: 2, dashGapColor: Colors.white,
lineThickness: 1, dashColor: AppColors.grey_semi,
dashColor: Color(0xff888888), dashLength: 2,
) lineThickness: 0.5,
),
), ),
], ],
), ),
); );
} }
/// Proof section (image/file path) /// Proof section (image/file path)
Widget _buildProofLink(BuildContext context, String label, String? filePath) { Widget _buildProofLink(BuildContext context, String label, String? filePath, double scaleFactor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: EdgeInsets.symmetric(vertical: 6 * scaleFactor),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
flex: 5, flex: 5,
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontFamily: "JakartaRegular",
fontWeight: FontWeight.w500, fontSize: 14,
color: AppColors.semi_black,
), ),
), ),
), ),
...@@ -459,17 +445,13 @@ class _AttendanceRequestDetailScreenState ...@@ -459,17 +445,13 @@ class _AttendanceRequestDetailScreenState
child: filePath != null child: filePath != null
? InkWell( ? InkWell(
onTap: () { onTap: () {
showDialog( print("++++++++++++++++ImageUrel: $filePath");
context: context, Navigator.push(
builder: (_) => Dialog( context,
shape: RoundedRectangleBorder( MaterialPageRoute(
borderRadius: BorderRadius.circular(12), builder:
), (context) => Image.network(filePath),
child: ClipRRect( // Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
borderRadius: BorderRadius.circular(12),
child: Fileviewer(fileName: label, fileUrl: filePath,),
),
), ),
); );
}, },
...@@ -492,7 +474,6 @@ class _AttendanceRequestDetailScreenState ...@@ -492,7 +474,6 @@ class _AttendanceRequestDetailScreenState
); );
} }
Color getTextColor(value) { Color getTextColor(value) {
var color = AppColors.approved_text_color; var color = AppColors.approved_text_color;
switch (value) { switch (value) {
...@@ -534,9 +515,4 @@ class _AttendanceRequestDetailScreenState ...@@ -534,9 +515,4 @@ class _AttendanceRequestDetailScreenState
} }
return color; return color;
} }
} }
\ No newline at end of file
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:generp/Utils/GlobalConstants.dart'; import 'package:generp/Utils/GlobalConstants.dart';
import 'package:generp/screens/hrm/AddManualAttendance.dart'; import 'package:generp/screens/hrm/AddManualAttendance.dart';
import 'package:generp/screens/hrm/AttendanceRequestDetail.dart'; import 'package:generp/screens/hrm/AttendanceRequestDetail.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../Notifiers/hrmProvider/attendanceListProvider.dart'; import '../../Notifiers/hrmProvider/attendanceListProvider.dart';
import '../../Models/hrmModels/attendanceRequestListResponse.dart';
import '../../Utils/app_colors.dart'; import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart';
import '../CommonFilter2.dart';
import '../commonDateRangeFilter.dart'; import '../commonDateRangeFilter.dart';
import 'AddLiveAttendance.dart'; import 'AddLiveAttendance.dart';
import 'package:intl/intl.dart';
Map<String, String> getDateRange(String selectedDate) {
final now = DateTime.now();
final formatter = DateFormat("yyyy-MM-dd");
late DateTime from;
late DateTime to;
switch (selectedDate) {
case "All":
from = now;
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":
// For custom, you should open a date picker dialog
from = now; // Placeholder
to = now; // Placeholder
break;
default:
from = now;
to = now;
}
return {
"from": formatter.format(from),
"to": formatter.format(to),
};
}
class Attendancelist extends StatefulWidget { class Attendancelist extends StatefulWidget {
const Attendancelist({super.key}); const Attendancelist({super.key});
...@@ -70,32 +21,29 @@ class Attendancelist extends StatefulWidget { ...@@ -70,32 +21,29 @@ class Attendancelist extends StatefulWidget {
} }
class _AttendancelistState extends State<Attendancelist> { class _AttendancelistState extends State<Attendancelist> {
String selectedType = "All"; // @override
String selectedDate = "Today"; // void initState() {
// super.initState();
final List<String> typeOptions = ["All", "Check In", "Check Out","Check In/Out"]; // WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final List<String> dateOptions = [ // final provider = Provider.of<Attendancelistprovider>(context, listen: false);
"Today", // provider.fetchAttendanceRequests(context);
"Yesterday", // });
"This Month", // }
"Past 7 days",
"Last Month",
"Custom",
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dateRange = getDateRange(selectedDate);
final fromDate = dateRange["from"]!;
final toDate = dateRange["to"]!;
final type = (selectedType == "All") ? "" : selectedType;
return SafeArea( return SafeArea(
top: false, top: false,
child: ChangeNotifierProvider( child: ChangeNotifierProvider(
create: (_) => create: (_) {
Attendancelistprovider()..fetchAttendanceRequests(context, type, "", ""), final provider = Attendancelistprovider();
child: Consumer<Attendancelistprovider>( Future.microtask(() {
provider.fetchAttendanceRequests(context);
});
return provider;
},
builder: (context, child) {
return Consumer<Attendancelistprovider>(
builder: (context, provider, child) { builder: (context, provider, child) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
...@@ -124,193 +72,19 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -124,193 +72,19 @@ class _AttendancelistState extends State<Attendancelist> {
), ),
actions: [ actions: [
InkResponse( InkResponse(
onTap: () { onTap: () async {
showModalBottomSheet( final result = await CommonFilter2().showFilterBottomSheet(context);
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Filter Attendance",
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Colors.blue,
fontFamily: "Plus Jakarta Sans"
),
),
const SizedBox(height: 16),
/// Type Dropdown
Text("Type", style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
fontFamily: "Plus Jakarta Sans"
)
),
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",
style: TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
value: selectedType,
items: typeOptions
.map((type) => DropdownMenuItem<String>(
value: type,
child: Text(type),
))
.toList(),
onChanged: (val) {
setModalState(() {
selectedType = val!;
});
},
buttonStyleData: const ButtonStyleData(
height: 52,
padding: EdgeInsets.zero,
),
dropdownStyleData: DropdownStyleData(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
const SizedBox(height: 18),
/// Date Dropdown
const Text("Date", style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
fontFamily: "Plus Jakarta Sans"
)),
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 Date",
style: TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
value: selectedDate,
items: dateOptions
.map((date) => DropdownMenuItem<String>(
value: date,
child: Text(date),
))
.toList(),
onChanged: (val) {
setModalState(() {
selectedDate = val!;
});
},
buttonStyleData: const ButtonStyleData(
height: 52,
padding: EdgeInsets.zero,
),
dropdownStyleData: DropdownStyleData(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
const SizedBox(height: 26),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
// on apply
provider.fetchAttendanceRequests(context, selectedType, "", "");
Navigator.pop(context);
},
child: const Text(
"Apply",
style: TextStyle(
fontFamily: "Plus Jakarta Sans",
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: Colors.grey.shade400),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
// on cancel
Navigator.pop(context);
},
child: const Text(
"Cancel",
style: TextStyle(
fontFamily: "Plus Jakarta Sans",
color: Colors.black87,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 22),
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( child: SvgPicture.asset(
"assets/svg/filter_ic.svg", "assets/svg/filter_ic.svg",
...@@ -319,23 +93,56 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -319,23 +93,56 @@ class _AttendancelistState extends State<Attendancelist> {
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
], ],
), ),
backgroundColor: const Color(0xFFF6F6F8), backgroundColor: const Color(0xFFF6F6F8),
body: Column( body: Column(
children: [ 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 /// Attendance list
Expanded( Expanded(
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue,)); return const Center(child: CircularProgressIndicator(color: Colors.blue));
} }
if (provider.errorMessage != null) { if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!)); return Center(child: Text(provider.errorMessage!));
} }
if (provider.response?.requestList == null || if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) { provider.response!.requestList!.isEmpty) {
return const Center(child: Text("No requests found")); return const Center(
child: Text(
"No attendance records found",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
);
} }
final list = provider.response!.requestList!; final list = provider.response!.requestList!;
...@@ -344,7 +151,6 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -344,7 +151,6 @@ class _AttendancelistState extends State<Attendancelist> {
itemCount: list.length, itemCount: list.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
final initials = _generateInitials(item);
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
...@@ -399,22 +205,18 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -399,22 +205,18 @@ class _AttendancelistState extends State<Attendancelist> {
item.type ?? "-", item.type ?? "-",
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontFamily: "JakartaRegular",
color: Color(0xff2D2D2D), fontSize: 14,
fontFamily: "Plus Jakarta Sans", color: AppColors.semi_black,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400
), ),
), ),
Text( Text(
item.date ?? "-", item.date ?? "-",
style: const TextStyle( style: TextStyle(
fontSize: 12.5, fontFamily: "JakartaRegular",
color: Color(0xff818181), fontSize: 14,
fontFamily: "Plus Jakarta Sans", color: AppColors.grey_semi,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400
), ),
), ),
], ],
...@@ -426,9 +228,8 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -426,9 +228,8 @@ class _AttendancelistState extends State<Attendancelist> {
item.attendanceType ?? "-", item.attendanceType ?? "-",
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle( style: TextStyle(
fontFamily: "Plus Jakarta Sans", fontFamily: "JakartaMedium",
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500,
color: (item.attendanceType ?? "").toLowerCase() == "live" color: (item.attendanceType ?? "").toLowerCase() == "live"
? Colors.green ? Colors.green
: Colors.orange, : Colors.orange,
...@@ -443,12 +244,11 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -443,12 +244,11 @@ class _AttendancelistState extends State<Attendancelist> {
}, },
), ),
) )
], ],
), ),
bottomNavigationBar: Container( bottomNavigationBar: Container(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
height: 65, height: 54,
decoration: const BoxDecoration(color: Colors.white), decoration: const BoxDecoration(color: Colors.white),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
...@@ -466,6 +266,7 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -466,6 +266,7 @@ class _AttendancelistState extends State<Attendancelist> {
), ),
), ),
).then((_) { ).then((_) {
provider.fetchAttendanceRequests(context);
}); });
}, },
child: Row( child: Row(
...@@ -488,13 +289,12 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -488,13 +289,12 @@ class _AttendancelistState extends State<Attendancelist> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => const AddManualAttendanceScreen(),
const AddManualAttendanceScreen(),
settings: const RouteSettings( settings: const RouteSettings(
name: 'AddManualAttendanceScreen'), name: 'AddManualAttendanceScreen'),
), ),
).then((_) { ).then((_) {
provider.fetchAttendanceRequests(context);
}); });
}, },
child: Row( child: Row(
...@@ -512,21 +312,12 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -512,21 +312,12 @@ class _AttendancelistState extends State<Attendancelist> {
), ),
), ),
); );
}, },
),
),
); );
} },
)
/// Generate avatar initials like L1A, L1R );
String _generateInitials(RequestList item) {
final attType = (item.attendanceType?.isNotEmpty ?? false)
? item.attendanceType![0].toUpperCase()
: "U";
final type = (item.type?.isNotEmpty ?? false)
? item.type![0].toUpperCase()
: "X";
return "$attType$type";
} }
...@@ -567,7 +358,6 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -567,7 +358,6 @@ class _AttendancelistState extends State<Attendancelist> {
return AppColors.rejected_text_color; return AppColors.rejected_text_color;
case 'Updated': case 'Updated':
return AppColors.processed_text_color; return AppColors.processed_text_color;
} }
return color; return color;
} }
...@@ -590,7 +380,4 @@ class _AttendancelistState extends State<Attendancelist> { ...@@ -590,7 +380,4 @@ class _AttendancelistState extends State<Attendancelist> {
return "Requested"; return "Requested";
} }
} }
} }
\ No newline at end of file
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:generp/screens/hrm/OrganizationStructureScreen.dart';
import 'package:generp/screens/hrm/RewardListScreen.dart'; import 'package:generp/screens/hrm/RewardListScreen.dart';
import '../../Utils/app_colors.dart'; import '../../Utils/app_colors.dart';
import 'AttendanceRequestDetail.dart'; import 'AttendanceRequestDetail.dart';
...@@ -121,6 +122,15 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -121,6 +122,15 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
color: const Color(0xffEDF8FF), color: const Color(0xffEDF8FF),
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OrganizationStructureScreen(),
),
);
},
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
...@@ -139,17 +149,25 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -139,17 +149,25 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
], ],
), ),
), ),
),
], ],
), ),
), ),
/// Bottom Grid Section /// Bottom Grid Section
Padding( // Bottom Grid Section
padding: const EdgeInsets.all(15), 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( child: GridView.count(
crossAxisCount: 2, // items per row crossAxisCount: crossAxisCount,
crossAxisSpacing: 8.5, crossAxisSpacing: 8.5,
mainAxisSpacing: 16, mainAxisSpacing: 16,
childAspectRatio: 2.0, // tiles height childAspectRatio: 1.7,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
...@@ -176,11 +194,10 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -176,11 +194,10 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const LeaveApplicationScreen(), builder: (context) => const LeaveApplicationListScreen(),
), ),
); );
}, },
), ),
_buildTile( _buildTile(
label: "Rewards List", label: "Rewards List",
...@@ -212,6 +229,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -212,6 +229,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
), ),
], ],
), ),
);
},
), ),
], ],
), ),
...@@ -228,77 +247,84 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> { ...@@ -228,77 +247,84 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
} }
/// Reusable Tile Widget (Row style) /// Reusable Tile Widget (Row style)
/// Reusable Tile Widget (Row style) - Updated to match design
Widget _buildTile({ Widget _buildTile({
required String label, required String label,
required String subtitle, required String subtitle,
required String assetIcon, // SVG/PNG asset instead of IconData required String assetIcon, // SVG/PNG asset
required Color txtColor, required Color txtColor,
VoidCallback? onTap, VoidCallback? onTap,
}) { }) {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(14),
child: Container( child: Container(
padding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 15,
),
margin: EdgeInsets.symmetric(
vertical: 7,
horizontal: 5,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(14),
), ),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
/// Left side text /// Left side text
Expanded( Expanded(
flex: 2, flex: 2,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
label, label,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 14,
fontFamily: "Plus Jakarta Sans", color: AppColors.app_blue,
fontStyle: FontStyle.normal, fontFamily: "JakartaMedium",
fontWeight: FontWeight.w500,
color: txtColor,
), ),
), ),
const SizedBox(height: 5), SizedBox(height: 4),
Text( Text(
subtitle, subtitle,
style: const TextStyle( style: TextStyle(
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
fontSize: 12, fontSize: 12,
height: 1.4, color: AppColors.grey_semi,
color: Color(0xff818181), fontFamily: "JakartaMedium",
), ),
), ),
], ],
), ),
), ),
SizedBox(width: 10),
/// Right side icon (SVG/PNG) /// Right side icon (SVG/PNG)
Expanded( Expanded(
child: Align( flex: 1,
alignment: Alignment.centerRight,
child: Container( child: Container(
height: 48, height: 42,
width: 48, width: 42,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg color: const Color(0xFFEDF8FF), // icon bg
), ),
child: Center( child: Center(
child: SvgPicture.asset( child: SvgPicture.asset(
height: 28, height: 25,
width: 28, width: 25,
assetIcon, assetIcon,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
), ),
), ),
),
], ],
), ),
), ),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment