Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Sai Srinivas
GEN_ERP_2025
Compare Revisions
5528042935da07e590b68b92d72d5d1b549c8f24...332a8e914c4aab70caf007a280d64c563822a035
Hide whitespace changes
Inline
Side-by-side
lib/Notifiers/HomeScreenNotifier.dart
View file @
332a8e91
...
@@ -16,6 +16,7 @@ class HomescreenNotifier extends ChangeNotifier {
...
@@ -16,6 +16,7 @@ class HomescreenNotifier extends ChangeNotifier {
String
_curdate
=
""
;
String
_curdate
=
""
;
String
_empId
=
""
;
String
_empId
=
""
;
String
_session
=
""
;
String
_session
=
""
;
String
_requestId
=
""
;
String
_webPageUrl
=
""
;
String
_webPageUrl
=
""
;
String
_whizzdomPageUrl
=
""
;
String
_whizzdomPageUrl
=
""
;
String
_roleStatus
=
""
;
String
_roleStatus
=
""
;
...
@@ -34,6 +35,8 @@ class HomescreenNotifier extends ChangeNotifier {
...
@@ -34,6 +35,8 @@ class HomescreenNotifier extends ChangeNotifier {
String
get
session
=>
_session
;
String
get
session
=>
_session
;
String
get
requestId
=>
_requestId
;
String
get
webPageUrl
=>
_webPageUrl
;
String
get
webPageUrl
=>
_webPageUrl
;
String
get
whizzdomPageUrl
=>
_whizzdomPageUrl
;
String
get
whizzdomPageUrl
=>
_whizzdomPageUrl
;
...
@@ -68,6 +71,7 @@ class HomescreenNotifier extends ChangeNotifier {
...
@@ -68,6 +71,7 @@ class HomescreenNotifier extends ChangeNotifier {
_email
=
await
SharedpreferencesService
().
getString
(
"UserEmail"
)
??
""
;
_email
=
await
SharedpreferencesService
().
getString
(
"UserEmail"
)
??
""
;
_session
=
await
SharedpreferencesService
().
getString
(
"Session_id"
)
??
""
;
_session
=
await
SharedpreferencesService
().
getString
(
"Session_id"
)
??
""
;
_roleStatus
=
await
SharedpreferencesService
().
getString
(
"roles"
)
??
""
;
_roleStatus
=
await
SharedpreferencesService
().
getString
(
"roles"
)
??
""
;
_requestId
=
await
SharedpreferencesService
().
getString
(
"attendRequestId"
)
??
""
;
var
lastLocationTime
=
await
SharedpreferencesService
().
getString
(
var
lastLocationTime
=
await
SharedpreferencesService
().
getString
(
"lastLocationTime"
,
"lastLocationTime"
,
);
);
...
...
lib/Notifiers/hrmProvider/LeaveApplicationDetailsProvider.dart
0 → 100644
View file @
332a8e91
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
lib/Notifiers/hrmProvider/attendanceDetailsProvider.dart
0 → 100644
View file @
332a8e91
import
'package:flutter/cupertino.dart'
;
import
'package:generp/Models/hrmModels/attendanceRequestDetailsResponse.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/attendanceRequestListResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
AttendanceDetailsProvider
extends
ChangeNotifier
{
attendanceRequestDetailsResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
attendanceRequestDetailsResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
Future
<
void
>
fetchAttendanceRequestDetail
(
context
,
String
requestId
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
_response
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
attendanceRequestDetailAPI
(
provider
.
empId
,
provider
.
session
,
requestId
);
if
(
result
!=
null
)
{
_response
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Something went wrong:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
}
lib/Notifiers/hrmProvider/attendanceListProvider.dart
0 → 100644
View file @
332a8e91
import
'dart:io'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/attendanceRequestListResponse.dart'
;
import
'../../Models/ordersModels/commonResponse.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
Attendancelistprovider
extends
ChangeNotifier
{
attendanceRequestListResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
attendanceRequestListResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
// Filter states
String
_selectedType
=
"All"
;
String
_selectedDateRange
=
"This Month"
;
DateTimeRange
?
_customDateRange
;
String
get
selectedType
=>
_selectedType
;
String
get
selectedDateRange
=>
_selectedDateRange
;
DateTimeRange
?
get
customDateRange
=>
_customDateRange
;
// Addition of attendance
CommonResponse
?
_addResponse
;
CommonResponse
?
get
addResponse
=>
_addResponse
;
bool
_isSubmitting
=
false
;
bool
get
isSubmitting
=>
_isSubmitting
;
// Date controllers for filter UI
final
TextEditingController
fromDateController
=
TextEditingController
();
final
TextEditingController
toDateController
=
TextEditingController
();
// For manual attendance date field
final
TextEditingController
dateController
=
TextEditingController
();
DateTime
?
_selectedDate
;
DateTime
?
get
selectedDate
=>
_selectedDate
;
// Type options for filter
final
List
<
String
>
typeOptions
=
[
"All"
,
"Check In"
,
"Check Out"
,
"Check In/Out"
];
// Date range options for filter
final
List
<
String
>
dateRangeOptions
=
[
"All"
,
"Today"
,
"Yesterday"
,
"This Month"
,
"Past 7 days"
,
"Last Month"
,
"Custom"
,
];
bool
isDateValid
()
{
if
(
_selectedDate
==
null
)
return
false
;
DateTime
today
=
DateTime
.
now
();
DateTime
yesterday
=
today
.
subtract
(
const
Duration
(
days:
1
));
// normalize (remove time part)
DateTime
normalizedSelected
=
DateTime
(
_selectedDate
!.
year
,
_selectedDate
!.
month
,
_selectedDate
!.
day
);
DateTime
normalizedToday
=
DateTime
(
today
.
year
,
today
.
month
,
today
.
day
);
DateTime
normalizedYesterday
=
DateTime
(
yesterday
.
year
,
yesterday
.
month
,
yesterday
.
day
);
return
normalizedSelected
==
normalizedToday
||
normalizedSelected
==
normalizedYesterday
;
}
String
?
validateManualAttendance
({
//its working or not
required
String
type
,
required
String
?
checkInTime
,
required
String
?
checkInLoc
,
required
File
?
checkInProof
,
required
String
?
checkOutTime
,
required
String
?
checkOutLoc
,
required
File
?
checkOutProof
,
required
String
?
checkInDesc
,
required
String
?
checkOutDesc
,
})
{
if
(!
isDateValid
())
{
return
"Date must be today or yesterday"
;
}
if
(
type
.
isEmpty
)
return
"Please select type"
;
if
(
type
==
"Check In"
)
{
if
((
checkInTime
??
""
).
isEmpty
||
(
checkInLoc
??
""
).
isEmpty
||
(
checkInDesc
??
""
).
isEmpty
||
checkInProof
==
null
)
{
return
"Please fill all Check In fields"
;
}
}
if
(
type
==
"Check Out"
)
{
if
((
checkOutTime
??
""
).
isEmpty
||
(
checkOutLoc
??
""
).
isEmpty
||
(
checkOutDesc
??
""
).
isEmpty
||
checkOutProof
==
null
)
{
return
"Please fill all Check Out fields"
;
}
}
if
(
type
==
"Check In/Out"
)
{
if
((
checkInTime
??
""
).
isEmpty
||
(
checkInLoc
??
""
).
isEmpty
||
(
checkInDesc
??
""
).
isEmpty
||
checkInProof
==
null
||
(
checkOutTime
??
""
).
isEmpty
||
(
checkOutLoc
??
""
).
isEmpty
||
(
checkOutDesc
??
""
).
isEmpty
||
checkOutProof
==
null
)
{
return
"Please fill all Check In & Check Out fields"
;
}
}
return
null
;
// everything ok
}
/// Fetch attendance request list with filters
Future
<
void
>
fetchAttendanceRequests
(
BuildContext
context
,
{
String
?
type
,
String
?
dateRange
,
DateTimeRange
?
customRange
})
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
// Update filter states if provided
if
(
type
!=
null
)
_selectedType
=
type
;
if
(
dateRange
!=
null
)
_selectedDateRange
=
dateRange
;
if
(
customRange
!=
null
)
_customDateRange
=
customRange
;
// Calculate date range based on selection
final
dateParams
=
_getDateRangeParams
(
_selectedDateRange
,
_customDateRange
);
// Convert "All" type to empty string for API
final
apiType
=
_selectedType
==
"All"
?
""
:
_selectedType
;
final
result
=
await
ApiCalling
.
attendanceRequestListAPI
(
provider
.
empId
,
provider
.
session
,
apiType
,
dateParams
[
'from'
]!,
dateParams
[
'to'
]!,
);
debugPrint
(
'Fetching attendance from:
${dateParams['from']}
to:
${dateParams['to']}
'
);
if
(
result
!=
null
)
{
_response
=
result
;
if
(
_response
?.
requestList
==
null
||
_response
!.
requestList
!.
isEmpty
)
{
_errorMessage
=
"No attendance records found!"
;
}
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
debugPrint
(
'Error fetching attendance:
$e
'
);
}
_isLoading
=
false
;
notifyListeners
();
}
/// --- Add Attendance Request ---
Future
<
void
>
addAttendanceRequest
(
BuildContext
context
,
{
required
String
process
,
required
String
type
,
required
String
loc
,
required
String
checkDate
,
String
?
checkInTime
,
String
?
checkInLoc
,
File
?
checkInProof
,
String
?
checkOutTime
,
String
?
checkOutLoc
,
File
?
checkOutProof
,
String
?
note
,
})
async
{
_isSubmitting
=
true
;
_errorMessage
=
null
;
_addResponse
=
null
;
notifyListeners
();
try
{
final
homeProvider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
addAttendanceRequestAPI
(
sessionId:
homeProvider
.
session
,
empId:
homeProvider
.
empId
,
process:
process
,
type:
type
,
loc:
loc
,
checkDate:
checkDate
,
checkInTime:
checkInTime
,
checkInLoc:
checkInLoc
,
checkInProof:
checkInProof
,
checkOutTime:
checkOutTime
,
checkOutLoc:
checkOutLoc
,
checkOutProof:
checkOutProof
,
note:
note
,
);
if
(
result
!=
null
)
{
_addResponse
=
result
;
if
(
result
.
error
!=
null
&&
result
.
error
!.
isNotEmpty
)
{
_errorMessage
=
result
.
error
;
}
else
{
_addResponse
=
result
;
}
}
else
{
_errorMessage
=
"Failed to submit request!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error submitting request:
$e
"
;
}
_isSubmitting
=
false
;
notifyListeners
();
}
/// Apply filters coming from bottom sheet
void
updateFiltersFromSheet
(
BuildContext
context
,
{
required
String
type
,
required
String
selectedValue
,
DateTimeRange
?
customRange
,
})
{
_selectedType
=
type
;
_selectedDateRange
=
selectedValue
;
_customDateRange
=
customRange
;
fetchAttendanceRequests
(
context
,
type:
_selectedType
,
dateRange:
_selectedDateRange
,
customRange:
_customDateRange
,
);
}
/// Set type filter and refresh data
void
setTypeFilter
(
BuildContext
context
,
String
type
)
{
_selectedType
=
type
;
fetchAttendanceRequests
(
context
);
}
/// Set date range filter and refresh data
void
setDateRangeFilter
(
BuildContext
context
,
String
dateRange
,
{
DateTimeRange
?
customRange
})
{
_selectedDateRange
=
dateRange
;
if
(
customRange
!=
null
)
{
_customDateRange
=
customRange
;
fromDateController
.
text
=
_formatDate
(
customRange
.
start
);
toDateController
.
text
=
_formatDate
(
customRange
.
end
);
}
fetchAttendanceRequests
(
context
);
}
/// Clear all filters and refresh data
void
clearFilters
(
BuildContext
context
)
{
_selectedType
=
"All"
;
_selectedDateRange
=
"This Month"
;
_customDateRange
=
null
;
fromDateController
.
clear
();
toDateController
.
clear
();
fetchAttendanceRequests
(
context
);
}
/// Reset form and data
void
resetForm
(
BuildContext
context
)
{
_response
=
null
;
_errorMessage
=
null
;
clearFilters
(
context
);
notifyListeners
();
}
/// Get date range parameters for API
Map
<
String
,
String
>
_getDateRangeParams
(
String
dateRange
,
DateTimeRange
?
customRange
)
{
final
now
=
DateTime
.
now
();
final
formatter
=
DateFormat
(
"dd MMM yyyy"
);
late
DateTime
from
;
late
DateTime
to
;
switch
(
dateRange
)
{
case
"All"
:
from
=
DateTime
(
now
.
year
-
1
);
to
=
now
;
break
;
case
"Today"
:
from
=
now
;
to
=
now
;
break
;
case
"Yesterday"
:
from
=
now
.
subtract
(
const
Duration
(
days:
1
));
to
=
now
.
subtract
(
const
Duration
(
days:
1
));
break
;
case
"This Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
+
1
,
0
);
break
;
case
"Past 7 days"
:
from
=
now
.
subtract
(
const
Duration
(
days:
6
));
to
=
now
;
break
;
case
"Last Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
-
1
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
,
0
);
break
;
case
"Custom"
:
if
(
customRange
!=
null
)
{
from
=
customRange
.
start
;
to
=
customRange
.
end
;
}
else
{
from
=
now
.
subtract
(
const
Duration
(
days:
30
));
to
=
now
;
}
break
;
default
:
from
=
now
;
to
=
now
;
}
return
{
"from"
:
formatter
.
format
(
from
),
"to"
:
formatter
.
format
(
to
),
};
}
/// Format date for display
String
_formatDate
(
DateTime
date
)
{
return
DateFormat
(
"dd MMM yyyy"
).
format
(
date
);
}
/// Apply filters and refresh data
void
applyFilters
(
BuildContext
context
)
{
fetchAttendanceRequests
(
context
,
type:
_selectedType
,
dateRange:
_selectedDateRange
,
customRange:
_customDateRange
,
);
}
/// Set Selected Date
void
setSelectedDate
(
DateTime
date
)
{
_selectedDate
=
date
;
dateController
.
text
=
DateFormat
(
"dd MMM yyyy"
).
format
(
date
);
notifyListeners
();
}
/// Show Cupertino Date Picker
void
showDatePickerDialog
(
BuildContext
context
)
{
if
(
_selectedDate
==
null
)
{
setSelectedDate
(
DateTime
.
now
());
}
showCupertinoModalPopup
<
void
>(
context:
context
,
builder:
(
BuildContext
context
)
=>
Container
(
height:
250
,
padding:
const
EdgeInsets
.
only
(
top:
6.0
),
margin:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
color:
CupertinoColors
.
systemBackground
.
resolveFrom
(
context
),
child:
SafeArea
(
top:
false
,
child:
Column
(
children:
[
// Cancel + Done Buttons
SizedBox
(
height:
55
,
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
CupertinoButton
(
child:
Text
(
'Cancel'
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
CupertinoButton
(
child:
Text
(
'Done'
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
onPressed:
()
{
setSelectedDate
(
_selectedDate
??
DateTime
.
now
());
Navigator
.
pop
(
context
);
},
),
],
),
),
// Cupertino Date Picker
Expanded
(
child:
CupertinoDatePicker
(
dateOrder:
DatePickerDateOrder
.
dmy
,
initialDateTime:
_selectedDate
??
DateTime
.
now
(),
mode:
CupertinoDatePickerMode
.
date
,
use24hFormat:
true
,
showDayOfWeek:
true
,
onDateTimeChanged:
(
DateTime
newDate
)
{
setSelectedDate
(
newDate
);
},
),
),
],
),
),
),
);
}
}
lib/Notifiers/hrmProvider/leaveApplicationListProvider.dart
0 → 100644
View file @
332a8e91
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
();
}
}
lib/Notifiers/hrmProvider/rewardListProvider.dart
0 → 100644
View file @
332a8e91
import
'package:flutter/material.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/rewardListResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
RewardListProvider
extends
ChangeNotifier
{
rewardListResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
/// Getters
rewardListResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
/// Fetch Reward List
Future
<
void
>
fetchRewardList
(
BuildContext
context
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
_response
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
rewardListAPI
(
provider
.
empId
,
provider
.
session
,
);
if
(
result
!=
null
)
{
_response
=
result
;
}
else
{
_errorMessage
=
"No reward data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Something went wrong:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
}
lib/Notifiers/hrmProvider/tourExpensesDetailsProvider.dart
0 → 100644
View file @
332a8e91
import
'package:flutter/cupertino.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/tourExpensesDetailsResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
TourExpensesDetailsProvider
extends
ChangeNotifier
{
tourExpensesDetailsResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
tourExpensesDetailsResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
Future
<
void
>
fetchTourExpensesDetails
(
BuildContext
context
,
String
tourBillId
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
_response
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
tourExpensesDetailAPI
(
provider
.
session
,
provider
.
empId
,
tourBillId
,
);
print
(
"==== Tour Submitted ===="
);
print
(
"empId: "
+
provider
.
empId
);
print
(
"Session:
${provider.session}
"
);
print
(
":
$result
."
);
print
(
"finish"
);
print
(
"============================="
);
if
(
result
!=
null
)
{
_response
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Something went wrong:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
}
lib/Notifiers/hrmProvider/tourExpensesProvider.dart
0 → 100644
View file @
332a8e91
import
'dart:io'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:generp/Models/hrmModels/tourExpensesAddViewResponse.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/tourExpensesListResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
TourExpensesProvider
extends
ChangeNotifier
{
tourExpensesListResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
tourExpensesListResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
tourExpensesAddViewResponse
?
_response2
;
tourExpensesAddViewResponse
?
get
response2
=>
_response2
;
List
<
String
>
get
daAmountList
=>
_response2
?.
daAmount
?.
map
((
e
)
=>
e
.
toString
()).
toList
()
??
[];
List
<
String
>
get
tourTypeList
=>
_response2
?.
tourType
??
[];
List
<
String
>
get
travelTypeList
=>
_response2
?.
travelType
??
[];
// Controllers for Add form
final
TextEditingController
fromDateField
=
TextEditingController
();
final
TextEditingController
toDateField
=
TextEditingController
();
final
TextEditingController
dateController
=
TextEditingController
();
DateTime
?
_date
;
DateTime
?
_fromDate
;
DateTime
?
_toDate
;
/// Format date (yyyy-MM-dd)
String
_formatDate
(
DateTime
date
)
{
return
DateFormat
(
'yyyy-MM-dd'
).
format
(
date
);
}
/// Set single date
void
setDate
(
DateTime
newDate
)
{
_date
=
newDate
;
dateController
.
text
=
_formatDate
(
newDate
);
notifyListeners
();
}
/// Fetch tour expenses list
Future
<
void
>
fetchTourExpenses
(
BuildContext
context
,
String
pageNumber
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
tourExpensesListAPI
(
provider
.
empId
,
provider
.
session
,
pageNumber
,
);
debugPrint
(
'empId:
${provider.empId}
, session:
${provider.session}
'
);
if
(
result
!=
null
)
{
_response
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
Future
<
void
>
fetchTourExpensesAddView
(
BuildContext
context
,
String
tourBillId
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
tourExpensesAddViewAPI
(
provider
.
empId
,
provider
.
session
,
tourBillId
,
);
debugPrint
(
'empId:
${provider.empId}
, session:
${provider.session}
, tourBillId:
$tourBillId
'
);
if
(
result
!=
null
)
{
_response2
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
Future
<
bool
>
addTourBill
({
required
BuildContext
context
,
required
String
placeOfVisit
,
required
String
daAmount
,
required
String
tourType
,
required
String
tourDate
,
required
List
<
Map
<
String
,
dynamic
>>
travelExpenses
,
required
List
<
Map
<
String
,
dynamic
>>
hotelExpenses
,
required
List
<
Map
<
String
,
dynamic
>>
otherExpenses
,
List
<
File
>?
travelImages
,
List
<
File
>?
hotelImages
,
List
<
File
>?
otherImages
,
})
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
homeProvider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
if
((
homeProvider
.
session
??
""
).
isEmpty
||
(
homeProvider
.
empId
??
""
).
isEmpty
)
{
_errorMessage
=
"Invalid session or employee ID"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
debugPrint
(
"Submitting Tour Bill => "
"place:
$placeOfVisit
, da:
$daAmount
, type:
$tourType
, "
"date:
$tourDate
, travelExp:
${travelExpenses.length}
, "
"hotelExp:
${hotelExpenses.length}
, "
"otherExp:
${otherExpenses.length}
, "
"travelImages:
${travelImages?.length}
, "
"hotelImages:
${hotelImages?.length}
,"
"otherImages:
${otherImages?.length}
"
);
final
result
=
await
ApiCalling
.
addTourBillAPI
(
sessionId:
homeProvider
.
session
??
""
,
empId:
homeProvider
.
empId
??
""
,
placeOfVisit:
placeOfVisit
,
daAmount:
daAmount
,
tourType:
tourType
,
tourDate:
tourDate
,
travelExpenses:
travelExpenses
,
hotelExpenses:
hotelExpenses
,
otherExpenses:
otherExpenses
,
travelImages:
travelImages
,
hotelImages:
hotelImages
,
otherImages:
otherImages
,
);
if
(
result
!=
null
)
{
debugPrint
(
" Tour Bill Added Successfully"
);
_isLoading
=
false
;
notifyListeners
();
return
true
;
}
else
{
_errorMessage
=
"Failed to add Tour Bill"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
}
/// Show Cupertino DatePicker for leave form
/// Show Cupertino DatePicker for leave form
Future
<
DateTime
?>
showDatePickerDialog
(
BuildContext
context
,
{
bool
isFromDate
=
true
})
async
{
DateTime
currentDate
=
DateTime
.
now
();
DateTime
?
pickedDate
;
await
showCupertinoModalPopup
<
void
>(
context:
context
,
builder:
(
BuildContext
context
)
=>
Container
(
height:
250
,
padding:
const
EdgeInsets
.
only
(
top:
6.0
),
margin:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
color:
CupertinoColors
.
systemBackground
.
resolveFrom
(
context
),
child:
SafeArea
(
top:
false
,
child:
Column
(
children:
[
SizedBox
(
height:
55
,
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
CupertinoButton
(
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
CupertinoButton
(
child:
const
Text
(
"Done"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
{
pickedDate
=
currentDate
;
if
(
isFromDate
)
{
fromDateField
.
text
=
_formatDate
(
pickedDate
!);
}
else
{
toDateField
.
text
=
_formatDate
(
pickedDate
!);
}
Navigator
.
pop
(
context
);
},
),
],
),
),
Expanded
(
child:
CupertinoDatePicker
(
dateOrder:
DatePickerDateOrder
.
dmy
,
initialDateTime:
currentDate
,
mode:
CupertinoDatePickerMode
.
date
,
onDateTimeChanged:
(
DateTime
newDate
)
{
currentDate
=
newDate
;
},
),
),
],
),
),
),
);
return
pickedDate
;
}
}
lib/main.dart
View file @
332a8e91
...
@@ -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'
;
...
@@ -226,6 +227,12 @@ class MyApp extends StatelessWidget {
...
@@ -226,6 +227,12 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider
(
create:
(
_
)
=>
followUpUpdateProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
followUpUpdateProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
Appointmentcalendarprovider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
Appointmentcalendarprovider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
Addnewleadsandprospectsprovider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
Addnewleadsandprospectsprovider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
Attendancelistprovider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
AttendanceDetailsProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
TourExpensesProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
TourExpensesDetailsProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
RewardListProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
LeaveApplicationListProvider
()),
],
],
child:
Builder
(
child:
Builder
(
builder:
(
BuildContext
context
)
{
builder:
(
BuildContext
context
)
{
...
...
lib/screens/CommonFilter2.dart
0 → 100644
View file @
332a8e91
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
];
}
}
lib/screens/HomeScreen.dart
View file @
332a8e91
...
@@ -10,6 +10,7 @@ import 'package:flutter_svg/flutter_svg.dart';
...
@@ -10,6 +10,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import
'package:generp/Utils/commonWidgets.dart'
;
import
'package:generp/Utils/commonWidgets.dart'
;
import
'../Utils/commonServices.dart'
;
import
'../Utils/commonServices.dart'
;
import
'genTracker/ScanEnterGeneratorIDScreen.dart'
;
import
'genTracker/ScanEnterGeneratorIDScreen.dart'
;
import
'hrm/HrmDashboardScreen.dart'
;
import
'notifierExports.dart'
;
import
'notifierExports.dart'
;
import
'screensExports.dart'
;
import
'screensExports.dart'
;
import
'package:geolocator/geolocator.dart'
;
import
'package:geolocator/geolocator.dart'
;
...
@@ -124,6 +125,8 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -124,6 +125,8 @@ class _MyHomePageState extends State<MyHomePage> {
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
double
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
double
screenHeight
=
MediaQuery
.
of
(
context
).
size
.
height
;
switch
(
_source
.
keys
.
toList
()[
0
])
{
switch
(
_source
.
keys
.
toList
()[
0
])
{
case
ConnectivityResult
.
mobile
:
case
ConnectivityResult
.
mobile
:
connection
=
'Online'
;
connection
=
'Online'
;
...
@@ -138,10 +141,10 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -138,10 +141,10 @@ class _MyHomePageState extends State<MyHomePage> {
return
(
connection
==
'Online'
)
return
(
connection
==
'Online'
)
?
Consumer2
<
HomescreenNotifier
,
ProfileNotifer
>(
?
Consumer2
<
HomescreenNotifier
,
ProfileNotifer
>(
builder:
(
context
,
homescreen
,
profile
,
child
)
{
builder:
(
context
,
homescreen
,
profile
,
child
)
{
final
coreRequiredRoles
=
[
"12"
,
"540"
,
"433"
,
"434"
,
];
final
coreRequiredRoles
=
[
"12"
,
"540"
,
"433"
,
"434"
,
"430"
];
final
requiredRoles
=
[
"430"
,
"430"
,
"431"
,
"431"
];
final
requiredRoles
=
[
"430"
,
"430"
,
"431"
,
"431"
];
final
coreNames
=
[
"CRM"
,
"Orders"
,
"Service"
,
"Gen Tracker"
,
];
final
coreNames
=
[
"CRM"
,
"Orders"
,
"Service"
,
"Gen Tracker"
,
"HRM"
];
final
names
=
[
"Attendance"
,
"Finance"
,
"ERP"
,
"Whizzdom"
];
final
names
=
[
"Attendance"
,
"Finance"
,
"ERP"
,
"Whizzdom"
];
final
subtitles
=
[
final
subtitles
=
[
"Check-in,Check-out"
,
"Check-in,Check-out"
,
...
@@ -154,6 +157,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -154,6 +157,7 @@ class _MyHomePageState extends State<MyHomePage> {
"assets/svg/home/home_order_ic.svg"
,
"assets/svg/home/home_order_ic.svg"
,
"assets/svg/home/home_service_ic.svg"
,
"assets/svg/home/home_service_ic.svg"
,
"assets/svg/home/home_gentracker_ic.svg"
,
"assets/svg/home/home_gentracker_ic.svg"
,
"assets/svg/home/home_erp_ic.svg"
,
];
];
final
icons
=
[
final
icons
=
[
...
@@ -167,6 +171,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -167,6 +171,7 @@ class _MyHomePageState extends State<MyHomePage> {
"Orders, TPC, Dispatch"
,
"Orders, TPC, Dispatch"
,
"Visits, P.C. Wallet"
,
"Visits, P.C. Wallet"
,
"Generator Details"
,
"Generator Details"
,
"Tour Bills, Live Attendance"
,
];
];
final
coreFilteredItems
=
<
Map
<
String
,
String
>>[];
final
coreFilteredItems
=
<
Map
<
String
,
String
>>[];
final
filteredItems
=
<
Map
<
String
,
String
>>[];
final
filteredItems
=
<
Map
<
String
,
String
>>[];
...
@@ -211,23 +216,23 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -211,23 +216,23 @@ class _MyHomePageState extends State<MyHomePage> {
toolbarHeight:
0
,
toolbarHeight:
0
,
backgroundColor:
Colors
.
white
,
backgroundColor:
Colors
.
white
,
),
),
body:
Container
(
body:
SingleChildScrollView
(
decoration:
BoxDecoration
(
child:
Container
(
gradient:
LinearGradient
(
decoration:
BoxDecoration
(
colors:
[
gradient:
LinearGradient
(
AppColors
.
scaffold_bg_color
,
colors:
[
AppColors
.
scaffold_bg_color
,
AppColors
.
scaffold_bg_color
,
Color
(
0xFFCEEDFF
),
AppColors
.
scaffold_bg_color
,
],
Color
(
0xFFCEEDFF
),
begin:
Alignment
.
topCenter
,
],
end:
Alignment
.
bottomCenter
,
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
),
),
),
),
child:
Column
(
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
children:
[
Expanded
(
InkResponse
(
flex:
4
,
child:
InkResponse
(
onTap:
()
{
onTap:
()
{
HapticFeedback
.
selectionClick
();
HapticFeedback
.
selectionClick
();
_showProfileBottomSheet
(
_showProfileBottomSheet
(
...
@@ -275,9 +280,9 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -275,9 +280,9 @@ class _MyHomePageState extends State<MyHomePage> {
mainAxisAlignment:
mainAxisAlignment:
MainAxisAlignment
.
center
,
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
children:
[
// Text(
// Text(
// "${profile.employeeName}",
// "${profile.employeeName}",
// maxLines: 1,
// maxLines: 1,
...
@@ -331,7 +336,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -331,7 +336,7 @@ class _MyHomePageState extends State<MyHomePage> {
mainAxisAlignment:
mainAxisAlignment:
MainAxisAlignment
.
start
,
MainAxisAlignment
.
start
,
children:
[
children:
[
Container
(
Container
(
width:
12
,
width:
12
,
height:
12
,
height:
12
,
...
@@ -353,8 +358,8 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -353,8 +358,8 @@ class _MyHomePageState extends State<MyHomePage> {
color:
Color
(
0xFF2D2D2D
),
color:
Color
(
0xFF2D2D2D
),
),
),
),
),
],
],
),
),
],
],
...
@@ -396,7 +401,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -396,7 +401,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
),
),
],
],
),
),
),
),
...
@@ -406,10 +411,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -406,10 +411,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
),
),
),
SizedBox
(
Expanded
(
flex:
13
,
child:
SizedBox
(
child:
Column
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
children:
[
...
@@ -541,8 +543,8 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -541,8 +543,8 @@ class _MyHomePageState extends State<MyHomePage> {
// ),
// ),
// ),
// ),
// ],
// ],
if
(
homescreen
.
roleStatus
.
contains
(
if
(
homescreen
.
roleStatus
.
contains
(
"432"
,
"432"
,
))
...[
))
...[
...
@@ -556,7 +558,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -556,7 +558,7 @@ class _MyHomePageState extends State<MyHomePage> {
left:
10
,
left:
10
,
bottom:
5
,
bottom:
5
,
top:
10
top:
10
),
),
child:
Text
(
child:
Text
(
"Workforce & Operations"
,
"Workforce & Operations"
,
...
@@ -723,7 +725,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -723,7 +725,7 @@ class _MyHomePageState extends State<MyHomePage> {
left:
10
,
left:
10
,
bottom:
5
,
bottom:
5
,
top:
10
top:
10
),
),
padding:
const
EdgeInsets
.
only
(
padding:
const
EdgeInsets
.
only
(
top:
10
,
top:
10
,
...
@@ -950,7 +952,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -950,7 +952,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
);
}
}
},
},
child:
Container
(
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
padding:
EdgeInsets
.
symmetric
(
vertical:
5
,
vertical:
5
,
...
@@ -1027,7 +1029,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1027,7 +1029,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
],
],
],
],
// if (filteredItems.isNotEmpty) ...[
// if (filteredItems.isNotEmpty) ...[
// Container(
// Container(
// margin: EdgeInsets.symmetric(
// margin: EdgeInsets.symmetric(
...
@@ -1235,7 +1237,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1235,7 +1237,7 @@ class _MyHomePageState extends State<MyHomePage> {
// ),
// ),
// ),
// ),
// ],
// ],
if
(
coreFilteredItems
.
isNotEmpty
)
...[
if
(
coreFilteredItems
.
isNotEmpty
)
...[
Container
(
Container
(
margin:
EdgeInsets
.
only
(
margin:
EdgeInsets
.
only
(
...
@@ -1263,6 +1265,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1263,6 +1265,7 @@ class _MyHomePageState extends State<MyHomePage> {
child:
GridView
.
builder
(
child:
GridView
.
builder
(
shrinkWrap:
true
,
shrinkWrap:
true
,
itemCount:
coreFilteredItems
.
length
,
itemCount:
coreFilteredItems
.
length
,
physics:
NeverScrollableScrollPhysics
(),
gridDelegate:
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount
(
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
2
,
crossAxisCount:
2
,
...
@@ -1332,6 +1335,19 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1332,6 +1335,19 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
);
);
case
"HRM"
:
res
=
await
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
HrmdashboardScreen
(),
settings:
RouteSettings
(
name:
'CrmdashboardScreen'
,
),
),
);
default
:
default
:
print
(
"111"
);
print
(
"111"
);
break
;
break
;
...
@@ -1342,7 +1358,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1342,7 +1358,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
);
}
}
},
},
child:
Container
(
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
padding:
EdgeInsets
.
symmetric
(
vertical:
5
,
vertical:
5
,
...
@@ -1406,7 +1422,7 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1406,7 +1422,7 @@ class _MyHomePageState extends State<MyHomePage> {
Expanded
(
Expanded
(
flex:
1
,
flex:
1
,
child:
SvgPicture
.
asset
(
child:
SvgPicture
.
asset
(
f
ilteredItems
[
ci
][
'icon'
]
??
coreF
ilteredItems
[
ci
][
'icon'
]
??
"-"
,
"-"
,
),
),
),
),
...
@@ -1418,241 +1434,241 @@ class _MyHomePageState extends State<MyHomePage> {
...
@@ -1418,241 +1434,241 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
),
),
],
],
],
],
),
),
),
),
),
Align
(
Align
(
alignment:
Alignment
.
bottomCenter
,
child:
Container
(
height:
40
,
alignment:
Alignment
.
bottomCenter
,
alignment:
Alignment
.
bottomCenter
,
margin:
EdgeInsets
.
only
(
bottom:
20
),
child:
Container
(
child:
Image
.
asset
(
height:
40
,
fit:
BoxFit
.
scaleDown
,
alignment:
Alignment
.
bottomCenter
,
"assets/images/horizontal_logo.png"
,
margin:
EdgeInsets
.
only
(
bottom:
20
),
child:
Image
.
asset
(
fit:
BoxFit
.
scaleDown
,
"assets/images/horizontal_logo.png"
,
),
),
),
),
),
),
// Expanded(
//
Expanded(
//
flex: 10,
//
flex: 10,
//
child: Container(
//
child: Container
(
//
padding: EdgeInsets.only
(
//
padding: EdgeInsets.only(
//
left: 20,
//
lef
t: 20,
//
righ
t: 20,
//
right
:
2
0,
//
top
: 0,
// to
p
: 0,
//
bot
to
m
:
1
0,
//
bottom: 10
,
//
)
,
// ),
//
margin: EdgeInsets.only(top: 10
),
//
margin: EdgeInsets.only(top: 10),
//
child: GridView.builder(
//
child: GridView.builder(
//
itemCount: filteredItems.length,
//
itemCount: filteredItems.length,
//
gridDelegate:
//
gridDelegate:
//
SliverGridDelegateWithFixedCrossAxisCount(
//
SliverGridDelegateWithFixedC
rossAxisCount
(
//
c
rossAxisCount
: 2,
// crossAxis
Count: 2
,
// crossAxis
Spacing: 10
,
//
cross
AxisSpacing: 10,
//
main
AxisSpacing: 10,
//
mainAxisSpacing: 10
,
//
)
,
//
),
//
itemBuilder: (context, index) {
// item
Bu
il
d
er
: (context,
index
) {
//
final
item
= f
il
t
er
edItems[
index
];
//
final item = filteredItems[index];
//
return InkResponse(
//
return InkResponse(
//
onTap: () async {
//
onTap: () async {
//
var res;
//
var res;
//
switch (item['name']) {
//
switch (item['name']) {
//
case "Attendance":
//
case "Attendance":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
AttendanceScreen(),
//
AttendanceScreen(),
//
settings: RouteSettings(
//
settings: RouteSettings(
//
arguments: 'AttendanceScreen',
//
arguments: 'AttendanceScreen'
,
//
)
,
//
),
//
),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "ERP":
//
case "ERP":
//
bool isGpsEnabled =
//
bool isGps
Enabled
=
//
await Geolocator.isLocationService
Enabled
();
//
await Geolocator.isLocationService
Enabled
();
//
if (isGps
Enabled
) {
//
if (
isGpsEnable
d) {
//
if (
Platform.isAndroi
d) {
//
if (Pl
at
f
or
m.isAndroid) {
//
res = await Navig
ator
.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) => WebErpScreen(
//
(context) => WebErpScreen(
//
erp_url:
//
erp_url:
//
homescreen
//
homescreen
//
.webPageUrl,
//
.webPageUrl
,
//
)
,
//
),
//
),
//
),
//
);
//
);
//
} else {
//
} else {
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) => WebERPIOS(
//
(context) => WebERPIOS(
//
url:
//
url:
//
homescreen
//
homescreen
//
.webPageUrl,
//
.webPageUrl
,
//
)
,
//
),
//
),
//
),
//
);
//
);
//
}
//
}
//
} else {
//
} else {
//
requestGpsPermission();
//
requestGpsPermission();
//
}
//
}
//
//
// break;
//
break;
//
case "Gen Tracker":
//
case "Gen Tracker":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Gentrackerdashboard(),
//
Gentrackerdashboard(),
//
settings: RouteSettings(
//
settings: RouteSettings(
//
arguments:
//
arguments:
//
'Gentrackerdashboard',
//
'Gentrackerdashboard'
,
//
)
,
//
),
//
),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "Service Engineer":
//
case "Service Engineer":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Serviceengineerdashboard(),
//
Serviceengineerdashboard(
),
// ),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "Nearby":
//
case "Nearby":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Nearbygenerators(),
//
Nearbygenerators(
),
// ),
//
),
//
);
//
);
//
//
// break;
//
break;
//
case "Inventory":
//
case "Inventory":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
InventoryScreen(),
//
InventoryScreen(
),
// ),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "Whizzdom":
//
case "Whizzdom":
//
bool isGpsEnabled =
//
bool isGps
Enabled
=
//
await Geolocator.isLocationService
Enabled
();
//
await Geolocator.isLocationService
Enabled
();
//
if (isGps
Enabled
) {
//
if (isGpsEnabled) {
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(
//
(
//
context,
//
context,
//
) => WebWhizzdomScreen(
//
) => WebWhizzdomScreen(
//
whizzdom_url:
//
whizzdom_url:
//
homescreen
//
homescreen
//
.whizzdomPageUrl,
//
.whizzdomPageUrl
,
//
)
,
//
),
//
),
//
),
//
);
//
);
//
} else {
//
} else {
//
requestGpsPermission();
//
requestGpsPermission();
//
}
//
}
//
break;
//
break;
//
case "Common":
//
case "Common":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Commondashboard(),
//
Commondashboard(
),
// ),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "Finance":
//
case "Finance":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Financedashboard(),
//
Financedashboard(),
//
settings: RouteSettings(
//
settings: RouteSettings(
//
arguments: 'Financedashboard',
//
arguments: 'Financedashboard'
,
//
)
,
//
),
//
),
//
),
//
);
//
)
;
//
break
;
//
break;
//
case "Orders":
//
case "Orders":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
Ordermoduledashboard(),
//
Ordermoduledashboard(
),
// ),
//
),
//
);
//
);
//
case "CRM":
//
case "CRM":
//
res = await Navigator.push(
//
res = await Navigator.push(
//
context,
//
context,
//
MaterialPageRoute(
//
MaterialPageRoute(
//
builder:
//
builder:
//
(context) =>
//
(context) =>
//
CrmdashboardScreen(),
//
CrmdashboardScreen(),
//
settings: RouteSettings(
//
settings: RouteSettings(
//
name: 'CrmdashboardScreen',
//
name: 'CrmdashboardScreen'
,
//
)
,
//
),
//
),
//
),
//
);
//
);
//
default:
//
default:
//
print("111");
//
print("111")
;
//
break
;
//
break;
//
}
//
}
//
if (res == true) {
//
if (res == true) {
//
homescreen.DashboardApiFunction(
//
homescreen.DashboardApiFunction(
//
context,
//
context,
//
);
//
);
//
}
//
}
// }
,
//
},
//
child: Container(
//
child: Container
(
//
decoration: BoxDecoration
(
//
decoration: BoxDecoration(
//
color: Colors.white,
//
color: Colors.white
,
//
borderRadius: BorderRadius.circular(30)
,
//
borderRadius: BorderRadius.circular(30
),
// ),
//
),
//
child: Column(
//
child: Column(
//
crossAxisAlignment:
//
c
rossAxisAlignment
:
//
C
rossAxisAlignment
.center,
//
Cross
AxisAlignment
.center,
//
main
AxisAlignment
:
//
m
ainAxisAlignment
:
//
M
ainAxisAlignment
.center,
//
MainAxisAlignment.center,
//
children: [
//
children: [
//
SvgPicture.asset(
//
SvgPicture.asset(
//
item['icon']!,
//
item['icon']!
,
//
height: 45
,
//
height: 45
,
//
)
,
// ),
//
SizedBox(height: 10
),
//
SizedBox(height: 10
),
//
Text(item['name']!
),
//
Text(item['name']!)
,
//
]
,
//
]
,
//
)
,
//
),
//
),
//
),
//
);
//
);
//
},
//
}
,
//
)
,
//
),
//
),
//
),
//
),
// )
,
]
,
]
,
)
,
),
),
),
),
// floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
...
...
lib/screens/hrm/AddLeaveRequestScreen.dart
0 → 100644
View file @
332a8e91
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
;
}
}
lib/screens/hrm/AddLiveAttendance.dart
0 → 100644
View file @
332a8e91
import
'dart:io'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:geocoding/geocoding.dart'
;
import
'package:geolocator/geolocator.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
class
AddLiveAttendanceScreen
extends
StatefulWidget
{
const
AddLiveAttendanceScreen
({
Key
?
key
})
:
super
(
key:
key
);
@override
State
<
AddLiveAttendanceScreen
>
createState
()
=>
_AddLiveAttendanceScreenState
();
}
class
_AddLiveAttendanceScreenState
extends
State
<
AddLiveAttendanceScreen
>
{
String
?
typeError
;
String
?
locationError
;
String
?
proofError
;
String
?
selectedType
;
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
TextEditingController
locationController
=
TextEditingController
();
final
TextEditingController
descriptionController
=
TextEditingController
();
final
List
<
String
>
types
=
[
"Check In"
,
"Check Out"
];
final
ImagePicker
picker
=
ImagePicker
();
XFile
?
proofFile
;
bool
isSubmitting
=
false
;
String
get
locationHeading
=>
selectedType
==
null
?
"Location"
:
"
$selectedType
Location"
;
String
get
descriptionHeading
=>
selectedType
==
null
?
"Description"
:
"
$selectedType
Description"
;
String
get
proofButtonText
=>
selectedType
==
null
?
"Attach Proof"
:
"Attach
$selectedType
Proof"
;
bool
get
isSubmitEnabled
=>
selectedType
!=
null
&&
locationController
.
text
.
trim
().
isNotEmpty
&&
proofFile
!=
null
;
@override
void
initState
()
{
super
.
initState
();
_autoFetchLocation
();
}
Future
<
void
>
_autoFetchLocation
()
async
{
String
loc
=
await
getCurrentLocation
();
setState
(()
{
locationController
.
text
=
loc
;
});
}
Future
<
String
>
getCurrentLocation
()
async
{
try
{
LocationPermission
permission
=
await
Geolocator
.
checkPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
permission
=
await
Geolocator
.
requestPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
return
"Location permission denied"
;
}
}
if
(
permission
==
LocationPermission
.
deniedForever
)
{
return
"Location permissions permanently denied"
;
}
Position
position
=
await
Geolocator
.
getCurrentPosition
(
desiredAccuracy:
LocationAccuracy
.
high
);
List
<
Placemark
>
placemarks
=
await
placemarkFromCoordinates
(
position
.
latitude
,
position
.
longitude
);
if
(
placemarks
.
isNotEmpty
)
{
Placemark
place
=
placemarks
.
first
;
return
"
${place.name}
,
${place.locality}
,
${place.administrativeArea}
,
${place.country}
"
;
}
else
{
return
"
${position.latitude}
,
${position.longitude}
"
;
}
}
catch
(
e
)
{
return
"Error:
$e
"
;
}
}
void
_showPicker
(
BuildContext
context
)
{
showModalBottomSheet
(
context:
context
,
builder:
(
BuildContext
bc
)
{
return
SafeArea
(
child:
Wrap
(
children:
<
Widget
>[
ListTile
(
leading:
const
Icon
(
Icons
.
photo_camera
),
title:
const
Text
(
'Camera'
),
onTap:
()
async
{
Navigator
.
of
(
context
).
pop
();
final
XFile
?
image
=
await
picker
.
pickImage
(
source
:
ImageSource
.
camera
);
if
(
image
!=
null
)
{
setState
(()
=>
proofFile
=
image
);
}
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
'Gallery'
),
onTap:
()
async
{
Navigator
.
of
(
context
).
pop
();
final
XFile
?
image
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
if
(
image
!=
null
)
{
setState
(()
=>
proofFile
=
image
);
}
},
),
],
),
);
},
);
}
void
submitAttendance
(
BuildContext
context
)
async
{
setState
(()
{
typeError
=
null
;
locationError
=
null
;
proofError
=
null
;
});
bool
hasError
=
false
;
if
(
selectedType
==
null
)
{
typeError
=
"Please select a type"
;
hasError
=
true
;
}
if
(
locationController
.
text
.
trim
().
isEmpty
)
{
locationError
=
"Please enter a location"
;
hasError
=
true
;
}
if
(
proofFile
==
null
)
{
proofError
=
"Please attach proof"
;
hasError
=
true
;
}
if
(
hasError
)
{
setState
(()
{});
return
;
}
setState
(()
=>
isSubmitting
=
true
);
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
await
provider
.
addAttendanceRequest
(
context
,
process:
"Live"
,
type:
selectedType
??
""
,
loc:
locationController
.
text
,
checkDate:
DateTime
.
now
().
toString
().
split
(
" "
).
first
,
checkInTime:
selectedType
==
"Check In"
?
TimeOfDay
.
now
().
format
(
context
)
:
null
,
checkInLoc:
selectedType
==
"Check In"
?
locationController
.
text
:
null
,
checkInProof:
selectedType
==
"Check In"
?
File
(
proofFile
!.
path
)
:
null
,
checkOutTime:
selectedType
==
"Check Out"
?
TimeOfDay
.
now
().
format
(
context
)
:
null
,
checkOutLoc:
selectedType
==
"Check Out"
?
locationController
.
text
:
null
,
checkOutProof:
selectedType
==
"Check Out"
?
File
(
proofFile
!.
path
)
:
null
,
note:
descriptionController
.
text
,
);
setState
(()
{
isSubmitting
=
false
;
selectedType
=
null
;
locationController
.
clear
();
descriptionController
.
clear
();
proofFile
=
null
;
});
_showSnack
(
context
,
"Attendance Submitted Successfully!"
);
_autoFetchLocation
();
}
void
_showSnack
(
BuildContext
context
,
String
msg
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
msg
),
backgroundColor:
Colors
.
black87
),
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
backgroundColor:
Colors
.
white
,
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
Colors
.
white
,
elevation:
0
,
title:
Row
(
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
),
const
SizedBox
(
width:
10
),
Text
(
"Add Live Attendance"
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
],
),
),
body:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
18
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
/// Type Dropdown
const
Text
(
"Type"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Type"
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w400
),
),
value:
selectedType
,
items:
types
.
map
((
e
)
=>
DropdownMenuItem
<
String
>(
value:
e
,
child:
Text
(
e
,
style:
const
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaMedium"
),
overflow:
TextOverflow
.
ellipsis
,
),
))
.
toList
(),
onChanged:
(
val
)
=>
setState
(()
=>
selectedType
=
val
),
iconStyleData:
ddtheme
.
iconStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
),
if
(
typeError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
typeError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
const
SizedBox
(
height:
16
),
/// Location
Text
(
locationHeading
,
style:
const
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
TextField
(
controller:
locationController
,
decoration:
_inputDecoration
(
"Enter location"
),
),
if
(
locationError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
locationError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
const
SizedBox
(
height:
16
),
/// Description
Text
(
descriptionHeading
,
style:
const
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
TextField
(
controller:
descriptionController
,
maxLines:
3
,
decoration:
_inputDecoration
(
"Write Description"
),
),
const
SizedBox
(
height:
20
),
/// Attach Proof
InkResponse
(
onTap:
()
=>
_showPicker
(
context
),
child:
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
decoration:
BoxDecoration
(
color:
Colors
.
blue
.
shade50
,
border:
Border
.
all
(
color:
Colors
.
blue
.
shade200
),
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Center
(
child:
Text
(
proofButtonText
,
style:
const
TextStyle
(
fontSize:
16
,
color:
Colors
.
blue
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
,
),
),
),
),
),
if
(
proofError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
proofError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
if
(
proofFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Row
(
children:
[
const
Icon
(
Icons
.
check_circle
,
color:
Colors
.
green
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
"Attached:
${proofFile!.name}
"
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontFamily:
"JakartaMedium"
,
fontSize:
14
))),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
),
onPressed:
()
=>
setState
(()
=>
proofFile
=
null
),
),
],
)
],
const
SizedBox
(
height:
24
),
/// Submit Button
InkResponse
(
onTap:
isSubmitEnabled
&&
!
isSubmitting
?
()
=>
submitAttendance
(
context
)
:
null
,
child:
Container
(
height:
48
,
alignment:
Alignment
.
center
,
decoration:
BoxDecoration
(
color:
isSubmitEnabled
?
AppColors
.
app_blue
:
Colors
.
grey
.
shade400
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
isSubmitting
?
const
CircularProgressIndicator
(
color:
Colors
.
white
,
strokeWidth:
2
)
:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
16
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
),
),
),
),
],
),
),
);
}
InputDecoration
_inputDecoration
(
String
hint
)
{
return
InputDecoration
(
hintText:
hint
,
hintStyle:
const
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w400
,
color:
Colors
.
grey
,
),
filled:
true
,
fillColor:
Colors
.
grey
.
shade100
,
enabledBorder:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
borderSide:
BorderSide
.
none
,
),
focusedBorder:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
borderSide:
BorderSide
(
color:
AppColors
.
app_blue
),
),
);
}
}
lib/screens/hrm/AddManualAttendance.dart
0 → 100644
View file @
332a8e91
import
'dart:io'
;
import
'package:connectivity_plus/connectivity_plus.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'package:geolocator/geolocator.dart'
;
import
'package:geocoding/geocoding.dart'
;
import
'../../Models/ordersModels/commonResponse.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonServices.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
class
AddManualAttendanceScreen
extends
StatefulWidget
{
const
AddManualAttendanceScreen
({
super
.
key
});
@override
State
<
AddManualAttendanceScreen
>
createState
()
=>
_AddManualAttendanceScreenState
();
}
class
_AddManualAttendanceScreenState
extends
State
<
AddManualAttendanceScreen
>
{
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
ImagePicker
picker
=
ImagePicker
();
// Connectivity
Map
_source
=
{
ConnectivityResult
.
mobile
:
true
};
final
MyConnectivity
_connectivity
=
MyConnectivity
.
instance
;
String
connection
=
"Online"
;
// Controllers
final
checkInTime
=
TextEditingController
();
final
checkInLocation
=
TextEditingController
();
final
checkInDescription
=
TextEditingController
();
XFile
?
checkInProof
;
final
checkOutTime
=
TextEditingController
();
final
checkOutLocation
=
TextEditingController
();
final
checkOutDescription
=
TextEditingController
();
XFile
?
checkOutProof
;
String
?
selectedType
;
final
List
<
String
>
types
=
[
"Check In"
,
"Check Out"
,
"Check In/Out"
];
// Errors
String
?
dateError
,
typeError
;
String
?
checkInTimeError
,
checkInLocError
,
checkInDescError
,
checkInProofError
;
String
?
checkOutTimeError
,
checkOutLocError
,
checkOutDescError
,
checkOutProofError
;
// In your Attendancelistprovider class
CommonResponse
?
get
addResponse
=>
addResponse
;
String
?
get
errorMessage
=>
errorMessage
;
@override
void
initState
()
{
super
.
initState
();
_connectivity
.
initialise
();
_connectivity
.
myStream
.
listen
((
src
)
{
setState
(()
=>
_source
=
src
);
});
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
_fetchInitialLocation
();
});
}
@override
void
dispose
()
{
_connectivity
.
disposeStream
();
super
.
dispose
();
}
Future
<
void
>
_fetchInitialLocation
()
async
{
String
loc
=
await
getCurrentLocation
();
setState
(()
{
checkInLocation
.
text
=
loc
;
checkOutLocation
.
text
=
loc
;
});
}
Future
<
String
>
getCurrentLocation
()
async
{
try
{
LocationPermission
permission
=
await
Geolocator
.
checkPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
permission
=
await
Geolocator
.
requestPermission
();
if
(
permission
==
LocationPermission
.
denied
)
return
"Permission denied"
;
}
if
(
permission
==
LocationPermission
.
deniedForever
)
{
return
"Permission permanently denied"
;
}
Position
pos
=
await
Geolocator
.
getCurrentPosition
(
desiredAccuracy:
LocationAccuracy
.
high
);
List
<
Placemark
>
placemarks
=
await
placemarkFromCoordinates
(
pos
.
latitude
,
pos
.
longitude
);
if
(
placemarks
.
isNotEmpty
)
{
Placemark
p
=
placemarks
.
first
;
return
"
${p.locality}
,
${p.administrativeArea}
,
${p.country}
"
;
}
return
"
${pos.latitude}
,
${pos.longitude}
"
;
}
catch
(
e
)
{
return
"Error:
$e
"
;
}
}
Future
<
void
>
_pickTime
(
TextEditingController
controller
)
async
{
final
TimeOfDay
?
picked
=
await
showTimePicker
(
context:
context
,
initialTime:
TimeOfDay
.
now
());
if
(
picked
!=
null
)
controller
.
text
=
picked
.
format
(
context
);
}
Future
<
void
>
_pickFile
(
bool
isCheckIn
)
async
{
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
context:
context
,
builder:
(
_
)
{
return
SafeArea
(
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
ListTile
(
leading:
const
Icon
(
Icons
.
camera_alt
),
title:
const
Text
(
"Capture photo from camera"
),
onTap:
()
async
{
final
XFile
?
file
=
await
picker
.
pickImage
(
source
:
ImageSource
.
camera
);
if
(
file
!=
null
)
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
file
;
else
checkOutProof
=
file
;
});
}
Navigator
.
pop
(
context
);
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
"Select photo from gallery"
),
onTap:
()
async
{
final
XFile
?
file
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
if
(
file
!=
null
)
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
file
;
else
checkOutProof
=
file
;
});
}
Navigator
.
pop
(
context
);
},
),
],
),
);
},
);
}
void
_submitForm
(
BuildContext
context
)
async
{
// reset errors first
dateError
=
null
;
typeError
=
null
;
checkInTimeError
=
checkInLocError
=
checkInDescError
=
checkInProofError
=
null
;
checkOutTimeError
=
checkOutLocError
=
checkOutDescError
=
checkOutProofError
=
null
;
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
// --- Date Validation ---
if
(
provider
.
dateController
.
text
.
isEmpty
)
{
dateError
=
"Please select a date"
;
}
else
{
try
{
final
enteredDate
=
DateFormat
(
"dd MMM yyyy"
).
parse
(
provider
.
dateController
.
text
);
provider
.
setSelectedDate
(
enteredDate
);
if
(!
provider
.
isDateValid
())
{
dateError
=
"Date must be today or yesterday"
;
}
}
catch
(
e
)
{
dateError
=
"Invalid date format (use dd MMM yyyy)"
;
}
}
// --- Type Validation ---
if
(
selectedType
==
null
)
{
typeError
=
"Please select type"
;
}
// --- Check In Validations ---
if
(
selectedType
==
"Check In"
||
selectedType
==
"Check In/Out"
)
{
if
(
checkInTime
.
text
.
isEmpty
)
checkInTimeError
=
"Please select check-in time"
;
if
(
checkInLocation
.
text
.
isEmpty
)
checkInLocError
=
"Please enter check-in location"
;
if
(
checkInDescription
.
text
.
isEmpty
)
checkInDescError
=
"Please enter description"
;
if
(
checkInProof
==
null
)
checkInProofError
=
"Please attach check-in proof"
;
}
// --- Check Out Validations ---
if
(
selectedType
==
"Check Out"
||
selectedType
==
"Check In/Out"
)
{
if
(
checkOutTime
.
text
.
isEmpty
)
checkOutTimeError
=
"Please select check-out time"
;
if
(
checkOutLocation
.
text
.
isEmpty
)
checkOutLocError
=
"Please enter check-out location"
;
if
(
checkOutDescription
.
text
.
isEmpty
)
checkOutDescError
=
"Please enter description"
;
if
(
checkOutProof
==
null
)
checkOutProofError
=
"Please attach check-out proof"
;
}
// --- Stop if any error ---
if
([
dateError
,
typeError
,
checkInTimeError
,
checkInLocError
,
checkInDescError
,
checkInProofError
,
checkOutTimeError
,
checkOutLocError
,
checkOutDescError
,
checkOutProofError
].
any
((
e
)
=>
e
!=
null
))
{
setState
(()
{});
return
;
}
// --- Build data according to type ---
String
?
finalCheckInTime
;
String
?
finalCheckInLoc
;
File
?
finalCheckInProof
;
String
?
finalCheckOutTime
;
String
?
finalCheckOutLoc
;
File
?
finalCheckOutProof
;
String
?
finalNote
;
if
(
selectedType
==
"Check In"
)
{
finalCheckInTime
=
checkInTime
.
text
;
finalCheckInLoc
=
checkInLocation
.
text
;
finalCheckInProof
=
File
(
checkInProof
!.
path
);
finalNote
=
checkInDescription
.
text
;
}
else
if
(
selectedType
==
"Check Out"
)
{
finalCheckOutTime
=
checkOutTime
.
text
;
finalCheckOutLoc
=
checkOutLocation
.
text
;
finalCheckOutProof
=
File
(
checkOutProof
!.
path
);
finalNote
=
checkOutDescription
.
text
;
}
else
if
(
selectedType
==
"Check In/Out"
)
{
finalCheckInTime
=
checkInTime
.
text
;
finalCheckInLoc
=
checkInLocation
.
text
;
finalCheckInProof
=
File
(
checkInProof
!.
path
);
finalCheckOutTime
=
checkOutTime
.
text
;
finalCheckOutLoc
=
checkOutLocation
.
text
;
finalCheckOutProof
=
File
(
checkOutProof
!.
path
);
finalNote
=
"
${checkInDescription.text}
/
${checkOutDescription.text}
"
;
}
// --- Submit to provider ---
await
provider
.
addAttendanceRequest
(
context
,
process:
"Manual"
,
type:
selectedType
!,
loc:
selectedType
==
"Check In"
?
checkInLocation
.
text
:
selectedType
==
"Check Out"
?
checkOutLocation
.
text
:
"
${checkInLocation.text}
,
${checkOutLocation.text}
"
,
checkDate:
provider
.
dateController
.
text
,
checkInTime:
finalCheckInTime
,
checkInLoc:
finalCheckInLoc
,
checkInProof:
finalCheckInProof
,
checkOutTime:
finalCheckOutTime
,
checkOutLoc:
finalCheckOutLoc
,
checkOutProof:
finalCheckOutProof
,
note:
finalNote
,
);
// Check the response from provider
if
(
provider
.
addResponse
!=
null
&&
provider
.
addResponse
!.
error
==
"0"
)
{
// Success case
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
provider
.
addResponse
!.
message
??
"Attendance Submitted Successfully"
)),
);
// --- Reset fields ---
setState
(()
{
selectedType
=
null
;
provider
.
dateController
.
clear
();
checkInTime
.
clear
();
checkInLocation
.
clear
();
checkInDescription
.
clear
();
checkInProof
=
null
;
checkOutTime
.
clear
();
checkOutLocation
.
clear
();
checkOutDescription
.
clear
();
checkOutProof
=
null
;
});
_fetchInitialLocation
();
}
else
{
// Error case - show appropriate message
String
errorMessage
=
provider
.
errorMessage
??
"Failed to submit attendance"
;
// Handle specific server error for Check Out without Check In
if
(
errorMessage
.
contains
(
"Check In is not Available"
))
{
errorMessage
=
"Cannot submit Check Out without a Check In record for this date"
;
}
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
errorMessage
),
backgroundColor:
Colors
.
red
),
);
}
}
Future
<
bool
>
_onBackPressed
(
BuildContext
context
)
async
{
Navigator
.
pop
(
context
,
true
);
return
true
;
}
@override
Widget
build
(
BuildContext
context
)
{
switch
(
_source
.
keys
.
toList
()[
0
])
{
case
ConnectivityResult
.
mobile
:
case
ConnectivityResult
.
wifi
:
connection
=
'Online'
;
break
;
default
:
connection
=
'Offline'
;
}
return
(
connection
==
"Online"
)
?
Platform
.
isAndroid
?
WillPopScope
(
onWillPop:
()
=>
_onBackPressed
(
context
),
child:
SafeArea
(
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
),
),
)
:
_scaffold
(
context
)
:
NoNetwork
(
context
);
}
Widget
_scaffold
(
BuildContext
context
)
{
return
Consumer
<
Attendancelistprovider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
backgroundColor:
Colors
.
white
,
appBar:
appbar2New
(
context
,
"Add Manual Attendance"
,
()
{},
SizedBox
.
shrink
(),
0xFFFFFFFF
,
),
body:
Scrollbar
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
12
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// TextWidget(context, "Date"),
GestureDetector
(
onTap:
()
=>
provider
.
showDatePickerDialog
(
context
),
child:
AbsorbPointer
(
child:
textControllerWidget
(
context
,
provider
.
dateController
,
"Date"
,
"Select Date"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
),
),
errorWidget
(
context
,
dateError
),
TextWidget
(
context
,
"Type"
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Type"
),
items:
types
.
map
((
e
)
=>
DropdownMenuItem
(
value:
e
,
child:
Text
(
e
),
))
.
toList
(),
value:
selectedType
,
onChanged:
(
val
)
{
setState
(()
=>
selectedType
=
val
);
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
errorWidget
(
context
,
typeError
),
if
(
selectedType
==
"Check In"
||
selectedType
==
"Check In/Out"
)
_buildSection
(
"Check In"
),
if
(
selectedType
==
"Check Out"
||
selectedType
==
"Check In/Out"
)
_buildSection
(
"Check Out"
),
SizedBox
(
height:
80
),
],
),
),
),
bottomNavigationBar:
InkResponse
(
onTap:
provider
.
isSubmitting
?
null
:
()
=>
_submitForm
(
context
),
child:
Container
(
height:
45
,
alignment:
Alignment
.
center
,
margin:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
provider
.
isSubmitting
?
CircularProgressIndicator
.
adaptive
(
valueColor:
AlwaysStoppedAnimation
(
AppColors
.
white
),
)
:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
),
),
),
),
);
},
);
}
Widget
_buildSection
(
String
title
)
{
final
isCheckIn
=
title
==
"Check In"
;
final
timeCtrl
=
isCheckIn
?
checkInTime
:
checkOutTime
;
final
locCtrl
=
isCheckIn
?
checkInLocation
:
checkOutLocation
;
final
descCtrl
=
isCheckIn
?
checkInDescription
:
checkOutDescription
;
final
proofFile
=
isCheckIn
?
checkInProof
:
checkOutProof
;
final
proofError
=
isCheckIn
?
checkInProofError
:
checkOutProofError
;
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// TextWidget(context, "$title Time"),
GestureDetector
(
onTap:
()
=>
_pickTime
(
timeCtrl
),
// ⏰ open time picker
child:
AbsorbPointer
(
child:
textControllerWidget
(
context
,
timeCtrl
,
"
$title
Time"
,
"Select Time"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
),
),
errorWidget
(
context
,
isCheckIn
?
checkInTimeError
:
checkOutTimeError
),
textControllerWidget
(
context
,
locCtrl
,
"
$title
Location"
,
"Enter Location"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorWidget
(
context
,
isCheckIn
?
checkInLocError
:
checkOutLocError
),
textControllerWidget
(
context
,
descCtrl
,
"
$title
Description"
,
"Enter Description"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
done
,
),
errorWidget
(
context
,
isCheckIn
?
checkInDescError
:
checkOutDescError
),
InkResponse
(
onTap:
()
=>
_pickFile
(
isCheckIn
),
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
vertical:
10
),
height:
45
,
width:
double
.
infinity
,
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
"Attach
$title
Proof"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
if
(
proofFile
!=
null
)
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
child:
Text
(
proofFile
.
name
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
12
),
),
),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
,
size:
18
),
onPressed:
()
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
null
;
else
checkOutProof
=
null
;
});
},
)
],
),
errorWidget
(
context
,
proofError
),
],
);
}
}
lib/screens/hrm/AddTourExpBillScreen.dart
0 → 100644
View file @
332a8e91
import
'dart:io'
;
import
'package:connectivity_plus/connectivity_plus.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'package:file_picker/file_picker.dart'
;
import
'../../Notifiers/hrmProvider/tourExpensesProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonServices.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
class
AddBillScreen
extends
StatefulWidget
{
final
String
pageTitleName
;
const
AddBillScreen
({
super
.
key
,
required
this
.
pageTitleName
});
@override
State
<
AddBillScreen
>
createState
()
=>
_AddBillScreenState
();
}
class
_AddBillScreenState
extends
State
<
AddBillScreen
>
{
final
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
List
<
FocusNode
>
focusNodes
=
List
.
generate
(
8
,
(
index
)
=>
FocusNode
());
Map
_source
=
{
ConnectivityResult
.
mobile
:
true
};
final
MyConnectivity
_connectivity
=
MyConnectivity
.
instance
;
final
TextEditingController
placeController
=
TextEditingController
();
final
TextEditingController
noteController
=
TextEditingController
();
// Validation errors
String
?
placeError
;
String
?
daAmountError
;
String
?
tourTypeError
;
String
?
tourDateError
;
String
?
noteError
;
List
<
Map
<
String
,
String
>>
travelExpenses
=
[];
List
<
Map
<
String
,
String
>>
hotelExpenses
=
[];
List
<
Map
<
String
,
String
>>
otherExpenses
=
[];
List
<
File
>
travelImages
=
[];
List
<
File
>
hotelImages
=
[];
List
<
File
>
otherImages
=
[];
String
?
selectedDAAmount
;
String
?
selectedTourType
;
String
?
selectedTravelType
;
@override
void
initState
()
{
super
.
initState
();
_connectivity
.
initialise
();
_connectivity
.
myStream
.
listen
((
source
)
{
setState
(()
=>
_source
=
source
);
});
// Add listeners to clear errors when user starts typing
placeController
.
addListener
(()
{
if
(
placeError
!=
null
&&
placeController
.
text
.
isNotEmpty
)
{
setState
(()
=>
placeError
=
null
);
}
});
noteController
.
addListener
(()
{
if
(
noteError
!=
null
&&
noteController
.
text
.
isNotEmpty
)
{
setState
(()
=>
noteError
=
null
);
}
});
Future
.
microtask
(()
{
final
provider
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
);
provider
.
fetchTourExpensesAddView
(
context
,
"0"
);
// fresh bill
});
}
@override
void
dispose
()
{
placeController
.
dispose
();
noteController
.
dispose
();
for
(
var
node
in
focusNodes
)
{
node
.
dispose
();
}
_connectivity
.
disposeStream
();
super
.
dispose
();
}
Future
<
bool
>
_onBackPressed
(
BuildContext
context
)
async
{
Navigator
.
pop
(
context
,
true
);
return
true
;
}
// Function to validate all fields
bool
validateFields
()
{
String
?
newPlaceError
=
placeController
.
text
.
isEmpty
?
"Place of visit is required"
:
null
;
String
?
newDaAmountError
=
selectedDAAmount
==
null
?
"DA Amount is required"
:
null
;
String
?
newTourTypeError
=
selectedTourType
==
null
?
"Tour Type is required"
:
null
;
String
?
newTourDateError
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
).
dateController
.
text
.
isEmpty
?
"Tour Date is required"
:
null
;
String
?
newNoteError
=
noteController
.
text
.
isEmpty
?
"Note is required"
:
null
;
// Only update if there are actual changes to avoid unnecessary rebuilds
if
(
placeError
!=
newPlaceError
||
daAmountError
!=
newDaAmountError
||
tourTypeError
!=
newTourTypeError
||
tourDateError
!=
newTourDateError
||
noteError
!=
newNoteError
)
{
setState
(()
{
placeError
=
newPlaceError
;
daAmountError
=
newDaAmountError
;
tourTypeError
=
newTourTypeError
;
tourDateError
=
newTourDateError
;
noteError
=
newNoteError
;
});
}
return
newPlaceError
==
null
&&
newDaAmountError
==
null
&&
newTourTypeError
==
null
&&
newTourDateError
==
null
&&
newNoteError
==
null
;
}
// Format date to "02 Sep 2025" format
String
_formatDate
(
DateTime
date
)
{
const
months
=
[
'Jan'
,
'Feb'
,
'Mar'
,
'Apr'
,
'May'
,
'Jun'
,
'Jul'
,
'Aug'
,
'Sep'
,
'Oct'
,
'Nov'
,
'Dec'
];
return
'
${date.day.toString().padLeft(2, '0')}
${months[date.month - 1]}
${date.year}
'
;
}
@override
Widget
build
(
BuildContext
context
)
{
switch
(
_source
.
keys
.
toList
()[
0
])
{
case
ConnectivityResult
.
mobile
:
case
ConnectivityResult
.
wifi
:
connection
=
'Online'
;
break
;
case
ConnectivityResult
.
none
:
default
:
connection
=
'Offline'
;
}
return
(
connection
==
"Online"
)
?
Platform
.
isAndroid
?
WillPopScope
(
onWillPop:
()
=>
_onBackPressed
(
context
),
child:
SafeArea
(
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
)),
)
:
_scaffold
(
context
)
:
NoNetwork
(
context
);
}
Future
<
File
?>
pickImage
(
BuildContext
context
)
async
{
final
ImagePicker
picker
=
ImagePicker
();
return
showModalBottomSheet
<
File
?>(
context:
context
,
builder:
(
ctx
)
{
return
SafeArea
(
child:
Wrap
(
children:
[
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
'Pick from Gallery'
),
onTap:
()
async
{
final
picked
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
Navigator
.
pop
(
ctx
,
picked
!=
null
?
File
(
picked
.
path
)
:
null
);
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
camera_alt
),
title:
const
Text
(
'Take a Photo'
),
onTap:
()
async
{
final
picked
=
await
picker
.
pickImage
(
source
:
ImageSource
.
camera
);
Navigator
.
pop
(
ctx
,
picked
!=
null
?
File
(
picked
.
path
)
:
null
);
},
),
],
),
);
},
);
}
Widget
_scaffold
(
BuildContext
context
)
{
return
Consumer
<
TourExpensesProvider
>(
builder:
(
context
,
provider
,
_
)
{
if
(
provider
.
isLoading
)
{
return
Scaffold
(
body:
Container
(
child:
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
)),),
);
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Scaffold
(
body:
Center
(
child:
Text
(
provider
.
errorMessage
!)));
}
return
Scaffold
(
resizeToAvoidBottomInset:
true
,
backgroundColor:
AppColors
.
scaffold_bg_color
,
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
title:
Text
(
widget
.
pageTitleName
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
leading:
IconButton
(
icon:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
),
body:
Scrollbar
(
thumbVisibility:
false
,
child:
SingleChildScrollView
(
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
10
),
decoration:
BoxDecoration
(
color:
AppColors
.
white
,
borderRadius:
BorderRadius
.
circular
(
20
),
),
margin:
EdgeInsets
.
only
(
top:
10
,
left:
10
,
right:
10
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
textControllerWidget
(
context
,
placeController
,
"Place of Visit"
,
"Enter Place"
,
(
value
)
{
// Clear error when user types
if
(
placeError
!=
null
&&
value
.
isNotEmpty
)
{
setState
(()
=>
placeError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
focusNodes
[
0
],
focusNodes
[
1
],
TextInputAction
.
next
,
),
errorWidget
(
context
,
placeError
),
TextWidget
(
context
,
"DA Amount"
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
'Select DA Amount'
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
items:
provider
.
daAmountList
.
map
((
item
)
=>
DropdownMenuItem
<
String
>(
value:
item
,
child:
Text
(
item
,
style:
const
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
))
.
toList
(),
value:
selectedDAAmount
,
onChanged:
(
String
?
value
)
{
setState
(()
{
selectedDAAmount
=
value
;
if
(
daAmountError
!=
null
)
daAmountError
=
null
;
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
errorWidget
(
context
,
daAmountError
),
TextWidget
(
context
,
"Tour Type"
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
'Select Tour Type'
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
items:
provider
.
tourTypeList
.
map
((
item
)
=>
DropdownMenuItem
<
String
>(
value:
item
,
child:
Text
(
item
,
style:
const
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
))
.
toList
(),
value:
selectedTourType
,
onChanged:
(
String
?
value
)
{
setState
(()
{
selectedTourType
=
value
;
if
(
tourTypeError
!=
null
)
tourTypeError
=
null
;
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
errorWidget
(
context
,
tourTypeError
),
TextWidget
(
context
,
"Tour Date"
),
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
true
);
if
(
d
!=
null
)
{
provider
.
dateController
.
text
=
_formatDate
(
d
);
if
(
tourDateError
!=
null
)
{
setState
(()
=>
tourDateError
=
null
);
}
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
TextFormField
(
controller:
provider
.
dateController
,
enabled:
false
,
decoration:
InputDecoration
(
hintText:
"Select Tour Date"
,
hintStyle:
TextStyle
(
fontWeight:
FontWeight
.
w400
,
color:
Color
(
0xFFB4BEC0
),
fontSize:
14
,
),
border:
InputBorder
.
none
,
contentPadding:
EdgeInsets
.
symmetric
(
horizontal:
15
),
),
),
),
),
errorWidget
(
context
,
tourDateError
),
textControllerWidget
(
context
,
noteController
,
"Note"
,
"Enter Note"
,
(
value
)
{
// Clear error when user types
if
(
noteError
!=
null
&&
value
.
isNotEmpty
)
{
setState
(()
=>
noteError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
focusNodes
[
2
],
focusNodes
[
3
],
TextInputAction
.
next
,
300
,
// Allow up to 300 characters
),
errorWidget
(
context
,
noteError
),
const
SizedBox
(
height:
16
),
/// Travel Expenses Section
sectionHeader
(
"Travel Expenses"
,
onAddTap:
()
{
showAddTravelExpenseSheet
(
context
,
travelExpenses
,
()
=>
setState
(()
{}),
provider
.
travelTypeList
,
travelImages
,
);
}),
if
(
travelExpenses
.
isNotEmpty
)
travelExpenseList
(
travelExpenses
),
/// Hotel Expenses Section
sectionHeader
(
"Hotel Expenses"
,
onAddTap:
()
{
showAddHotelExpenseSheet
(
context
,
hotelExpenses
,
()
=>
setState
(()
{}),
provider
,
hotelImages
,
);
}),
if
(
hotelExpenses
.
isNotEmpty
)
hotelExpenseList
(
hotelExpenses
),
/// Other Expenses Section
sectionHeader
(
"Other Expenses"
,
onAddTap:
()
{
showAddOtherExpenseSheet
(
context
,
otherExpenses
,
()
=>
setState
(()
{}),
provider
,
otherImages
,
);
}),
if
(
otherExpenses
.
isNotEmpty
)
otherExpenseList
(
otherExpenses
),
const
SizedBox
(
height:
80
),
],
),
),
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation
.
centerFloat
,
bottomNavigationBar:
InkResponse
(
onTap:
()
async
{
// Validate all fields first
if
(!
validateFields
())
{
return
;
}
final
provider
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
);
provider
.
dateController
.
clear
();
tourDateError
=
null
;
final
success
=
await
provider
.
addTourBill
(
context:
context
,
placeOfVisit:
placeController
.
text
,
daAmount:
selectedDAAmount
??
""
,
tourType:
selectedTourType
??
""
,
tourDate:
provider
.
dateController
.
text
,
travelExpenses:
travelExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
hotelExpenses:
hotelExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
otherExpenses:
otherExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
travelImages:
travelImages
,
hotelImages:
hotelImages
,
otherImages:
otherImages
,
);
print
(
"image==================
$travelImages
"
);
if
(
success
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Tour Bill Submitted Successfully"
)),
);
Navigator
.
pop
(
context
,
true
);
}
else
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
provider
.
errorMessage
??
"Failed to submit bill"
)),
);
}
},
child:
Container
(
height:
45
,
alignment:
Alignment
.
center
,
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
15
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
5
),
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
),
),
),
),
);
},
);
}
Widget
sectionHeader
(
String
title
,
{
VoidCallback
?
onAddTap
})
{
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
title
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
fontFamily:
"JakartaMedium"
,
)),
const
SizedBox
(
height:
6
),
Container
(
height:
45
,
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
.
shade400
,
width:
0.7
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
InkWell
(
onTap:
onAddTap
,
child:
const
Center
(
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Icon
(
Icons
.
add
,
color:
Colors
.
blue
),
SizedBox
(
width:
6
),
Text
(
"Add Expenses"
,
style:
TextStyle
(
color:
Colors
.
blue
,
fontSize:
14
,
fontFamily:
"JakartaMedium"
,
)),
],
),
),
),
),
const
SizedBox
(
height:
10
),
],
);
}
Widget
travelExpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
itemCount:
items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
200
,
margin:
const
EdgeInsets
.
only
(
right:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/travel_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
exp
[
"travel_type"
]
??
"Travel"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
if
(
exp
[
"from"
]
!=
null
&&
exp
[
"to"
]
!=
null
)
...[
const
SizedBox
(
height:
2
),
Text
(
"
${exp["from"]}
→
${exp["to"]}
"
,
style:
TextStyle
(
fontSize:
12
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
],
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
},
),
);
}
Widget
hotelExpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
itemCount:
items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
160
,
margin:
const
EdgeInsets
.
only
(
right:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/hotel_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
exp
[
"hotel_name"
]
??
"-"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
},
),
);
}
Widget
otherExpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
itemCount:
items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
160
,
margin:
const
EdgeInsets
.
only
(
right:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/books_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
exp
[
"description"
]
??
"-"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
2
),
Text
(
exp
[
"date"
]
!=
null
?
exp
[
"date"
]!.
split
(
"T"
).
first
:
"-"
,
style:
TextStyle
(
fontSize:
12
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
},
),
);
}
Future
<
File
?>
pickFile
()
async
{
FilePickerResult
?
result
=
await
FilePicker
.
platform
.
pickFiles
(
type:
FileType
.
any
);
if
(
result
!=
null
&&
result
.
files
.
isNotEmpty
)
{
return
File
(
result
.
files
.
single
.
path
!);
}
return
null
;
}
// --- Travel Expense BottomSheet ---
Future
<
void
>
showAddTravelExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
travelExpenses
,
VoidCallback
onUpdated
,
List
<
String
>
travelTypes
,
List
<
File
>
travelImages
,
)
{
final
fromController
=
TextEditingController
();
final
toController
=
TextEditingController
();
final
fareController
=
TextEditingController
();
String
?
selectedTravelType
;
File
?
billFile
;
String
?
fromError
,
toError
,
typeError
,
fareError
,
billError
;
// Listeners to clear errors when user starts typing
fromController
.
addListener
(()
{
if
(
fromError
!=
null
&&
fromController
.
text
.
isNotEmpty
)
{
fromError
=
null
;
}
});
toController
.
addListener
(()
{
if
(
toError
!=
null
&&
toController
.
text
.
isNotEmpty
)
{
toError
=
null
;
}
});
fareController
.
addListener
(()
{
if
(
fareError
!=
null
&&
fareController
.
text
.
isNotEmpty
)
{
fareError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
// Function to update state and clear errors when fields change
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors if needed
bool
validateFields
()
{
String
?
newFromError
=
fromController
.
text
.
isEmpty
?
"From is required"
:
null
;
String
?
newToError
=
toController
.
text
.
isEmpty
?
"To is required"
:
null
;
String
?
newTypeError
=
selectedTravelType
==
null
?
"Please select type"
:
null
;
String
?
newFareError
=
fareController
.
text
.
isEmpty
?
"Fare is required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
// Only update if there are actual changes to avoid unnecessary rebuilds
if
(
fromError
!=
newFromError
||
toError
!=
newToError
||
typeError
!=
newTypeError
||
fareError
!=
newFareError
||
billError
!=
newBillError
)
{
updateState
(()
{
fromError
=
newFromError
;
toError
=
newToError
;
typeError
=
newTypeError
;
fareError
=
newFareError
;
billError
=
newBillError
;
});
}
return
newFromError
==
null
&&
newToError
==
null
&&
newTypeError
==
null
&&
newFareError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
)
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Travel Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
textControllerWidget
(
context
,
fromController
,
"From"
,
"Enter Starting Location"
,
(
value
)
{
// Clear error when user types
if
(
fromError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
fromError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
fromError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
toController
,
"To"
,
"Enter Destination Location"
,
(
value
)
{
// Clear error when user types
if
(
toError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
toError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
toError
),
const
SizedBox
(
height:
12
),
TextWidget
(
context
,
"Travel Type"
),
DropdownButtonHideUnderline
(
child:
Container
(
width:
double
.
infinity
,
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
"Select Travel Type"
,
style:
TextStyle
(
fontSize:
14
,
color:
Color
(
0xFFB4BEC0
),
)
),
items:
travelTypes
.
map
((
t
)
=>
DropdownMenuItem
(
value:
t
,
child:
Text
(
t
,
style:
TextStyle
(
fontSize:
14
),
)
)
).
toList
(),
value:
selectedTravelType
,
onChanged:
(
val
)
{
updateState
(()
{
selectedTravelType
=
val
;
if
(
typeError
!=
null
)
typeError
=
null
;
});
},
buttonStyleData:
ButtonStyleData
(
height:
50
,
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
only
(
left:
14
,
right:
14
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
14
),
color:
AppColors
.
text_field_color
,
),
),
dropdownStyleData:
DropdownStyleData
(
maxHeight:
200
,
width:
MediaQuery
.
of
(
context
).
size
.
width
-
60
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
14
),
color:
Colors
.
white
,
),
offset:
const
Offset
(
0
,
-
5
),
scrollbarTheme:
ScrollbarThemeData
(
radius:
const
Radius
.
circular
(
40
),
thickness:
MaterialStateProperty
.
all
<
double
>(
6
),
thumbVisibility:
MaterialStateProperty
.
all
<
bool
>(
true
),
),
),
),
),
),
errorText
(
typeError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
fareController
,
"Fare Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error when user types
if
(
fareError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
fareError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
fareError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
4.0
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
flex:
5
,
child:
Text
(
"
${billFile!.path.split('/').last}
"
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
color:
AppColors
.
semi_black
,
fontSize:
11
,
fontWeight:
FontWeight
.
w600
,
),
),
),
Expanded
(
flex:
1
,
child:
InkResponse
(
onTap:
()
=>
updateState
(()
=>
billFile
=
null
),
child:
SvgPicture
.
asset
(
"assets/svg/ic_close.svg"
,
width:
15
,
height:
15
,
),
),
),
],
),
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
travelExpenses
.
add
({
"from"
:
fromController
.
text
,
"to"
:
toController
.
text
,
"travel_type"
:
selectedTravelType
!,
"amount"
:
fareController
.
text
,
});
travelImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Hotel Expense BottomSheet ---
Future
<
void
>
showAddHotelExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
hotelExpenses
,
VoidCallback
onUpdated
,
TourExpensesProvider
provider
,
List
<
File
>
hotelImages
,
)
{
final
hotelController
=
TextEditingController
();
final
amountController
=
TextEditingController
();
DateTime
?
fromDate
,
toDate
;
File
?
billFile
;
String
?
hotelError
,
fromDateError
,
toDateError
,
amountError
,
billError
;
// Listeners to clear errors when user starts typing
hotelController
.
addListener
(()
{
if
(
hotelError
!=
null
&&
hotelController
.
text
.
isNotEmpty
)
{
hotelError
=
null
;
}
});
amountController
.
addListener
(()
{
if
(
amountError
!=
null
&&
amountController
.
text
.
isNotEmpty
)
{
amountError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
// Function to update state and clear errors
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors
bool
validateFields
()
{
String
?
newHotelError
=
hotelController
.
text
.
isEmpty
?
"Hotel name required"
:
null
;
String
?
newFromDateError
=
fromDate
==
null
?
"From date required"
:
null
;
String
?
newToDateError
=
toDate
==
null
?
"To date required"
:
null
;
String
?
newAmountError
=
amountController
.
text
.
isEmpty
?
"Amount required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
if
(
hotelError
!=
newHotelError
||
fromDateError
!=
newFromDateError
||
toDateError
!=
newToDateError
||
amountError
!=
newAmountError
||
billError
!=
newBillError
)
{
updateState
(()
{
hotelError
=
newHotelError
;
fromDateError
=
newFromDateError
;
toDateError
=
newToDateError
;
amountError
=
newAmountError
;
billError
=
newBillError
;
});
}
return
newHotelError
==
null
&&
newFromDateError
==
null
&&
newToDateError
==
null
&&
newAmountError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
),
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Hotel Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
textControllerWidget
(
context
,
hotelController
,
"Hotel Name"
,
"Enter Hotel Name"
,
(
value
)
{
// Clear error
if
(
hotelError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
hotelError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
hotelError
),
const
SizedBox
(
height:
12
),
Text
(
"Stay Duration"
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
6
),
Row
(
children:
[
Expanded
(
child:
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
true
);
if
(
d
!=
null
)
{
updateState
(()
{
fromDate
=
d
;
if
(
fromDateError
!=
null
)
fromDateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Center
(
child:
Text
(
fromDate
==
null
?
"From Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
fromDate
!),
style:
TextStyle
(
fontSize:
14
,
color:
fromDate
==
null
?
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
false
);
if
(
d
!=
null
)
{
updateState
(()
{
toDate
=
d
;
if
(
toDateError
!=
null
)
toDateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Center
(
child:
Text
(
toDate
==
null
?
"To Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
toDate
!),
style:
TextStyle
(
fontSize:
14
,
color:
toDate
==
null
?
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
),
],
),
if
(
fromDateError
!=
null
)
errorText
(
fromDateError
),
if
(
toDateError
!=
null
)
errorText
(
toDateError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
amountController
,
"Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error
if
(
amountError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
amountError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
amountError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Row
(
children:
[
const
Icon
(
Icons
.
check_circle
,
color:
Colors
.
green
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
"Attached:
${billFile!.path.split('/').last}
"
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontFamily:
"JakartaMedium"
,
fontSize:
14
))),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
),
onPressed:
()
=>
updateState
(()
=>
billFile
=
null
),
),
],
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
hotelExpenses
.
add
({
"hotel_name"
:
hotelController
.
text
,
"from_date"
:
fromDate
!.
toIso8601String
(),
"to_date"
:
toDate
!.
toIso8601String
(),
"amount"
:
amountController
.
text
,
});
hotelImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Other Expense BottomSheet ---
Future
<
void
>
showAddOtherExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
otherExpenses
,
VoidCallback
onUpdated
,
TourExpensesProvider
provider
,
List
<
File
>
otherImages
,
)
{
final
titleController
=
TextEditingController
();
final
amountController
=
TextEditingController
();
File
?
billFile
;
DateTime
?
date
;
String
?
titleError
,
amountError
,
dateError
,
billError
;
// Listeners to clear errors when user starts typing
titleController
.
addListener
(()
{
if
(
titleError
!=
null
&&
titleController
.
text
.
isNotEmpty
)
{
titleError
=
null
;
}
});
amountController
.
addListener
(()
{
if
(
amountError
!=
null
&&
amountController
.
text
.
isNotEmpty
)
{
amountError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors
bool
validateFields
()
{
String
?
newDateError
=
date
==
null
?
"Date required"
:
null
;
String
?
newTitleError
=
titleController
.
text
.
isEmpty
?
"Title required"
:
null
;
String
?
newAmountError
=
amountController
.
text
.
isEmpty
?
"Amount required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
if
(
dateError
!=
newDateError
||
titleError
!=
newTitleError
||
amountError
!=
newAmountError
||
billError
!=
newBillError
)
{
updateState
(()
{
dateError
=
newDateError
;
titleError
=
newTitleError
;
amountError
=
newAmountError
;
billError
=
newBillError
;
});
}
return
newDateError
==
null
&&
newTitleError
==
null
&&
newAmountError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
),
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Other Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
TextWidget
(
context
,
"Date"
),
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
false
);
if
(
d
!=
null
)
{
updateState
(()
{
date
=
d
;
if
(
dateError
!=
null
)
dateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
,
horizontal:
12
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Text
(
date
==
null
?
"Select Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
date
!),
style:
TextStyle
(
fontSize:
14
,
color:
date
==
null
?
const
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
errorText
(
dateError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
titleController
,
"Description"
,
"Enter Title"
,
(
value
)
{
// Clear error
if
(
titleError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
titleError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
titleError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
amountController
,
"Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error
if
(
amountError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
amountError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
amountError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
4.0
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
flex:
5
,
child:
Text
(
"
${billFile!.path.split('/').last}
"
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
color:
AppColors
.
semi_black
,
fontSize:
11
,
fontWeight:
FontWeight
.
w600
,
),
),
),
Expanded
(
flex:
1
,
child:
InkResponse
(
onTap:
()
=>
updateState
(()
=>
billFile
=
null
),
child:
SvgPicture
.
asset
(
"assets/svg/ic_close.svg"
,
width:
15
,
height:
15
,
),
),
),
],
),
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
otherExpenses
.
add
({
"description"
:
titleController
.
text
,
"amount"
:
amountController
.
text
,
"date"
:
date
!.
toIso8601String
(),
});
otherImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
}
\ No newline at end of file
lib/screens/hrm/AttendanceRequestDetail.dart
0 → 100644
View file @
332a8e91
import
'package:dotted_line/dotted_line.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:geocoding/geocoding.dart'
;
import
'package:provider/provider.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'../../Notifiers/hrmProvider/attendanceDetailsProvider.dart'
;
import
'../../Notifiers/HomeScreenNotifier.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../finance/FileViewer.dart'
;
/// screen for attendance details
class
AttendanceRequestDetailScreen
extends
StatefulWidget
{
final
attendanceListId
;
const
AttendanceRequestDetailScreen
({
super
.
key
,
required
this
.
attendanceListId
});
@override
State
<
AttendanceRequestDetailScreen
>
createState
()
=>
_AttendanceRequestDetailScreenState
();
}
class
_AttendanceRequestDetailScreenState
extends
State
<
AttendanceRequestDetailScreen
>
{
late
AttendanceDetailsProvider
provider
;
@override
Widget
build
(
BuildContext
context
)
{
return
ChangeNotifierProvider
(
create:
(
_
)
=>
AttendanceDetailsProvider
()..
fetchAttendanceRequestDetail
(
context
,
widget
.
attendanceListId
),
child:
Consumer
<
AttendanceDetailsProvider
>(
builder:
(
context
,
provider
,
child
)
{
// Get screen dimensions for responsive scaling
final
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
screenHeight
=
MediaQuery
.
of
(
context
).
size
.
height
;
// Scale factors based on screen size
final
scaleFactor
=
screenWidth
/
360
;
// Base width for scaling
final
textScaleFactor
=
MediaQuery
.
of
(
context
).
textScaleFactor
.
clamp
(
1.0
,
1.2
);
return
Scaffold
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
Color
(
0xFFFFFFFF
),
title:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
*
scaleFactor
,
),
),
SizedBox
(
width:
10
*
scaleFactor
),
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
Text
(
"Attendance Details"
,
style:
TextStyle
(
fontSize:
18
,
height:
1.1
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
),
],
),
),
backgroundColor:
Color
(
0xFFF6F6F8
),
body:
Builder
(
builder:
(
context
)
{
if
(
provider
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
,));
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Center
(
child:
Text
(
provider
.
errorMessage
!));
}
if
(
provider
.
response
?.
requestDetails
==
null
)
{
return
const
Center
(
child:
Text
(
"No details found"
));
}
final
details
=
provider
.
response
!.
requestDetails
!;
/// scr
return
SingleChildScrollView
(
padding:
EdgeInsets
.
all
(
16.0
*
scaleFactor
),
child:
Column
(
children:
[
Card
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
16
*
scaleFactor
),
),
elevation:
2
,
child:
Padding
(
padding:
EdgeInsets
.
all
(
16.0
*
scaleFactor
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Container
(
margin:
EdgeInsets
.
only
(
bottom:
0.5
*
scaleFactor
),
padding:
EdgeInsets
.
all
(
12
*
scaleFactor
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
*
scaleFactor
),
),
child:
Row
(
children:
[
/// Left Avatar
Container
(
height:
48
*
scaleFactor
,
width:
48
*
scaleFactor
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
const
Color
(
0xFFEDF8FF
),
// icon bg
),
child:
Center
(
child:
SvgPicture
.
asset
(
height:
28
*
scaleFactor
,
width:
28
*
scaleFactor
,
"assets/svg/hrm/attendanceList.svg"
,
fit:
BoxFit
.
contain
,
),
),
),
SizedBox
(
width:
12
*
scaleFactor
),
/// Middle text
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
details
.
type
??
"-"
,
style:
TextStyle
(
decoration:
TextDecoration
.
underline
,
decorationStyle:
TextDecorationStyle
.
dotted
,
decorationColor:
AppColors
.
grey_thick
,
height:
1.2
,
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
SizedBox
(
height:
2
*
scaleFactor
),
Text
(
details
.
date
??
"-"
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
app_blue
,
),
),
],
),
),
/// Right side (Live/Manual)
Container
(
height:
30
*
scaleFactor
,
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
*
scaleFactor
,
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
6
*
scaleFactor
),
color:
getDecorationColor
(
details
.
status
)
),
child:
Center
(
child:
Text
(
details
.
status
??
"-"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w600
,
color:
getTextColor
(
details
.
status
.
toString
()),
),
),
),
),
],
),
),
// Employee Details
_buildSectionHeader
(
"Employee Details"
,
scaleFactor
),
_buildDetailTile
(
"Employee Name"
,
details
.
employeeName
,
scaleFactor
),
_buildDetailTile
(
"Created Employee"
,
details
.
createdEmpName
,
scaleFactor
),
// Check In/Out
_buildSectionHeader
(
"Check In/Out Details"
,
scaleFactor
),
_buildDate_TimeTile
(
"Check In Date & Time"
,
details
.
date
,
details
.
checkInTime
,
scaleFactor
),
_buildDate_TimeTile
(
"Check Out Date & Time"
,
details
.
date
,
details
.
checkOutTime
,
scaleFactor
),
_buildDetailTile
(
"Original Check In"
,
details
.
checkInTime
,
scaleFactor
),
_buildDetailTile
(
"Original Check Out"
,
"--"
,
scaleFactor
),
_buildDetailTile
(
"Original Check In Location"
,
details
.
checkInLocation
,
scaleFactor
),
_buildDetailTile
(
"Original Check Out Location"
,
details
.
checkOutLocation
,
scaleFactor
),
buildLocationTile
(
"Location"
,
details
.
location
,
scaleFactor
),
// Proofs
if
((
details
.
checkInProofDirFilePath
!=
null
&&
details
.
checkInProofDirFilePath
!.
isNotEmpty
)
||
(
details
.
checkOutProofDirFilePath
!=
null
&&
details
.
checkOutProofDirFilePath
!.
isNotEmpty
))
...[
_buildSectionHeader
(
"Proofs"
,
scaleFactor
),
if
(
details
.
checkInProofDirFilePath
!=
null
&&
details
.
checkInProofDirFilePath
!.
isNotEmpty
)
_buildProofLink
(
context
,
"Check In Proof"
,
details
.
checkInProofDirFilePath
,
scaleFactor
),
if
(
details
.
checkOutProofDirFilePath
!=
null
&&
details
.
checkOutProofDirFilePath
!.
isNotEmpty
)
_buildProofLink
(
context
,
"Check Out Proof"
,
details
.
checkOutProofDirFilePath
,
scaleFactor
),
],
// Remarks & Approvals
_buildSectionHeader
(
"Remarks & Approvals"
,
scaleFactor
),
_buildDetailTile
(
"Level 1 Approved By"
,
details
.
level1EmpName
,
scaleFactor
),
_buildDetailTile
(
"Level 2 Approved By"
,
details
.
level2EmpName
,
scaleFactor
),
_buildDetailTile
(
"Level 1 Remark"
,
details
.
level1Remarks
,
scaleFactor
),
_buildDetailTile
(
"Level 2 Remark"
,
details
.
level2Remarks
,
scaleFactor
),
///remain data
_buildSectionHeader
(
"Other Details"
,
scaleFactor
),
_buildDetailTile
(
"Check In Type"
,
details
.
checkInType
,
scaleFactor
),
_buildDetailTile
(
"Check Out Type"
,
details
.
chechOutType
,
scaleFactor
),
_buildDetailTile
(
"Check Out Time"
,
details
.
checkOutTime
,
scaleFactor
),
// Attendance Info
_buildDetailTile
(
"ID"
,
details
.
id
,
scaleFactor
),
_buildDetailTile
(
"Attendance Type"
,
details
.
attendanceType
,
scaleFactor
),
_buildDetailTile
(
"Note"
,
details
.
note
,
scaleFactor
),
_buildDetailTile
(
"Created Datetime"
,
details
.
requestedDatetime
,
scaleFactor
),
],
),
),
),
SizedBox
(
height:
30
*
scaleFactor
),
],
),
);
},
),
);
},
)
);
}
/// Reusable Row Widget for details
Widget
_buildDetailTile
(
String
label
,
String
?
value
,
double
scaleFactor
)
{
return
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
3
*
scaleFactor
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
// Align top if wraps
children:
[
// Label
Expanded
(
flex:
5
,
// keep same ratio as other tiles
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
SizedBox
(
width:
4
,),
// Value
Expanded
(
flex:
5
,
// take remaining width
child:
Text
(
value
??
"-"
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xFF818181
),
),
softWrap:
true
,
overflow:
TextOverflow
.
visible
,
// wrap instead of clipping
),
),
],
),
);
}
/// for location
Widget
buildLocationTile
(
String
label
,
String
?
value
,
double
scaleFactor
)
{
return
FutureBuilder
<
String
>(
future:
getReadableLocation
(
value
),
builder:
(
context
,
snapshot
)
{
final
locationText
=
snapshot
.
data
??
"-"
;
return
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
// aligns top when wrapping
children:
[
// Label
Expanded
(
flex:
5
,
// ratio (adjust same as your Date/Time tile)
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
// Value (Clickable Location)
Expanded
(
flex:
5
,
// take remaining space
child:
GestureDetector
(
onTap:
()
async
{
final
uri
=
Uri
.
parse
(
"https://www.google.com/maps/search/?api=1&query=
$value
"
);
if
(
await
canLaunchUrl
(
uri
))
{
await
launchUrl
(
uri
,
mode:
LaunchMode
.
externalApplication
);
}
},
child:
Text
(
locationText
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
blue
,
decoration:
TextDecoration
.
underline
,
fontWeight:
FontWeight
.
w400
,
),
softWrap:
true
,
overflow:
TextOverflow
.
visible
,
),
),
),
],
),
);
},
);
}
Future
<
String
>
getReadableLocation
(
String
?
value
)
async
{
if
(
value
==
null
)
return
"-"
;
try
{
List
<
Location
>
locations
=
await
locationFromAddress
(
value
);
List
<
Placemark
>
placemarks
=
await
placemarkFromCoordinates
(
locations
[
0
].
latitude
,
locations
[
0
].
longitude
,
);
return
placemarks
.
first
.
locality
??
value
;
}
catch
(
e
)
{
return
value
;
// fallback to raw coordinates
}
}
/// for date and time
Widget
_buildDate_TimeTile
(
String
label
,
String
?
date
,
String
?
time
,
double
scaleFactor
)
{
return
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
// align top when wrapped
children:
[
// Label
Expanded
(
flex:
5
,
// adjust ratio
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
SizedBox
(
width:
4
,),
// Value (date + time)
Expanded
(
flex:
5
,
// adjust ratio so both fill row
child:
Text
(
'
${date ?? "-"}
,
${time ?? "-"}
'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontWeight:
FontWeight
.
w400
,
),
softWrap:
true
,
// allow wrapping
overflow:
TextOverflow
.
visible
,
),
),
],
),
);
}
Widget
_buildSectionHeader
(
String
title
,
double
scaleFactor
)
{
return
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
9
*
scaleFactor
),
child:
Row
(
children:
[
Text
(
title
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaSemiBold"
,
),
),
SizedBox
(
width:
10
*
scaleFactor
),
Expanded
(
child:
DottedLine
(
dashGapLength:
4
,
dashGapColor:
Colors
.
white
,
dashColor:
AppColors
.
grey_semi
,
dashLength:
2
,
lineThickness:
0.5
,
),
),
],
),
);
}
/// Proof section (image/file path)
Widget
_buildProofLink
(
BuildContext
context
,
String
label
,
String
?
filePath
,
double
scaleFactor
)
{
return
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
children:
[
Expanded
(
flex:
5
,
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
Expanded
(
flex:
0
,
child:
filePath
!=
null
?
InkWell
(
onTap:
()
{
print
(
"++++++++++++++++ImageUrel:
$filePath
"
);
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
Image
.
network
(
filePath
),
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
),
);
},
child:
const
Text
(
"View"
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
blue
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
decoration:
TextDecoration
.
underline
),
),
)
:
const
Text
(
"-"
),
),
],
),
);
}
Color
getTextColor
(
value
)
{
var
color
=
AppColors
.
approved_text_color
;
switch
(
value
)
{
case
'Requested'
:
return
AppColors
.
requested_text_color
;
case
'Level 1 Approved'
:
return
AppColors
.
approved_text_color
;
case
'Level 1 Rejected'
:
return
AppColors
.
rejected_text_color
;
case
'Level 2 Approved'
:
return
AppColors
.
approved_text_color
;
case
'Level 2 Rejected'
:
return
AppColors
.
rejected_text_color
;
case
'Processed'
:
return
AppColors
.
processed_text_color
;
case
'Payment Rejected'
:
return
AppColors
.
rejected_text_color
;
}
return
color
;
}
Color
getDecorationColor
(
value
)
{
var
color
=
AppColors
.
approved_bg_color
;
switch
(
value
)
{
case
'Requested'
:
return
AppColors
.
requested_bg_color
;
case
'Level 1 Approved'
:
return
AppColors
.
approved_bg_color
;
case
'Level 1 Rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'Level 2 Approved'
:
return
AppColors
.
approved_bg_color
;
case
'Level 2 Rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'Processed'
:
return
AppColors
.
processed_bg_color
;
case
'Payment Rejected'
:
return
AppColors
.
rejected_bg_color
;
}
return
color
;
}
}
\ No newline at end of file
lib/screens/hrm/Attendancelist.dart
0 → 100644
View file @
332a8e91
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:generp/Utils/GlobalConstants.dart'
;
import
'package:generp/screens/hrm/AddManualAttendance.dart'
;
import
'package:generp/screens/hrm/AttendanceRequestDetail.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../CommonFilter2.dart'
;
import
'../commonDateRangeFilter.dart'
;
import
'AddLiveAttendance.dart'
;
class
Attendancelist
extends
StatefulWidget
{
const
Attendancelist
({
super
.
key
});
@override
State
<
Attendancelist
>
createState
()
=>
_AttendancelistState
();
}
class
_AttendancelistState
extends
State
<
Attendancelist
>
{
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// provider.fetchAttendanceRequests(context);
// });
// }
@override
Widget
build
(
BuildContext
context
)
{
return
SafeArea
(
top:
false
,
child:
ChangeNotifierProvider
(
create:
(
_
)
{
final
provider
=
Attendancelistprovider
();
Future
.
microtask
(()
{
provider
.
fetchAttendanceRequests
(
context
);
});
return
provider
;
},
builder:
(
context
,
child
)
{
return
Consumer
<
Attendancelistprovider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
Colors
.
white
,
title:
Row
(
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
),
const
SizedBox
(
width:
10
),
Text
(
"Attendance List"
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
],
),
actions:
[
InkResponse
(
onTap:
()
async
{
final
result
=
await
CommonFilter2
().
showFilterBottomSheet
(
context
);
if
(
result
!=
null
)
{
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
provider
.
updateFiltersFromSheet
(
context
,
type:
result
[
'type'
]
??
"All"
,
selectedValue:
result
[
'selectedValue'
]
??
"This Month"
,
customRange:
result
[
'dateRange'
],
);
}
},
child:
SvgPicture
.
asset
(
"assets/svg/filter_ic.svg"
,
height:
25
,
),
),
const
SizedBox
(
width:
20
),
],
),
backgroundColor:
const
Color
(
0xFFF6F6F8
),
body:
Column
(
children:
[
/// Filter chips - show active filters
// if (provider.selectedType != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Wrap(
// spacing: 8,
// children: [
// if (provider.selectedType != "All")
// Chip(
// label: Text('Type: ${provider.selectedType}'),
// onDeleted: () {
// provider.setTypeFilter(context, "All");
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter(context, "This Month");
// },
// ),
// ],
// ),
// ),
/// Attendance list
Expanded
(
child:
Builder
(
builder:
(
context
)
{
if
(
provider
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
));
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Center
(
child:
Text
(
provider
.
errorMessage
!));
}
if
(
provider
.
response
?.
requestList
==
null
||
provider
.
response
!.
requestList
!.
isEmpty
)
{
return
const
Center
(
child:
Text
(
"No attendance records found"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
grey
),
),
);
}
final
list
=
provider
.
response
!.
requestList
!;
return
ListView
.
builder
(
padding:
const
EdgeInsets
.
all
(
8
),
itemCount:
list
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
list
[
index
];
return
InkWell
(
borderRadius:
BorderRadius
.
circular
(
16
),
onTap:
()
{
/// navigation flow
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
AttendanceRequestDetailScreen
(
attendanceListId:
item
.
id
,
),
),
);
},
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8.5
,
vertical:
5
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
8.5
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Row
(
children:
[
/// Left Avatar Circle
Container
(
height:
48
,
width:
50
,
padding:
const
EdgeInsets
.
all
(
8.0
),
decoration:
BoxDecoration
(
color:
_getAvatarColor
(
item
.
status
),
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
Text
(
getText
(
item
.
status
),
style:
TextStyle
(
color:
_getTextColor
(
item
.
status
),
fontSize:
14
,
fontWeight:
FontWeight
.
bold
,
),
),
),
),
const
SizedBox
(
width:
10
),
/// Middle Section
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
item
.
type
??
"-"
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
Text
(
item
.
date
??
"-"
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
grey_semi
,
),
),
],
),
),
/// Right Status (Live / Manual)
Text
(
item
.
attendanceType
??
"-"
,
textAlign:
TextAlign
.
right
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
fontSize:
14
,
color:
(
item
.
attendanceType
??
""
).
toLowerCase
()
==
"live"
?
Colors
.
green
:
Colors
.
orange
,
),
),
],
),
),
);
},
);
},
),
)
],
),
bottomNavigationBar:
Container
(
alignment:
Alignment
.
bottomCenter
,
height:
54
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
mainAxisAlignment:
MainAxisAlignment
.
spaceEvenly
,
children:
[
Expanded
(
child:
InkResponse
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
AddLiveAttendanceScreen
(),
settings:
const
RouteSettings
(
name:
'AddLiveAttendanceScreen'
,
),
),
).
then
((
_
)
{
provider
.
fetchAttendanceRequests
(
context
);
});
},
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
SvgPicture
.
asset
(
"assets/svg/hrm/live.svg"
),
const
SizedBox
(
width:
10
),
Text
(
"Live Request"
,
style:
TextStyle
(
color:
AppColors
.
semi_black
)),
],
),
),
),
const
SizedBox
(
width:
10
),
SvgPicture
.
asset
(
"assets/svg/crm/vertical_line_ic.svg"
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
InkResponse
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
AddManualAttendanceScreen
(),
settings:
const
RouteSettings
(
name:
'AddManualAttendanceScreen'
),
),
).
then
((
_
)
{
provider
.
fetchAttendanceRequests
(
context
);
});
},
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
SvgPicture
.
asset
(
"assets/svg/hrm/manual.svg"
),
const
SizedBox
(
width:
10
),
Text
(
"Manual Request"
,
style:
TextStyle
(
color:
AppColors
.
semi_black
)),
],
),
),
),
],
),
),
);
},
);
},
)
);
}
/// Avatar color generator
Color
_getAvatarColor
(
value
)
{
var
color
=
AppColors
.
approved_bg_color
;
switch
(
value
)
{
case
'Requested'
:
return
AppColors
.
requested_bg_color
;
case
'Level 1 Approved'
:
return
AppColors
.
approved_bg_color
;
case
'Level 1 Rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'Level 2 Approved'
:
return
AppColors
.
approved_bg_color
;
case
'Level 2 Rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'Updated'
:
return
AppColors
.
processed_bg_color
;
case
'Payment Rejected'
:
return
AppColors
.
rejected_bg_color
;
}
return
color
;
}
Color
_getTextColor
(
value
)
{
var
color
=
AppColors
.
approved_text_color
;
switch
(
value
)
{
case
'Requested'
:
return
AppColors
.
requested_text_color
;
case
'Level 1 Approved'
:
return
AppColors
.
approved_text_color
;
case
'Level 1 Rejected'
:
return
AppColors
.
rejected_text_color
;
case
'Level 2 Approved'
:
return
AppColors
.
approved_text_color
;
case
'Level 2 Rejected'
:
return
AppColors
.
rejected_text_color
;
case
'Updated'
:
return
AppColors
.
processed_text_color
;
}
return
color
;
}
getText
(
value
)
{
switch
(
value
)
{
case
'Requested'
:
return
"R"
;
case
'Level 1 Approved'
:
return
"L1A"
;
case
'Level 1 Rejected'
:
return
"L1R"
;
case
'Level 2 Approved'
:
return
"L2A"
;
case
'Level 2 Rejected'
:
return
"L2R"
;
case
'Updated'
:
return
"U"
;
default
:
return
"Requested"
;
}
}
}
\ No newline at end of file
lib/screens/hrm/HrmDashboardScreen.dart
0 → 100644
View file @
332a8e91
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:generp/screens/hrm/OrganizationStructureScreen.dart'
;
import
'package:generp/screens/hrm/RewardListScreen.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'AttendanceRequestDetail.dart'
;
import
'LeaveApplicationScreen.dart'
;
import
'TourExpensesListScreen.dart'
;
import
'attendancelist.dart'
;
class
HrmdashboardScreen
extends
StatefulWidget
{
const
HrmdashboardScreen
({
super
.
key
});
@override
State
<
HrmdashboardScreen
>
createState
()
=>
_HrmdashboardScreenState
();
}
class
_HrmdashboardScreenState
extends
State
<
HrmdashboardScreen
>
{
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
const
Color
(
0xFFCEEDFF
),
// elevation: 2.0,
title:
SizedBox
(
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
),
const
SizedBox
(
width:
10
),
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
Text
(
"HRM"
,
style:
TextStyle
(
fontSize:
18
,
height:
1.1
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
),
],
),
),
),
backgroundColor:
Color
(
0xffF6F6F8
),
body:
SingleChildScrollView
(
child:
Column
(
children:
[
/// Background elements
Stack
(
children:
[
Container
(
width:
double
.
infinity
,
height:
490
,
color:
const
Color
(
0xffF6F6F8
),
),
Container
(
width:
double
.
infinity
,
height:
490
,
padding:
const
EdgeInsets
.
only
(
top:
1
,
bottom:
30
),
decoration:
const
BoxDecoration
(
gradient:
LinearGradient
(
colors:
[
Color
(
0xFFCEEDFF
),
Color
(
0xFFf9f9fb
),
Color
(
0xffF6F6F8
)
],
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
),
),
),
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
only
(
top:
1
,
bottom:
30
),
child:
Image
.
asset
(
"assets/images/vector.png"
,
height:
230
,
width:
double
.
infinity
,
fit:
BoxFit
.
fitWidth
,
),
),
Column
(
children:
[
/// Top Section with Gradient
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
only
(
top:
60
,
bottom:
30
),
child:
Column
(
children:
[
/// Illustration
SvgPicture
.
asset
(
"assets/images/capa.svg"
,
height:
146
,
width:
400
,
fit:
BoxFit
.
contain
,
),
const
SizedBox
(
height:
32
),
/// Organization Structure Button
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
20
,
vertical:
8
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
const
Color
(
0xFF1487C9
),
// border color
width:
1.2
,
// thickness of the border
),
color:
const
Color
(
0xffEDF8FF
),
borderRadius:
BorderRadius
.
circular
(
30
),
),
child:
InkWell
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
OrganizationStructureScreen
(),
),
);
},
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
SvgPicture
.
asset
(
"assets/svg/hrm/groupIc.svg"
,
height:
29
,
width:
29
,
fit:
BoxFit
.
contain
,
),
const
SizedBox
(
width:
7
),
const
Text
(
"Organization Structure"
,
style:
TextStyle
(
fontSize:
15
,
fontWeight:
FontWeight
.
w500
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
),
),
const
Icon
(
Icons
.
chevron_right
,
color:
Colors
.
black54
),
],
),
),
),
],
),
),
/// Bottom Grid Section
// Bottom Grid Section
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
final
itemWidth
=
180.0
;
// Fixed desired width for each item
final
availableWidth
=
constraints
.
maxWidth
;
final
crossAxisCount
=
(
availableWidth
/
itemWidth
).
floor
().
clamp
(
2
,
4
);
return
Padding
(
padding:
const
EdgeInsets
.
all
(
14
),
child:
GridView
.
count
(
crossAxisCount:
crossAxisCount
,
crossAxisSpacing:
8.5
,
mainAxisSpacing:
16
,
childAspectRatio:
1.7
,
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
children:
[
_buildTile
(
label:
"Attendance List"
,
subtitle:
"Real-time request"
,
assetIcon:
"assets/svg/hrm/attendanceList.svg"
,
txtColor:
const
Color
(
0xff1487C9
),
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
Attendancelist
(),
),
);
},
),
_buildTile
(
label:
"Leave Application"
,
subtitle:
"Apply & Track"
,
assetIcon:
"assets/svg/hrm/leaveApplication.svg"
,
txtColor:
const
Color
(
0xff1487C9
),
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
LeaveApplicationListScreen
(),
),
);
},
),
_buildTile
(
label:
"Rewards List"
,
subtitle:
"Track earned rewards"
,
assetIcon:
"assets/svg/hrm/rewardList.svg"
,
txtColor:
const
Color
(
0xff1487C9
),
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
RewardListScreen
(),
),
);
},
),
_buildTile
(
label:
"Tour Expenses"
,
subtitle:
"Submit and manage claims"
,
assetIcon:
"assets/svg/hrm/tourExp.svg"
,
txtColor:
const
Color
(
0xff1487C9
),
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
TourExpensesListScreen
(),
),
);
},
),
],
),
);
},
),
],
),
],
),
],
),
),
);
}
/// Reusable Tile Widget (Row style)
/// Reusable Tile Widget (Row style) - Updated to match design
Widget
_buildTile
({
required
String
label
,
required
String
subtitle
,
required
String
assetIcon
,
// SVG/PNG asset
required
Color
txtColor
,
VoidCallback
?
onTap
,
})
{
return
InkWell
(
onTap:
onTap
,
borderRadius:
BorderRadius
.
circular
(
14
),
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
vertical:
5
,
horizontal:
15
,
),
margin:
EdgeInsets
.
symmetric
(
vertical:
7
,
horizontal:
5
,
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
/// Left side text
Expanded
(
flex:
2
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
label
,
style:
TextStyle
(
fontSize:
14
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
SizedBox
(
height:
4
),
Text
(
subtitle
,
style:
TextStyle
(
fontSize:
12
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
SizedBox
(
width:
10
),
/// Right side icon (SVG/PNG)
Expanded
(
flex:
1
,
child:
Container
(
height:
42
,
width:
42
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
const
Color
(
0xFFEDF8FF
),
// icon bg
),
child:
Center
(
child:
SvgPicture
.
asset
(
height:
25
,
width:
25
,
assetIcon
,
fit:
BoxFit
.
contain
,
),
),
),
),
],
),
),
);
}
}
\ No newline at end of file
lib/screens/hrm/LeaveApplicationDetailScreen.dart
0 → 100644
View file @
332a8e91
import
'package:dotted_line/dotted_line.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:provider/provider.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'../../Notifiers/hrmProvider/leaveApplicationDetailsProvider.dart'
;
import
'../../Notifiers/HomeScreenNotifier.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../finance/FileViewer.dart'
;
/// Screen for leave application details
class
LeaveApplicationDetailScreen
extends
StatefulWidget
{
final
String
leaveRequestId
;
const
LeaveApplicationDetailScreen
({
super
.
key
,
required
this
.
leaveRequestId
});
@override
State
<
LeaveApplicationDetailScreen
>
createState
()
=>
_LeaveApplicationDetailScreenState
();
}
class
_LeaveApplicationDetailScreenState
extends
State
<
LeaveApplicationDetailScreen
>
{
@override
Widget
build
(
BuildContext
context
)
{
return
ChangeNotifierProvider
(
create:
(
_
)
=>
LeaveApplicationDetailsProvider
()..
fetchLeaveApplicationDetails
(
context
,
widget
.
leaveRequestId
),
child:
Consumer
<
LeaveApplicationDetailsProvider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
const
Color
(
0xFFFFFFFF
),
title:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
),
const
SizedBox
(
width:
10
),
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
Text
(
"Leave Application Details"
,
style:
TextStyle
(
fontSize:
18
,
height:
1.1
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
),
],
),
),
backgroundColor:
const
Color
(
0xFFF6F6F8
),
body:
Builder
(
builder:
(
context
)
{
if
(
provider
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
));
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Center
(
child:
Text
(
provider
.
errorMessage
!));
}
if
(
provider
.
response
?.
requestDetails
==
null
)
{
return
const
Center
(
child:
Text
(
"No details found"
));
}
final
details
=
provider
.
response
!.
requestDetails
!;
/// Screen content
return
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
children:
[
Card
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
16
),
),
elevation:
2
,
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
10.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
/// Header with status
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
0.5
),
padding:
const
EdgeInsets
.
symmetric
(
vertical:
10
,
horizontal:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
/// Left Avatar
Container
(
height:
48
,
width:
48
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
const
Color
(
0xFFEDF8FF
),
// icon bg
),
child:
Center
(
child:
SvgPicture
.
asset
(
height:
28
,
width:
28
,
"assets/svg/hrm/leaveApplication.svg"
,
// Use appropriate icon
fit:
BoxFit
.
contain
,
),
),
),
const
SizedBox
(
width:
12
),
/// Middle text
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
details
.
leaveType
??
"-"
,
style:
TextStyle
(
decoration:
TextDecoration
.
underline
,
decorationStyle:
TextDecorationStyle
.
dotted
,
decorationColor:
AppColors
.
grey_thick
,
height:
1.2
,
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
const
SizedBox
(
height:
2
),
Text
(
"Applied:
${details.appliedDate ?? "-"}
"
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
app_blue
,
),
),
],
),
),
/// Right side status badge
Container
(
height:
30
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
5
,
vertical:
1
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
8
),
color:
_getStatusBackgroundColor
(
details
.
status
),
),
child:
Center
(
child:
Text
(
details
.
status
??
"-"
,
textAlign:
TextAlign
.
center
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
fontSize:
12
,
color:
_getStatusTextColor
(
details
.
status
),
),
),
),
),
],
),
),
/// Leave Details
Padding
(
padding:
const
EdgeInsets
.
all
(
8.0
),
child:
Column
(
children:
[
_buildSectionHeader
(
"Leave Details"
),
_buildDetailTile
(
"Application ID"
,
details
.
id
),
_buildDetailTile
(
"Applied Date"
,
details
.
appliedDate
),
_buildDetailTile
(
"Leave Type"
,
details
.
leaveType
),
_buildDateRangeTile
(
"Leave Period"
,
details
.
fromDate
,
details
.
toDate
),
_buildTimeRangeTile
(
"Time Period"
,
details
.
fromTime
,
details
.
toTime
),
_buildDetailTile
(
"Reason"
,
details
.
reason
),
/// Approval Details
_buildSectionHeader
(
"Approval Details"
),
_buildDetailTile
(
"Requested To"
,
details
.
requestedTo
),
_buildDetailTile
(
"Approved By"
,
details
.
approvedBy
),
_buildDetailTile
(
"Approved Date"
,
details
.
approvedDate
),
_buildDetailTile
(
"Approval Remarks"
,
details
.
approvalRemarks
),
/// Additional Information
_buildSectionHeader
(
"Additional Information"
),
_buildDetailTile
(
"Status"
,
details
.
status
),
_buildDetailTile
(
"From Time"
,
details
.
fromTime
),
_buildDetailTile
(
"To Time"
,
details
.
toTime
),
],
),
),
],
),
),
),
const
SizedBox
(
height:
30
),
],
),
);
},
),
);
},
),
);
}
/// Reusable Row Widget for details
Widget
_buildDetailTile
(
String
label
,
String
?
value
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
3
),
child:
Row
(
children:
[
Expanded
(
flex:
6
,
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
Expanded
(
flex:
0
,
child:
Text
(
value
??
"-"
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontWeight:
FontWeight
.
w400
,
),
),
),
],
),
);
}
/// For date range display
Widget
_buildDateRangeTile
(
String
label
,
String
?
fromDate
,
String
?
toDate
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
3
),
child:
Row
(
children:
[
Expanded
(
flex:
6
,
child:
Text
(
label
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
Expanded
(
flex:
0
,
child:
Text
(
'
${fromDate ?? "-"}
to
${toDate ?? "-"}
'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontWeight:
FontWeight
.
w400
,
),
),
),
],
),
);
}
/// For time range display
Widget
_buildTimeRangeTile
(
String
label
,
String
?
fromTime
,
String
?
toTime
)
{
if
((
fromTime
==
null
||
fromTime
.
isEmpty
)
&&
(
toTime
==
null
||
toTime
.
isEmpty
))
{
return
const
SizedBox
.
shrink
();
// Hide if no time data
}
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
3
),
child:
Row
(
children:
[
Expanded
(
flex:
6
,
child:
Text
(
label
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff2D2D2D
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
),
),
),
Expanded
(
flex:
0
,
child:
Text
(
'
${fromTime ?? "-"}
to
${toTime ?? "-"}
'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontWeight:
FontWeight
.
w400
,
),
),
),
],
),
);
}
/// Section header with dotted line
Widget
_buildSectionHeader
(
String
title
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
8
),
child:
Row
(
children:
[
Text
(
title
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaSemiBold"
,
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
DottedLine
(
dashGapLength:
4
,
dashGapColor:
Colors
.
white
,
dashColor:
AppColors
.
grey_semi
,
dashLength:
2
,
lineThickness:
0.5
,
),
),
],
),
);
}
/// Status background color
Color
_getStatusBackgroundColor
(
String
?
status
)
{
switch
(
status
?.
toLowerCase
())
{
case
'approved'
:
return
AppColors
.
approved_bg_color
;
case
'rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'requested'
:
default
:
return
AppColors
.
requested_bg_color
;
}
}
/// Status text color
Color
_getStatusTextColor
(
String
?
status
)
{
switch
(
status
?.
toLowerCase
())
{
case
'approved'
:
return
AppColors
.
approved_text_color
;
case
'rejected'
:
return
AppColors
.
rejected_text_color
;
case
'requested'
:
default
:
return
AppColors
.
requested_text_color
;
}
}
}
\ No newline at end of file
lib/screens/hrm/LeaveApplicationScreen.dart
0 → 100644
View file @
332a8e91
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Notifiers/hrmProvider/leaveApplicationListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../commonDateRangeFilter.dart'
;
import
'AddLeaveRequestScreen.dart'
;
import
'LeaveApplicationDetailScreen.dart'
;
class
LeaveApplicationListScreen
extends
StatefulWidget
{
const
LeaveApplicationListScreen
({
super
.
key
});
@override
State
<
LeaveApplicationListScreen
>
createState
()
=>
_LeaveApplicationListScreenState
();
}
class
_LeaveApplicationListScreenState
extends
State
<
LeaveApplicationListScreen
>
{
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final provider = Provider.of<LeaveApplicationListProvider>(context, listen: false);
// provider.fetchLeaveApplications(context);
// });
// }
@override
Widget
build
(
BuildContext
context
)
{
return
ChangeNotifierProvider
(
create:
(
_
)
{
final
provider
=
LeaveApplicationListProvider
();
Future
.
microtask
(()
{
provider
.
fetchLeaveApplications
(
context
);
});
return
provider
;
},
builder:
(
context
,
child
)
{
return
Consumer
<
LeaveApplicationListProvider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
appBar:
appbar2New
(
context
,
"Leave Application List"
,
provider
.
resetForm
,
Row
(
children:
[
InkResponse
(
onTap:
()
async
{
var
cf
=
Commondaterangefilter
();
var
result
=
await
cf
.
showFilterBottomSheet
(
context
);
if
(
result
!=
null
)
{
var
dateRange
=
result
[
'dateRange'
]
as
DateTimeRange
?;
var
formatted
=
result
[
'formatted'
]
as
List
<
String
>;
if
(
formatted
.
isNotEmpty
)
{
provider
.
setDateRangeFilter
(
"Custom"
,
customRange:
dateRange
);
provider
.
fetchLeaveApplications
(
context
,
dateRange:
"Custom"
,
customRange:
dateRange
,
);
}
}
},
child:
SvgPicture
.
asset
(
"assets/svg/filter_ic.svg"
,
height:
25
),
),
],
),
0xFFFFFFFF
,
),
backgroundColor:
const
Color
(
0xFFF6F6F8
),
body:
Column
(
children:
[
/// Filter chips (if you want visible filter indicators)
// if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Row(
// children: [
// if (provider.selectedStatus != "All")
// Chip(
// label: Text('Status: ${provider.selectedStatus}'),
// onDeleted: () {
// provider.setStatusFilter("All");
// provider.fetchLeaveApplications(context);
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter("This Month");
// provider.fetchLeaveApplications(context);
// },
// ),
// ],
// ),
// ),
/// Leave application list
Expanded
(
child:
Builder
(
builder:
(
context
)
{
if
(
provider
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
));
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Center
(
child:
Text
(
provider
.
errorMessage
!));
}
if
(
provider
.
response
?.
requestList
==
null
||
provider
.
response
!.
requestList
!.
isEmpty
)
{
return
const
Center
(
child:
Text
(
"No leave applications found"
));
}
final
list
=
provider
.
response
!.
requestList
!;
return
ListView
.
builder
(
padding:
const
EdgeInsets
.
all
(
8
),
itemCount:
list
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
list
[
index
];
// Parse the full string into a DateTime object
DateTime
parsedFromDate
=
DateFormat
(
"dd MMM yyyy, hh:mm a"
).
parse
(
item
.
fromPeriod
.
toString
());
String
dateFromMonth
=
DateFormat
(
"dd MMM"
).
format
(
parsedFromDate
);
// Parse the full string into a DateTime object
DateTime
parsedToDate
=
DateFormat
(
"dd MMM yyyy, hh:mm a"
).
parse
(
item
.
toPeriod
.
toString
());
String
dateToMonth
=
DateFormat
(
"dd MMM yyyy"
).
format
(
parsedToDate
);
return
InkWell
(
borderRadius:
BorderRadius
.
circular
(
16
),
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
LeaveApplicationDetailScreen
(
leaveRequestId:
item
.
id
.
toString
(),
),
),
).
then
((
_
)
{
provider
.
fetchLeaveApplications
(
context
);
});
},
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8.5
,
vertical:
5
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Row
(
children:
[
/// Left Status Circle
Container
(
height:
48
,
width:
48
,
padding:
const
EdgeInsets
.
all
(
8.0
),
decoration:
BoxDecoration
(
color:
_getStatusBackgroundColor
(
item
.
status
),
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
Text
(
_getStatusInitials
(
item
.
status
),
style:
TextStyle
(
color:
_getStatusTextColor
(
item
.
status
),
fontSize:
14
,
fontWeight:
FontWeight
.
bold
,
),
),
),
),
const
SizedBox
(
width:
12
),
/// Middle Section - Leave Details
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
item
.
leaveType
??
"-"
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
const
SizedBox
(
height:
4
),
Row
(
children:
[
Text
(
dateFromMonth
??
"-"
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
grey_semi
,
),
),
Text
(
" -
${dateToMonth}
"
??
"-"
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
grey_semi
,
),
),
],
),
// const SizedBox(height: 2),
// Text(
// "Period: ${item.fromPeriod ?? "-"} to ${item.toPeriod ?? "-"}",
// style: const TextStyle(
// fontSize: 12.5,
// color: Color(0xff818181),
// fontFamily: "Plus Jakarta Sans",
// ),
// ),
],
),
),
// /// Right Status
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// decoration: BoxDecoration(
// color: _getStatusBackgroundColor(item.status),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Text(
// item.status ?? "-",
// style: TextStyle(
// fontFamily: "JakartaMedium",
// fontSize: 13,
// color: _getStatusTextColor(item.status),
// ),
// ),
// ),
],
),
),
);
},
);
},
),
)
],
),
floatingActionButtonLocation:
FloatingActionButtonLocation
.
centerFloat
,
floatingActionButton:
InkResponse
(
onTap:
()
{
HapticFeedback
.
selectionClick
();
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
_
)
=>
ChangeNotifierProvider
(
create:
(
_
)
=>
LeaveApplicationListProvider
(),
child:
AddLeaveRequest
(
pageTitleName:
"Add Leave Request"
),
),
),
).
then
((
_
)
{
provider
.
fetchLeaveApplications
(
context
);
});
// show add bill screen here
},
child:
Container
(
height:
45
,
alignment:
Alignment
.
center
,
margin:
EdgeInsets
.
symmetric
(
horizontal:
20
),
padding:
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
5
),
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Text
(
"Add Leave Request"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
),
),
),
),
);
},
);
},
);
}
/// Get status background color
Color
_getStatusBackgroundColor
(
String
?
status
)
{
switch
(
status
?.
toLowerCase
())
{
case
'approved'
:
return
AppColors
.
approved_bg_color
;
case
'rejected'
:
return
AppColors
.
rejected_bg_color
;
case
'requested'
:
default
:
return
AppColors
.
requested_bg_color
;
}
}
/// Get status text color
Color
_getStatusTextColor
(
String
?
status
)
{
switch
(
status
?.
toLowerCase
())
{
case
'approved'
:
return
AppColors
.
approved_text_color
;
case
'rejected'
:
return
AppColors
.
rejected_text_color
;
case
'requested'
:
default
:
return
AppColors
.
requested_text_color
;
}
}
/// Get status initials
String
_getStatusInitials
(
String
?
status
)
{
switch
(
status
?.
toLowerCase
())
{
case
'approved'
:
return
"A"
;
case
'rejected'
:
return
"R"
;
case
'requested'
:
default
:
return
"P"
;
// Pending
}
}
}
\ No newline at end of file
Prev
1
2
3
4
Next