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