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
f6fbe101
Commit
f6fbe101
authored
Aug 29, 2025
by
Mohit Kumar
Browse files
AttendanceList
RewardList TourExpenses Implementation
parent
6d1deaf2
Changes
33
Show whitespace changes
Inline
Side-by-side
assets/svg/hrm/achievement_ic.svg
0 → 100644
View file @
f6fbe101
<svg
width=
"21"
height=
"21"
viewBox=
"0 0 21 21"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<g
clip-path=
"url(#clip0_319_5695)"
>
<path
d=
"M1.07683 7.38789C1.08816 7.31523 1.10027 7.24102 1.11238 7.16367L1.11706 7.13555C1.26472 6.20742 1.75886 5.28945 2.47917 4.53672C3.19949 3.78398 4.11238 3.23789 5.04988 3.04297C5.2741 2.99648 5.52136 2.95078 5.78738 2.90703C5.79805 2.70339 5.80925 2.49987 5.82097 2.29648C5.85105 1.78125 6.26355 1.31992 6.73738 1.27891C7.21121 1.23789 7.57956 1.63633 7.5616 2.16055C7.55535 2.33372 7.54923 2.50664 7.54324 2.6793C8.30066 2.60469 9.11355 2.55352 9.92605 2.53281C9.92605 2.37656 9.92748 2.22148 9.93035 2.06758C9.93659 1.53867 10.3268 1.10156 10.8014 1.10156C11.276 1.10156 11.6671 1.53945 11.6729 2.06758C11.6745 2.22383 11.6759 2.37891 11.6772 2.53281C12.4874 2.55352 13.2968 2.6043 14.0514 2.67852C14.0452 2.50586 14.0391 2.33307 14.0331 2.16016C14.0151 1.63555 14.3846 1.23594 14.8573 1.27813C15.33 1.32031 15.7436 1.78047 15.7737 2.2957C15.7854 2.49883 15.7966 2.70208 15.8073 2.90547C16.0768 2.94961 16.3276 2.99609 16.5542 3.04297C17.4917 3.23828 18.4034 3.78516 19.1249 4.53672C19.8464 5.28828 20.3393 6.20742 20.487 7.13555L20.4917 7.16406C20.5042 7.24219 20.5159 7.31523 20.5272 7.38789C14.0526 6.90977 7.55148 6.90977 1.07683 7.38789ZM20.8011 11.216C20.7686 13.5695 20.6038 14.341 20.4128 15.4801C20.4128 15.4898 20.4096 15.4992 20.4077 15.509C20.2432 16.4418 19.737 17.3734 19.0147 18.1445C18.2925 18.9156 17.3874 19.4797 16.4635 19.6895C15.1038 20.0051 12.8843 20.2672 10.8022 20.2582C8.72019 20.2672 6.50027 20.0051 5.1405 19.6895C4.21589 19.4801 3.31277 18.9137 2.58933 18.1449C1.86589 17.3762 1.36081 16.4422 1.19636 15.5094C1.19636 15.5 1.19285 15.4902 1.19128 15.4809C1.00027 14.341 0.835814 13.5707 0.803002 11.2164C0.794799 10.0418 0.838158 9.31328 0.893236 8.73633C7.48464 8.44414 14.1194 8.44414 20.7108 8.73633C20.7663 9.31289 20.8093 10.0418 20.8011 11.2164V11.216ZM7.19324 13.7902C7.1823 12.9789 6.54558 12.3148 5.76589 12.3027C5.58217 12.2984 5.39949 12.3316 5.22902 12.4002C5.05856 12.4689 4.9039 12.5716 4.7745 12.7021C4.6451 12.8326 4.54368 12.9882 4.47645 13.1592C4.40921 13.3302 4.37757 13.5132 4.38347 13.6969C4.40535 14.4781 5.05535 15.1574 5.8241 15.1977C6.59285 15.2379 7.20378 14.6012 7.19324 13.7902ZM12.2647 13.8266C12.2702 13.0082 11.6397 12.3477 10.8585 12.3473C10.0772 12.3469 9.44753 13.009 9.45222 13.8273C9.45691 14.6457 10.0882 15.3203 10.8585 15.3199C11.6288 15.3195 12.2589 14.6445 12.2647 13.8266ZM17.4155 13.6879C17.4214 13.5045 17.3898 13.3219 17.3226 13.1512C17.2554 12.9805 17.154 12.8253 17.0246 12.6952C16.8953 12.5651 16.7407 12.4627 16.5704 12.3945C16.4001 12.3263 16.2177 12.2936 16.0343 12.2984C15.253 12.3109 14.6178 12.9742 14.6061 13.7828C14.5944 14.5914 15.2046 15.2281 15.9733 15.1855C16.7421 15.143 17.3936 14.4691 17.4155 13.6879Z"
fill=
"url(#paint0_linear_319_5695)"
/>
</g>
<defs>
<linearGradient
id=
"paint0_linear_319_5695"
x1=
"12.1655"
y1=
"23.2878"
x2=
"-2.4722"
y2=
"3.87753"
gradientUnits=
"userSpaceOnUse"
>
<stop
stop-color=
"#498D1E"
/>
<stop
offset=
"1"
stop-color=
"#81C320"
/>
</linearGradient>
<clipPath
id=
"clip0_319_5695"
>
<rect
width=
"20"
height=
"20"
fill=
"white"
transform=
"translate(0.77832 0.679688)"
/>
</clipPath>
</defs>
</svg>
assets/svg/hrm/ballance_ic.svg
0 → 100644
View file @
f6fbe101
<svg
width=
"23"
height=
"23"
viewBox=
"0 0 23 23"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<g
clip-path=
"url(#clip0_319_5711)"
>
<path
d=
"M22.595 9.39164C22.3853 7.75582 20.9282 6.3718 19.2232 6.14492C16.2184 5.84758 11.7789 5.82266 11.778 5.8334C9.85776 5.81363 5.36237 5.99668 4.33284 6.14492C3.29041 6.27941 2.34811 6.85434 1.71733 7.64582V7.64668C1.31471 8.15371 1.04143 8.75012 0.961078 9.39164C0.838617 10.3804 0.742367 11.7317 0.791351 13.5798C0.843343 15.4283 1.01307 16.7823 1.18666 17.7744C1.4853 19.4163 2.96256 20.8583 4.60655 21.1346C5.60256 21.3245 9.9308 21.5883 11.7785 21.5548C13.6261 21.5888 17.9544 21.3249 18.9504 21.1346C20.5944 20.8583 22.0716 19.4163 22.3703 17.7744C22.5434 16.7818 22.7131 15.4279 22.7656 13.5798C22.8146 11.7313 22.7179 10.3804 22.5958 9.39164H22.595ZM18.9388 15.2182C18.0974 15.2182 17.4151 14.5363 17.4151 13.6945C17.4151 12.8528 18.097 12.1709 18.9388 12.1709C19.7805 12.1709 20.4624 12.8528 20.4624 13.6945C20.4624 14.5363 19.7805 15.2182 18.9388 15.2182ZM4.2976 5.06555C5.42381 4.88852 10.0451 4.68914 11.7776 4.71492C13.2678 4.70074 16.8939 4.82664 18.5967 4.99465C18.0532 3.45809 16.6683 2.23477 15.0914 2.03281C13.9823 1.89445 12.469 1.78102 10.3996 1.80895C8.33022 1.83988 6.81428 1.99629 5.70182 2.16387C3.86448 2.45348 2.22307 4.06996 1.90768 5.92063C1.89994 5.96703 1.89221 6.01988 1.88362 6.07746C2.58787 5.55367 3.41588 5.18672 4.2976 5.06512V5.06555Z"
fill=
"url(#paint0_linear_319_5711)"
/>
</g>
<defs>
<linearGradient
id=
"paint0_linear_319_5711"
x1=
"0.790113"
y1=
"11.5706"
x2=
"27.2783"
y2=
"27.6814"
gradientUnits=
"userSpaceOnUse"
>
<stop
stop-color=
"#FFC80B"
/>
<stop
offset=
"1"
stop-color=
"#E89318"
/>
</linearGradient>
<clipPath
id=
"clip0_319_5711"
>
<rect
width=
"22"
height=
"22"
fill=
"white"
transform=
"translate(0.77832 0.679688)"
/>
</clipPath>
</defs>
</svg>
assets/svg/hrm/location_ic.svg
0 → 100644
View file @
f6fbe101
<svg
width=
"21"
height=
"21"
viewBox=
"0 0 21 21"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<g
clip-path=
"url(#clip0_319_5704)"
>
<path
d=
"M10.4369 20.6078C10.6568 20.7035 10.908 20.7035 11.1279 20.6078C13.7295 19.475 19.0666 14.0719 19.0666 8.95938C19.0666 4.38672 15.3596 0.679688 10.7869 0.679688H10.7693C6.19629 0.679688 2.48926 4.38672 2.48926 8.95938C2.48926 14.0723 7.83457 19.4754 10.4365 20.6078H10.4369ZM10.7291 4.36875C12.0232 4.36875 13.0725 5.41797 13.0725 6.71211C13.0725 8.00625 12.0232 9.05547 10.7291 9.05547C9.43496 9.05547 8.38574 8.00625 8.38574 6.71211C8.38574 5.41797 9.43496 4.36875 10.7291 4.36875ZM6.90762 12.3672C6.86582 11.9605 6.88418 11.6184 6.88418 11.618C6.88418 11.618 6.86582 11.3973 6.90762 10.9973C6.90762 10.9938 6.9084 10.9906 6.90879 10.9871C6.94316 10.659 7.09082 10.3535 7.32519 10.1187C7.55957 9.88477 7.87012 9.73281 8.20019 9.69727C8.68496 9.64609 10.1596 9.61797 10.9092 9.61797C11.6588 9.61797 12.8619 9.6457 13.3467 9.69727C13.6764 9.73242 13.9873 9.88477 14.2217 10.1187C14.4561 10.3535 14.6037 10.659 14.6381 10.9871C14.6381 10.9906 14.6389 10.9938 14.6393 10.9973C14.6811 11.3977 14.6756 11.6172 14.6756 11.6176C14.6756 11.6176 14.6811 11.9609 14.6396 12.368C14.6396 12.3715 14.6389 12.3746 14.6385 12.3781C14.6041 12.7117 14.4572 13.041 14.2236 13.3082C13.9904 13.575 13.6807 13.7664 13.3514 13.8359C12.8678 13.9453 11.6623 14.0531 10.91 14.048C10.1576 14.0668 8.67949 13.95 8.1959 13.8352C7.8666 13.7656 7.55684 13.5742 7.32363 13.3074C7.09043 13.0402 6.94316 12.7109 6.90879 12.3777C6.90879 12.3742 6.90801 12.3707 6.90762 12.3676V12.3672Z"
fill=
"url(#paint0_linear_319_5704)"
/>
<path
d=
"M10.4369 20.6078C10.6568 20.7035 10.908 20.7035 11.1279 20.6078C13.7295 19.475 19.0666 14.0719 19.0666 8.95938C19.0666 4.38672 15.3596 0.679688 10.7869 0.679688H10.7693C6.19629 0.679688 2.48926 4.38672 2.48926 8.95938C2.48926 14.0723 7.83457 19.4754 10.4365 20.6078H10.4369ZM10.7291 4.36875C12.0232 4.36875 13.0725 5.41797 13.0725 6.71211C13.0725 8.00625 12.0232 9.05547 10.7291 9.05547C9.43496 9.05547 8.38574 8.00625 8.38574 6.71211C8.38574 5.41797 9.43496 4.36875 10.7291 4.36875ZM6.90762 12.3672C6.86582 11.9605 6.88418 11.6184 6.88418 11.618C6.88418 11.618 6.86582 11.3973 6.90762 10.9973C6.90762 10.9938 6.9084 10.9906 6.90879 10.9871C6.94316 10.659 7.09082 10.3535 7.32519 10.1187C7.55957 9.88477 7.87012 9.73281 8.20019 9.69727C8.68496 9.64609 10.1596 9.61797 10.9092 9.61797C11.6588 9.61797 12.8619 9.6457 13.3467 9.69727C13.6764 9.73242 13.9873 9.88477 14.2217 10.1187C14.4561 10.3535 14.6037 10.659 14.6381 10.9871C14.6381 10.9906 14.6389 10.9938 14.6393 10.9973C14.6811 11.3977 14.6756 11.6172 14.6756 11.6176C14.6756 11.6176 14.6811 11.9609 14.6396 12.368C14.6396 12.3715 14.6389 12.3746 14.6385 12.3781C14.6041 12.7117 14.4572 13.041 14.2236 13.3082C13.9904 13.575 13.6807 13.7664 13.3514 13.8359C12.8678 13.9453 11.6623 14.0531 10.91 14.048C10.1576 14.0668 8.67949 13.95 8.1959 13.8352C7.8666 13.7656 7.55684 13.5742 7.32363 13.3074C7.09043 13.0402 6.94316 12.7109 6.90879 12.3777C6.90879 12.3742 6.90801 12.3707 6.90762 12.3676V12.3672Z"
fill=
"url(#paint1_linear_319_5704)"
/>
</g>
<defs>
<linearGradient
id=
"paint0_linear_319_5704"
x1=
"2.48926"
y1=
"10.6797"
x2=
"19.0666"
y2=
"10.6797"
gradientUnits=
"userSpaceOnUse"
>
<stop
stop-color=
"#0080DE"
/>
<stop
offset=
"0.6"
stop-color=
"#49BCFF"
/>
<stop
offset=
"1"
stop-color=
"#61CAFF"
/>
</linearGradient>
<linearGradient
id=
"paint1_linear_319_5704"
x1=
"-6.72168"
y1=
"-0.320313"
x2=
"20.7783"
y2=
"12.6797"
gradientUnits=
"userSpaceOnUse"
>
<stop
stop-color=
"#6949C6"
/>
<stop
offset=
"0.6"
stop-color=
"#7C6EE0"
/>
<stop
offset=
"1"
stop-color=
"#A28EEF"
/>
</linearGradient>
<clipPath
id=
"clip0_319_5704"
>
<rect
width=
"20"
height=
"20"
fill=
"white"
transform=
"translate(0.77832 0.679688)"
/>
</clipPath>
</defs>
</svg>
lib/Models/hrmModels/hrmAccessiblePagesResponse.dart
0 → 100644
View file @
f6fbe101
class
hrmAccessiblePagesResponse
{
String
?
error
;
List
<
PagesAccessible
>?
pagesAccessible
;
String
?
message
;
int
?
sessionExists
;
hrmAccessiblePagesResponse
(
{
this
.
error
,
this
.
pagesAccessible
,
this
.
message
,
this
.
sessionExists
});
hrmAccessiblePagesResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
error
=
json
[
'error'
];
if
(
json
[
'pages_accessible'
]
!=
null
)
{
pagesAccessible
=
<
PagesAccessible
>[];
json
[
'pages_accessible'
].
forEach
((
v
)
{
pagesAccessible
!.
add
(
new
PagesAccessible
.
fromJson
(
v
));
});
}
message
=
json
[
'message'
];
sessionExists
=
json
[
'session_exists'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
data
[
'error'
]
=
this
.
error
;
if
(
this
.
pagesAccessible
!=
null
)
{
data
[
'pages_accessible'
]
=
this
.
pagesAccessible
!.
map
((
v
)
=>
v
.
toJson
()).
toList
();
}
data
[
'message'
]
=
this
.
message
;
data
[
'session_exists'
]
=
this
.
sessionExists
;
return
data
;
}
}
class
PagesAccessible
{
String
?
id
;
String
?
pageName
;
String
?
mode
;
PagesAccessible
({
this
.
id
,
this
.
pageName
,
this
.
mode
});
PagesAccessible
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
id
=
json
[
'id'
];
pageName
=
json
[
'page_name'
];
mode
=
json
[
'mode'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
data
[
'id'
]
=
this
.
id
;
data
[
'page_name'
]
=
this
.
pageName
;
data
[
'mode'
]
=
this
.
mode
;
return
data
;
}
}
lib/Models/hrmModels/leaveApplicationDetailsResponse.dart
0 → 100644
View file @
f6fbe101
class
leaveApplicationDetailsResponse
{
RequestDetails
?
requestDetails
;
String
?
error
;
String
?
message
;
int
?
sessionExists
;
leaveApplicationDetailsResponse
(
{
this
.
requestDetails
,
this
.
error
,
this
.
message
,
this
.
sessionExists
});
leaveApplicationDetailsResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
requestDetails
=
json
[
'request_details'
]
!=
null
?
new
RequestDetails
.
fromJson
(
json
[
'request_details'
])
:
null
;
error
=
json
[
'error'
];
message
=
json
[
'message'
];
sessionExists
=
json
[
'session_exists'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
if
(
this
.
requestDetails
!=
null
)
{
data
[
'request_details'
]
=
this
.
requestDetails
!.
toJson
();
}
data
[
'error'
]
=
this
.
error
;
data
[
'message'
]
=
this
.
message
;
data
[
'session_exists'
]
=
this
.
sessionExists
;
return
data
;
}
}
class
RequestDetails
{
String
?
id
;
String
?
appliedDate
;
String
?
fromDate
;
String
?
toDate
;
String
?
fromTime
;
String
?
toTime
;
String
?
reason
;
String
?
leaveType
;
String
?
status
;
String
?
requestedTo
;
String
?
approvedBy
;
String
?
approvedDate
;
String
?
approvalRemarks
;
RequestDetails
(
{
this
.
id
,
this
.
appliedDate
,
this
.
fromDate
,
this
.
toDate
,
this
.
fromTime
,
this
.
toTime
,
this
.
reason
,
this
.
leaveType
,
this
.
status
,
this
.
requestedTo
,
this
.
approvedBy
,
this
.
approvedDate
,
this
.
approvalRemarks
});
RequestDetails
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
id
=
json
[
'id'
];
appliedDate
=
json
[
'applied_date'
];
fromDate
=
json
[
'from_date'
];
toDate
=
json
[
'to_date'
];
fromTime
=
json
[
'from_time'
];
toTime
=
json
[
'to_time'
];
reason
=
json
[
'reason'
];
leaveType
=
json
[
'leave_type'
];
status
=
json
[
'status'
];
requestedTo
=
json
[
'requested_to'
];
approvedBy
=
json
[
'approved_by'
];
approvedDate
=
json
[
'approved_date'
];
approvalRemarks
=
json
[
'approval_remarks'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
data
[
'id'
]
=
this
.
id
;
data
[
'applied_date'
]
=
this
.
appliedDate
;
data
[
'from_date'
]
=
this
.
fromDate
;
data
[
'to_date'
]
=
this
.
toDate
;
data
[
'from_time'
]
=
this
.
fromTime
;
data
[
'to_time'
]
=
this
.
toTime
;
data
[
'reason'
]
=
this
.
reason
;
data
[
'leave_type'
]
=
this
.
leaveType
;
data
[
'status'
]
=
this
.
status
;
data
[
'requested_to'
]
=
this
.
requestedTo
;
data
[
'approved_by'
]
=
this
.
approvedBy
;
data
[
'approved_date'
]
=
this
.
approvedDate
;
data
[
'approval_remarks'
]
=
this
.
approvalRemarks
;
return
data
;
}
}
lib/Models/hrmModels/leaveApplicationLIstResponse.dart
0 → 100644
View file @
f6fbe101
class
leaveApplicationLIstResponse
{
List
<
RequestList
>?
requestList
;
String
?
error
;
String
?
message
;
int
?
sessionExists
;
leaveApplicationLIstResponse
(
{
this
.
requestList
,
this
.
error
,
this
.
message
,
this
.
sessionExists
});
leaveApplicationLIstResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
if
(
json
[
'request_list'
]
!=
null
)
{
requestList
=
<
RequestList
>[];
json
[
'request_list'
].
forEach
((
v
)
{
requestList
!.
add
(
new
RequestList
.
fromJson
(
v
));
});
}
error
=
json
[
'error'
];
message
=
json
[
'message'
];
sessionExists
=
json
[
'session_exists'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
if
(
this
.
requestList
!=
null
)
{
data
[
'request_list'
]
=
this
.
requestList
!.
map
((
v
)
=>
v
.
toJson
()).
toList
();
}
data
[
'error'
]
=
this
.
error
;
data
[
'message'
]
=
this
.
message
;
data
[
'session_exists'
]
=
this
.
sessionExists
;
return
data
;
}
}
class
RequestList
{
String
?
id
;
String
?
appliedDate
;
String
?
fromPeriod
;
String
?
toPeriod
;
String
?
status
;
String
?
leaveType
;
RequestList
(
{
this
.
id
,
this
.
appliedDate
,
this
.
fromPeriod
,
this
.
toPeriod
,
this
.
status
});
RequestList
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
id
=
json
[
'id'
];
appliedDate
=
json
[
'applied_date'
];
fromPeriod
=
json
[
'from_period'
];
toPeriod
=
json
[
'to_period'
];
status
=
json
[
'status'
];
leaveType
=
json
[
"leave_type"
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
data
[
'id'
]
=
this
.
id
;
data
[
'applied_date'
]
=
this
.
appliedDate
;
data
[
'from_period'
]
=
this
.
fromPeriod
;
data
[
'to_period'
]
=
this
.
toPeriod
;
data
[
'status'
]
=
this
.
status
;
data
[
"leave_type"
]
=
this
.
leaveType
;
return
data
;
}
}
lib/Models/hrmModels/tourExpensesAddViewResponse.dart
0 → 100644
View file @
f6fbe101
class
tourExpensesAddViewResponse
{
List
<
int
>?
daAmount
;
List
<
String
>?
tourType
;
List
<
String
>?
travelType
;
String
?
error
;
String
?
message
;
int
?
sessionExists
;
tourExpensesAddViewResponse
(
{
this
.
daAmount
,
this
.
tourType
,
this
.
travelType
,
this
.
error
,
this
.
message
,
this
.
sessionExists
});
tourExpensesAddViewResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
daAmount
=
json
[
'da_amount'
].
cast
<
int
>();
tourType
=
json
[
'tour_type'
].
cast
<
String
>();
travelType
=
json
[
'travel_type'
].
cast
<
String
>();
error
=
json
[
'error'
];
message
=
json
[
'message'
];
sessionExists
=
json
[
'session_exists'
];
}
Map
<
String
,
dynamic
>
toJson
()
{
final
Map
<
String
,
dynamic
>
data
=
new
Map
<
String
,
dynamic
>();
data
[
'da_amount'
]
=
this
.
daAmount
;
data
[
'tour_type'
]
=
this
.
tourType
;
data
[
'travel_type'
]
=
this
.
travelType
;
data
[
'error'
]
=
this
.
error
;
data
[
'message'
]
=
this
.
message
;
data
[
'session_exists'
]
=
this
.
sessionExists
;
return
data
;
}
}
lib/Notifiers/hrmProvider/LeaveApplicationDetailsProvider.dart
0 → 100644
View file @
f6fbe101
import
'package:flutter/cupertino.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/leaveApplicationDetailsResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
LeaveApplicationDetailsProvider
extends
ChangeNotifier
{
leaveApplicationDetailsResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
leaveApplicationDetailsResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
/// Fetch leave application details
Future
<
void
>
fetchLeaveApplicationDetails
(
BuildContext
context
,
String
leaveRequestId
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
_response
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
leaveApplicationDetailAPI
(
provider
.
session
,
provider
.
empId
,
leaveRequestId
,
);
if
(
result
!=
null
)
{
_response
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Something went wrong:
$e
"
;
debugPrint
(
'Error fetching leave application details:
$e
'
);
}
_isLoading
=
false
;
notifyListeners
();
}
/// Clear the current response data
void
clearData
()
{
_response
=
null
;
_errorMessage
=
null
;
notifyListeners
();
}
}
\ No newline at end of file
lib/Notifiers/hrmProvider/attendanceListProvider.dart
View file @
f6fbe101
import
'dart:io'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/attendanceRequestListResponse.dart'
;
import
'../../Utils/SharedpreferencesService.dart'
;
import
'../../Models/ordersModels/commonResponse.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
Attendancelistprovider
extends
ChangeNotifier
{
attendanceRequestListResponse
?
_response
;
bool
_isLoading
=
false
;
...
...
@@ -18,8 +20,114 @@ class Attendancelistprovider extends ChangeNotifier {
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
/// fetch aattendance request list
Future
<
void
>
fetchAttendanceRequests
(
context
,
String
type
,
String
from
,
String
to
)
async
{
// Filter states
String
_selectedType
=
"All"
;
String
_selectedDateRange
=
"This Month"
;
DateTimeRange
?
_customDateRange
;
String
get
selectedType
=>
_selectedType
;
String
get
selectedDateRange
=>
_selectedDateRange
;
DateTimeRange
?
get
customDateRange
=>
_customDateRange
;
// Addition of attendance
CommonResponse
?
_addResponse
;
CommonResponse
?
get
addResponse
=>
_addResponse
;
bool
_isSubmitting
=
false
;
bool
get
isSubmitting
=>
_isSubmitting
;
// Date controllers for filter UI
final
TextEditingController
fromDateController
=
TextEditingController
();
final
TextEditingController
toDateController
=
TextEditingController
();
// For manual attendance date field
final
TextEditingController
dateController
=
TextEditingController
();
DateTime
?
_selectedDate
;
DateTime
?
get
selectedDate
=>
_selectedDate
;
// Type options for filter
final
List
<
String
>
typeOptions
=
[
"All"
,
"Check In"
,
"Check Out"
,
"Check In/Out"
];
// Date range options for filter
final
List
<
String
>
dateRangeOptions
=
[
"All"
,
"Today"
,
"Yesterday"
,
"This Month"
,
"Past 7 days"
,
"Last Month"
,
"Custom"
,
];
bool
isDateValid
()
{
if
(
_selectedDate
==
null
)
return
false
;
DateTime
today
=
DateTime
.
now
();
DateTime
yesterday
=
today
.
subtract
(
const
Duration
(
days:
1
));
// normalize (remove time part)
DateTime
normalizedSelected
=
DateTime
(
_selectedDate
!.
year
,
_selectedDate
!.
month
,
_selectedDate
!.
day
);
DateTime
normalizedToday
=
DateTime
(
today
.
year
,
today
.
month
,
today
.
day
);
DateTime
normalizedYesterday
=
DateTime
(
yesterday
.
year
,
yesterday
.
month
,
yesterday
.
day
);
return
normalizedSelected
==
normalizedToday
||
normalizedSelected
==
normalizedYesterday
;
}
String
?
validateManualAttendance
({
//its working or not
required
String
type
,
required
String
?
checkInTime
,
required
String
?
checkInLoc
,
required
File
?
checkInProof
,
required
String
?
checkOutTime
,
required
String
?
checkOutLoc
,
required
File
?
checkOutProof
,
required
String
?
checkInDesc
,
required
String
?
checkOutDesc
,
})
{
if
(!
isDateValid
())
{
return
"Date must be today or yesterday"
;
}
if
(
type
.
isEmpty
)
return
"Please select type"
;
if
(
type
==
"Check In"
)
{
if
((
checkInTime
??
""
).
isEmpty
||
(
checkInLoc
??
""
).
isEmpty
||
(
checkInDesc
??
""
).
isEmpty
||
checkInProof
==
null
)
{
return
"Please fill all Check In fields"
;
}
}
if
(
type
==
"Check Out"
)
{
if
((
checkOutTime
??
""
).
isEmpty
||
(
checkOutLoc
??
""
).
isEmpty
||
(
checkOutDesc
??
""
).
isEmpty
||
checkOutProof
==
null
)
{
return
"Please fill all Check Out fields"
;
}
}
if
(
type
==
"Check In/Out"
)
{
if
((
checkInTime
??
""
).
isEmpty
||
(
checkInLoc
??
""
).
isEmpty
||
(
checkInDesc
??
""
).
isEmpty
||
checkInProof
==
null
||
(
checkOutTime
??
""
).
isEmpty
||
(
checkOutLoc
??
""
).
isEmpty
||
(
checkOutDesc
??
""
).
isEmpty
||
checkOutProof
==
null
)
{
return
"Please fill all Check In & Check Out fields"
;
}
}
return
null
;
// everything ok
}
/// Fetch attendance request list with filters
Future
<
void
>
fetchAttendanceRequests
(
BuildContext
context
,
{
String
?
type
,
String
?
dateRange
,
DateTimeRange
?
customRange
})
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
...
...
@@ -27,36 +135,234 @@ class Attendancelistprovider extends ChangeNotifier {
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
attendanceRequestListAPI
(
provider
.
empId
,
provider
.
session
,
type
,
from
,
to
);
debugPrint
(
'empId:
${provider.empId}
, session:
${provider.requestId}
'
);
// Update filter states if provided
if
(
type
!=
null
)
_selectedType
=
type
;
if
(
dateRange
!=
null
)
_selectedDateRange
=
dateRange
;
if
(
customRange
!=
null
)
_customDateRange
=
customRange
;
// Calculate date range based on selection
final
dateParams
=
_getDateRangeParams
(
_selectedDateRange
,
_customDateRange
);
// Convert "All" type to empty string for API
final
apiType
=
_selectedType
==
"All"
?
""
:
_selectedType
;
final
result
=
await
ApiCalling
.
attendanceRequestListAPI
(
provider
.
empId
,
provider
.
session
,
apiType
,
dateParams
[
'from'
]!,
dateParams
[
'to'
]!,
);
debugPrint
(
'Fetching attendance from:
${dateParams['from']}
to:
${dateParams['to']}
'
);
if
(
result
!=
null
)
{
_response
=
result
;
if
(
_response
?.
requestList
==
null
||
_response
!.
requestList
!.
isEmpty
)
{
_errorMessage
=
"No attendance records found!"
;
}
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
debugPrint
(
'Error fetching attendance:
$e
'
);
}
_isLoading
=
false
;
notifyListeners
();
}
DateTime
?
_date
;
final
TextEditingController
dateController
=
TextEditingController
();
/// --- Add Attendance Request ---
Future
<
void
>
addAttendanceRequest
(
BuildContext
context
,
{
required
String
process
,
required
String
type
,
required
String
loc
,
required
String
checkDate
,
String
?
checkInTime
,
String
?
checkInLoc
,
File
?
checkInProof
,
String
?
checkOutTime
,
String
?
checkOutLoc
,
File
?
checkOutProof
,
String
?
note
,
})
async
{
_isSubmitting
=
true
;
_errorMessage
=
null
;
_addResponse
=
null
;
notifyListeners
();
void
setDate
(
DateTime
newDate
)
{
_date
=
newDate
;
dateController
.
text
=
"
${newDate.day}
-
${newDate.month}
-
${newDate.year}
"
;
try
{
final
homeProvider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
addAttendanceRequestAPI
(
sessionId:
homeProvider
.
session
,
empId:
homeProvider
.
empId
,
process:
process
,
type:
type
,
loc:
loc
,
checkDate:
checkDate
,
checkInTime:
checkInTime
,
checkInLoc:
checkInLoc
,
checkInProof:
checkInProof
,
checkOutTime:
checkOutTime
,
checkOutLoc:
checkOutLoc
,
checkOutProof:
checkOutProof
,
note:
note
,
);
if
(
result
!=
null
)
{
_addResponse
=
result
;
if
(
result
.
error
!=
null
&&
result
.
error
!.
isNotEmpty
)
{
_errorMessage
=
result
.
error
;
}
else
{
_addResponse
=
result
;
}
}
else
{
_errorMessage
=
"Failed to submit request!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error submitting request:
$e
"
;
}
_isSubmitting
=
false
;
notifyListeners
();
}
/// Apply filters coming from bottom sheet
void
updateFiltersFromSheet
(
BuildContext
context
,
{
required
String
type
,
required
String
selectedValue
,
DateTimeRange
?
customRange
,
})
{
_selectedType
=
type
;
_selectedDateRange
=
selectedValue
;
_customDateRange
=
customRange
;
fetchAttendanceRequests
(
context
,
type:
_selectedType
,
dateRange:
_selectedDateRange
,
customRange:
_customDateRange
,
);
}
/// Set type filter and refresh data
void
setTypeFilter
(
BuildContext
context
,
String
type
)
{
_selectedType
=
type
;
fetchAttendanceRequests
(
context
);
}
/// Set date range filter and refresh data
void
setDateRangeFilter
(
BuildContext
context
,
String
dateRange
,
{
DateTimeRange
?
customRange
})
{
_selectedDateRange
=
dateRange
;
if
(
customRange
!=
null
)
{
_customDateRange
=
customRange
;
fromDateController
.
text
=
_formatDate
(
customRange
.
start
);
toDateController
.
text
=
_formatDate
(
customRange
.
end
);
}
fetchAttendanceRequests
(
context
);
}
/// Clear all filters and refresh data
void
clearFilters
(
BuildContext
context
)
{
_selectedType
=
"All"
;
_selectedDateRange
=
"This Month"
;
_customDateRange
=
null
;
fromDateController
.
clear
();
toDateController
.
clear
();
fetchAttendanceRequests
(
context
);
}
/// Reset form and data
void
resetForm
(
BuildContext
context
)
{
_response
=
null
;
_errorMessage
=
null
;
clearFilters
(
context
);
notifyListeners
();
}
/// Get date range parameters for API
Map
<
String
,
String
>
_getDateRangeParams
(
String
dateRange
,
DateTimeRange
?
customRange
)
{
final
now
=
DateTime
.
now
();
final
formatter
=
DateFormat
(
"dd MMM yyyy"
);
late
DateTime
from
;
late
DateTime
to
;
switch
(
dateRange
)
{
case
"All"
:
from
=
DateTime
(
now
.
year
-
1
);
to
=
now
;
break
;
case
"Today"
:
from
=
now
;
to
=
now
;
break
;
case
"Yesterday"
:
from
=
now
.
subtract
(
const
Duration
(
days:
1
));
to
=
now
.
subtract
(
const
Duration
(
days:
1
));
break
;
case
"This Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
+
1
,
0
);
break
;
case
"Past 7 days"
:
from
=
now
.
subtract
(
const
Duration
(
days:
6
));
to
=
now
;
break
;
case
"Last Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
-
1
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
,
0
);
break
;
case
"Custom"
:
if
(
customRange
!=
null
)
{
from
=
customRange
.
start
;
to
=
customRange
.
end
;
}
else
{
from
=
now
.
subtract
(
const
Duration
(
days:
30
));
to
=
now
;
}
break
;
default
:
from
=
now
;
to
=
now
;
}
return
{
"from"
:
formatter
.
format
(
from
),
"to"
:
formatter
.
format
(
to
),
};
}
/// Format date for display
String
_formatDate
(
DateTime
date
)
{
return
DateFormat
(
"dd MMM yyyy"
).
format
(
date
);
}
/// Apply filters and refresh data
void
applyFilters
(
BuildContext
context
)
{
fetchAttendanceRequests
(
context
,
type:
_selectedType
,
dateRange:
_selectedDateRange
,
customRange:
_customDateRange
,
);
}
/// Set Selected Date
void
setSelectedDate
(
DateTime
date
)
{
_selectedDate
=
date
;
dateController
.
text
=
DateFormat
(
"dd MMM yyyy"
).
format
(
date
);
notifyListeners
();
}
/// Show Cupertino Date Picker
void
showDatePickerDialog
(
BuildContext
context
)
{
if
(
_
d
ate
==
null
)
{
setDate
(
DateTime
.
now
());
if
(
_
selectedD
ate
==
null
)
{
set
Selected
Date
(
DateTime
.
now
());
}
showCupertinoModalPopup
<
void
>(
...
...
@@ -72,36 +378,48 @@ class Attendancelistprovider extends ChangeNotifier {
top:
false
,
child:
Column
(
children:
[
// Cancel + Done Buttons
SizedBox
(
height:
40
,
height:
55
,
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
CupertinoButton
(
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
{
Navigator
.
pop
(
context
);
},
child:
Text
(
'Cancel'
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
CupertinoButton
(
child:
const
Text
(
"Done"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
child:
Text
(
'Done'
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
onPressed:
()
{
set
Date
(
_d
ate
??
DateTime
.
now
());
set
SelectedDate
(
_selectedD
ate
??
DateTime
.
now
());
Navigator
.
pop
(
context
);
},
),
],
),
),
// Cupertino Date Picker
Expanded
(
child:
CupertinoDatePicker
(
dateOrder:
DatePickerDateOrder
.
dmy
,
initialDateTime:
_
d
ate
??
DateTime
.
now
(),
initialDateTime:
_
selectedD
ate
??
DateTime
.
now
(),
mode:
CupertinoDatePickerMode
.
date
,
use24hFormat:
true
,
showDayOfWeek:
true
,
onDateTimeChanged:
(
DateTime
newDate
)
{
_date
=
newDate
;
// temp update
setSelectedDate
(
newDate
);
},
),
),
...
...
lib/Notifiers/hrmProvider/leaveApplicationListProvider.dart
0 → 100644
View file @
f6fbe101
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:generp/Models/ordersModels/commonResponse.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/leaveApplicationLIstResponse.dart'
;
import
'../../services/api_calling.dart'
;
import
'../HomeScreenNotifier.dart'
;
class
LeaveApplicationListProvider
extends
ChangeNotifier
{
leaveApplicationLIstResponse
?
_response
;
bool
_isLoading
=
false
;
String
?
_errorMessage
;
leaveApplicationLIstResponse
?
get
response
=>
_response
;
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
// Filter states
String
_selectedStatus
=
"All"
;
String
_selectedDateRange
=
"This Month"
;
DateTimeRange
?
_customDateRange
;
String
get
selectedStatus
=>
_selectedStatus
;
String
get
selectedDateRange
=>
_selectedDateRange
;
DateTimeRange
?
get
customDateRange
=>
_customDateRange
;
// Date controllers for filter UI
final
TextEditingController
fromDateController
=
TextEditingController
();
final
TextEditingController
toDateController
=
TextEditingController
();
// Controllers for Add Leave form
final
TextEditingController
fromDateField
=
TextEditingController
();
final
TextEditingController
toDateField
=
TextEditingController
();
final
TextEditingController
fromTimeField
=
TextEditingController
();
final
TextEditingController
toTimeField
=
TextEditingController
();
final
TextEditingController
reasonController
=
TextEditingController
();
// Status options for filter
final
List
<
String
>
statusOptions
=
[
"All"
,
"Requested"
,
"Approved"
,
"Rejected"
];
// Date range options for filter
final
List
<
String
>
dateRangeOptions
=
[
"All"
,
"Today"
,
"Yesterday"
,
"This Month"
,
"Past 7 days"
,
"Last Month"
,
"Custom"
,
];
// Loading state for Add Leave
bool
_isSubmitting
=
false
;
bool
get
isSubmitting
=>
_isSubmitting
;
CommonResponse
?
_addResponse
;
CommonResponse
?
get
addResponse
=>
_addResponse
;
DateTime
?
_selectedDate
;
DateTime
?
get
selectedDate
=>
_selectedDate
;
/// Fetch leave application list with filters
Future
<
void
>
fetchLeaveApplications
(
BuildContext
context
,
{
String
?
status
,
String
?
dateRange
,
DateTimeRange
?
customRange
})
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
filterStatus
=
status
??
_selectedStatus
;
final
filterDateRange
=
dateRange
??
_selectedDateRange
;
final
filterCustomRange
=
customRange
??
_customDateRange
;
final
dateParams
=
_getDateRangeParams
(
filterDateRange
,
filterCustomRange
);
final
result
=
await
ApiCalling
.
leaveApplicationListAPI
(
provider
.
session
,
provider
.
empId
,
dateParams
[
'from'
]!,
dateParams
[
'to'
]!,
);
debugPrint
(
'Fetching leave applications from:
${dateParams['from']}
to:
${dateParams['to']}
'
);
if
(
result
!=
null
)
{
_response
=
result
;
if
(
filterStatus
!=
"All"
&&
_response
?.
requestList
!=
null
)
{
_response
!.
requestList
=
_response
!.
requestList
!
.
where
((
item
)
=>
item
.
status
?.
toLowerCase
()
==
filterStatus
.
toLowerCase
())
.
toList
();
}
if
(
_response
?.
requestList
==
null
||
_response
!.
requestList
!.
isEmpty
)
{
_errorMessage
=
"No leave applications found!"
;
}
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
debugPrint
(
'Error fetching leave applications:
$e
'
);
}
_isLoading
=
false
;
notifyListeners
();
}
/// --- Add Leave Request ---
Future
<
void
>
addLeaveRequest
(
BuildContext
context
,
{
required
String
fromDate
,
required
String
fromTime
,
required
String
toDate
,
required
String
toTime
,
required
String
leaveType
,
required
String
reason
,
})
async
{
_isSubmitting
=
true
;
_errorMessage
=
null
;
_addResponse
=
null
;
notifyListeners
();
try
{
final
homeProvider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
leaveRequestAddAPI
(
homeProvider
.
session
,
homeProvider
.
empId
,
fromDate
,
fromTime
,
toDate
,
toTime
,
leaveType
,
reason
,
);
if
(
result
!=
null
)
{
_addResponse
=
result
;
if
(
result
.
error
!=
null
&&
result
.
error
!.
isNotEmpty
)
{
_errorMessage
=
result
.
error
;
}
}
else
{
_errorMessage
=
"Failed to submit leave request!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error submitting leave request:
$e
"
;
}
_isSubmitting
=
false
;
notifyListeners
();
}
/// Set status filter
void
setStatusFilter
(
String
status
)
{
_selectedStatus
=
status
;
notifyListeners
();
}
/// Set date range filter
void
setDateRangeFilter
(
String
dateRange
,
{
DateTimeRange
?
customRange
})
{
_selectedDateRange
=
dateRange
;
if
(
customRange
!=
null
)
{
_customDateRange
=
customRange
;
fromDateController
.
text
=
_formatDate
(
customRange
.
start
);
toDateController
.
text
=
_formatDate
(
customRange
.
end
);
}
notifyListeners
();
}
/// Clear all filters
void
clearFilters
()
{
_selectedStatus
=
"All"
;
_selectedDateRange
=
"This Month"
;
_customDateRange
=
null
;
fromDateController
.
clear
();
toDateController
.
clear
();
notifyListeners
();
}
/// Reset form and data
void
resetForm
()
{
_response
=
null
;
_errorMessage
=
null
;
clearFilters
();
notifyListeners
();
}
/// Get date range parameters for API
Map
<
String
,
String
>
_getDateRangeParams
(
String
dateRange
,
DateTimeRange
?
customRange
)
{
final
now
=
DateTime
.
now
();
final
formatter
=
DateFormat
(
"dd MMM yyyy"
);
late
DateTime
from
;
late
DateTime
to
;
switch
(
dateRange
)
{
case
"All"
:
from
=
DateTime
(
now
.
year
-
1
);
to
=
now
;
break
;
case
"Today"
:
from
=
now
;
to
=
now
;
break
;
case
"Yesterday"
:
from
=
now
.
subtract
(
const
Duration
(
days:
1
));
to
=
now
.
subtract
(
const
Duration
(
days:
1
));
break
;
case
"This Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
+
1
,
0
);
break
;
case
"Past 7 days"
:
from
=
now
.
subtract
(
const
Duration
(
days:
6
));
to
=
now
;
break
;
case
"Last Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
-
1
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
,
0
);
break
;
case
"Custom"
:
if
(
customRange
!=
null
)
{
from
=
customRange
.
start
;
to
=
customRange
.
end
;
}
else
{
from
=
now
.
subtract
(
const
Duration
(
days:
30
));
to
=
now
;
}
break
;
default
:
from
=
now
;
to
=
now
;
}
return
{
"from"
:
formatter
.
format
(
from
),
"to"
:
formatter
.
format
(
to
),
};
}
/// Format date
String
_formatDate
(
DateTime
date
)
{
return
DateFormat
(
"dd MMM yyyy"
).
format
(
date
);
}
/// Show Cupertino DatePicker for leave form
void
showDatePickerDialog
(
BuildContext
context
,
{
bool
isFromDate
=
true
})
{
DateTime
?
currentDate
=
DateTime
.
now
();
showCupertinoModalPopup
<
void
>(
context:
context
,
builder:
(
BuildContext
context
)
=>
Container
(
height:
250
,
padding:
const
EdgeInsets
.
only
(
top:
6.0
),
margin:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
color:
CupertinoColors
.
systemBackground
.
resolveFrom
(
context
),
child:
SafeArea
(
top:
false
,
child:
Column
(
children:
[
SizedBox
(
height:
55
,
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
CupertinoButton
(
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
CupertinoButton
(
child:
const
Text
(
"Done"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
{
if
(
isFromDate
)
{
fromDateField
.
text
=
_formatDate
(
currentDate
??
DateTime
.
now
());
}
else
{
toDateField
.
text
=
_formatDate
(
currentDate
??
DateTime
.
now
());
}
Navigator
.
pop
(
context
);
},
),
],
),
),
Expanded
(
child:
CupertinoDatePicker
(
dateOrder:
DatePickerDateOrder
.
dmy
,
initialDateTime:
currentDate
,
mode:
CupertinoDatePickerMode
.
date
,
onDateTimeChanged:
(
DateTime
newDate
)
{
currentDate
=
newDate
;
},
),
),
],
),
),
),
);
}
/// Apply filters
void
applyFilters
(
BuildContext
context
)
{
fetchLeaveApplications
(
context
,
status:
_selectedStatus
,
dateRange:
_selectedDateRange
,
customRange:
_customDateRange
,
);
}
/// Export
List
<
List
<
String
>>
prepareExportData
()
{
final
headers
=
[
'ID'
,
'Applied Date'
,
'From Date'
,
'To Date'
,
'Leave Type'
,
'Status'
,
'Reason'
,
];
if
(
_response
?.
requestList
==
null
)
{
return
[
headers
];
}
final
rows
=
_response
!.
requestList
!.
map
((
item
)
=>
[
item
.
id
??
''
,
item
.
appliedDate
??
''
,
item
.
fromPeriod
??
''
,
item
.
toPeriod
??
''
,
''
,
// leave type if available
item
.
status
??
''
,
''
,
// reason if available
]).
toList
();
return
[
headers
,
...
rows
];
}
/// Set selected single date (if needed)
void
setSelectedDate
(
DateTime
date
)
{
_selectedDate
=
date
;
notifyListeners
();
}
}
lib/Notifiers/hrmProvider/tourExpensesProvider.dart
View file @
f6fbe101
import
'dart:io'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:generp/Models/hrmModels/tourExpensesAddViewResponse.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Models/hrmModels/tourExpensesListResponse.dart'
;
...
...
@@ -15,6 +19,37 @@ class TourExpensesProvider extends ChangeNotifier {
bool
get
isLoading
=>
_isLoading
;
String
?
get
errorMessage
=>
_errorMessage
;
tourExpensesAddViewResponse
?
_response2
;
tourExpensesAddViewResponse
?
get
response2
=>
_response2
;
List
<
String
>
get
daAmountList
=>
_response2
?.
daAmount
?.
map
((
e
)
=>
e
.
toString
()).
toList
()
??
[];
List
<
String
>
get
tourTypeList
=>
_response2
?.
tourType
??
[];
List
<
String
>
get
travelTypeList
=>
_response2
?.
travelType
??
[];
// Controllers for Add form
final
TextEditingController
fromDateField
=
TextEditingController
();
final
TextEditingController
toDateField
=
TextEditingController
();
final
TextEditingController
dateController
=
TextEditingController
();
DateTime
?
_date
;
DateTime
?
_fromDate
;
DateTime
?
_toDate
;
/// Format date (yyyy-MM-dd)
String
_formatDate
(
DateTime
date
)
{
return
DateFormat
(
'yyyy-MM-dd'
).
format
(
date
);
}
/// Set single date
void
setDate
(
DateTime
newDate
)
{
_date
=
newDate
;
dateController
.
text
=
_formatDate
(
newDate
);
notifyListeners
();
}
/// Fetch tour expenses list
Future
<
void
>
fetchTourExpenses
(
BuildContext
context
,
String
pageNumber
)
async
{
_isLoading
=
true
;
...
...
@@ -45,21 +80,115 @@ class TourExpensesProvider extends ChangeNotifier {
notifyListeners
();
}
DateTime
?
_date
;
final
TextEditingController
dateController
=
TextEditingController
();
Future
<
void
>
fetchTourExpensesAddView
(
BuildContext
context
,
String
tourBillId
)
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
void
setDate
(
DateTime
newDate
)
{
_date
=
newDate
;
dateController
.
text
=
"
${newDate.day}
-
${newDate.month}
-
${newDate.year}
"
;
try
{
final
provider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
final
result
=
await
ApiCalling
.
tourExpensesAddViewAPI
(
provider
.
empId
,
provider
.
session
,
tourBillId
,
);
debugPrint
(
'empId:
${provider.empId}
, session:
${provider.session}
, tourBillId:
$tourBillId
'
);
if
(
result
!=
null
)
{
_response2
=
result
;
}
else
{
_errorMessage
=
"No data found!"
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
}
_isLoading
=
false
;
notifyListeners
();
}
void
showDatePickerDialog
(
BuildContext
context
)
{
if
(
_date
==
null
)
{
setDate
(
DateTime
.
now
());
Future
<
bool
>
addTourBill
({
required
BuildContext
context
,
required
String
placeOfVisit
,
required
String
daAmount
,
required
String
tourType
,
required
String
tourDate
,
required
List
<
Map
<
String
,
dynamic
>>
travelExpenses
,
required
List
<
Map
<
String
,
dynamic
>>
hotelExpenses
,
required
List
<
Map
<
String
,
dynamic
>>
otherExpenses
,
List
<
File
>?
travelImages
,
List
<
File
>?
hotelImages
,
List
<
File
>?
otherImages
,
})
async
{
_isLoading
=
true
;
_errorMessage
=
null
;
notifyListeners
();
try
{
final
homeProvider
=
Provider
.
of
<
HomescreenNotifier
>(
context
,
listen:
false
);
if
((
homeProvider
.
session
??
""
).
isEmpty
||
(
homeProvider
.
empId
??
""
).
isEmpty
)
{
_errorMessage
=
"Invalid session or employee ID"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
showCupertinoModalPopup
<
void
>(
debugPrint
(
"Submitting Tour Bill => "
"place:
$placeOfVisit
, da:
$daAmount
, type:
$tourType
, "
"date:
$tourDate
, travelExp:
${travelExpenses.length}
, "
"hotelExp:
${hotelExpenses.length}
, "
"otherExp:
${otherExpenses.length}
, "
"travelImages:
${travelImages?.length}
, "
"hotelImages:
${hotelImages?.length}
,"
"otherImages:
${otherImages?.length}
"
);
final
result
=
await
ApiCalling
.
addTourBillAPI
(
sessionId:
homeProvider
.
session
??
""
,
empId:
homeProvider
.
empId
??
""
,
placeOfVisit:
placeOfVisit
,
daAmount:
daAmount
,
tourType:
tourType
,
tourDate:
tourDate
,
travelExpenses:
travelExpenses
,
hotelExpenses:
hotelExpenses
,
otherExpenses:
otherExpenses
,
travelImages:
travelImages
,
hotelImages:
hotelImages
,
otherImages:
otherImages
,
);
if
(
result
!=
null
)
{
debugPrint
(
" Tour Bill Added Successfully"
);
_isLoading
=
false
;
notifyListeners
();
return
true
;
}
else
{
_errorMessage
=
"Failed to add Tour Bill"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
}
catch
(
e
)
{
_errorMessage
=
"Error:
$e
"
;
_isLoading
=
false
;
notifyListeners
();
return
false
;
}
}
/// Show Cupertino DatePicker for leave form
/// Show Cupertino DatePicker for leave form
Future
<
DateTime
?>
showDatePickerDialog
(
BuildContext
context
,
{
bool
isFromDate
=
true
})
async
{
DateTime
currentDate
=
DateTime
.
now
();
DateTime
?
pickedDate
;
await
showCupertinoModalPopup
<
void
>(
context:
context
,
builder:
(
BuildContext
context
)
=>
Container
(
height:
250
,
...
...
@@ -73,18 +202,25 @@ class TourExpensesProvider extends ChangeNotifier {
child:
Column
(
children:
[
SizedBox
(
height:
40
,
height:
55
,
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
CupertinoButton
(
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
CupertinoButton
(
child:
const
Text
(
"Done"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
child:
const
Text
(
"Done"
,
style:
TextStyle
(
color:
Colors
.
blue
)),
onPressed:
()
{
setDate
(
_date
??
DateTime
.
now
());
pickedDate
=
currentDate
;
if
(
isFromDate
)
{
fromDateField
.
text
=
_formatDate
(
pickedDate
!);
}
else
{
toDateField
.
text
=
_formatDate
(
pickedDate
!);
}
Navigator
.
pop
(
context
);
},
),
...
...
@@ -94,10 +230,10 @@ class TourExpensesProvider extends ChangeNotifier {
Expanded
(
child:
CupertinoDatePicker
(
dateOrder:
DatePickerDateOrder
.
dmy
,
initialDateTime:
_date
??
DateTime
.
now
()
,
initialDateTime:
currentDate
,
mode:
CupertinoDatePickerMode
.
date
,
onDateTimeChanged:
(
DateTime
newDate
)
{
_d
ate
=
newDate
;
currentD
ate
=
newDate
;
},
),
),
...
...
@@ -106,5 +242,9 @@ class TourExpensesProvider extends ChangeNotifier {
),
),
);
return
pickedDate
;
}
}
lib/main.dart
View file @
f6fbe101
...
...
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import
'package:flutter/services.dart'
;
import
'package:flutter_local_notifications/flutter_local_notifications.dart'
;
import
'package:flutter_ringtone_player/flutter_ringtone_player.dart'
;
import
'package:generp/Models/hrmModels/leaveApplicationLIstResponse.dart'
;
import
'package:generp/Utils/app_colors.dart'
;
import
'screens/notifierExports.dart'
;
import
'package:generp/Utils/SharedpreferencesService.dart'
;
...
...
@@ -231,6 +232,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider
(
create:
(
_
)
=>
TourExpensesProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
TourExpensesDetailsProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
RewardListProvider
()),
ChangeNotifierProvider
(
create:
(
_
)
=>
LeaveApplicationListProvider
()),
],
child:
Builder
(
builder:
(
BuildContext
context
)
{
...
...
lib/screens/CommonFilter2.dart
0 → 100644
View file @
f6fbe101
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'../Utils/app_colors.dart'
;
import
'../Utils/dropdownTheme.dart'
;
class
CommonFilter2
{
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
List
<
String
>
filterItems
=
[
'Today'
,
'Yesterday'
,
'This Month'
,
'Past 7 days'
,
'Last Month'
,
'Custom'
,
];
final
List
<
String
>
typeItems
=
[
"All"
,
"Check In"
,
"Check Out"
,
"Check In/Out"
,
];
String
?
selectedValue
;
// Date range value
DateTimeRange
?
selectedDateRange
;
String
?
selectedType
;
// Type value
// Get DateTimeRange based on filter selection
DateTimeRange
?
getDateRange
(
String
filter
)
{
DateTime
now
=
DateTime
.
now
();
switch
(
filter
)
{
case
'Today'
:
return
DateTimeRange
(
start:
DateTime
(
now
.
year
,
now
.
month
,
now
.
day
),
end:
DateTime
(
now
.
year
,
now
.
month
,
now
.
day
),
);
case
'Yesterday'
:
DateTime
yesterday
=
now
.
subtract
(
const
Duration
(
days:
1
));
return
DateTimeRange
(
start:
DateTime
(
yesterday
.
year
,
yesterday
.
month
,
yesterday
.
day
),
end:
DateTime
(
yesterday
.
year
,
yesterday
.
month
,
yesterday
.
day
),
);
case
'This Month'
:
return
DateTimeRange
(
start:
DateTime
(
now
.
year
,
now
.
month
,
1
),
end:
DateTime
(
now
.
year
,
now
.
month
+
1
,
0
),
);
case
'Past 7 days'
:
return
DateTimeRange
(
start:
now
.
subtract
(
const
Duration
(
days:
6
)),
end:
DateTime
(
now
.
year
,
now
.
month
,
now
.
day
),
);
case
'Last Month'
:
return
DateTimeRange
(
start:
DateTime
(
now
.
year
,
now
.
month
-
1
,
1
),
end:
DateTime
(
now
.
year
,
now
.
month
,
0
),
);
case
'Custom'
:
return
null
;
default
:
return
null
;
}
}
// Format a single DateTime to string
String
formatDate
(
DateTime
date
)
{
return
"
${date.year}
-
${date.month.toString().padLeft(2, '0')}
-
${date.day.toString().padLeft(2, '0')}
"
;
}
// Get formatted date range as a list of strings
List
<
String
>
getFormattedDateRange
(
DateTimeRange
?
dateRange
)
{
if
(
dateRange
!=
null
)
{
return
[
formatDate
(
dateRange
.
start
),
formatDate
(
dateRange
.
end
),
];
}
return
[];
}
Future
<
Map
<
String
,
dynamic
>?>
showFilterBottomSheet
(
BuildContext
context
)
async
{
String
?
tempSelectedValue
=
selectedValue
;
DateTimeRange
?
tempSelectedDateRange
=
selectedDateRange
;
DateTime
?
tempStartDate
;
DateTime
?
tempEndDate
;
DateTime
displayedMonth
=
DateTime
.
now
();
String
?
tempSelectedType
=
selectedType
??
"All"
;
Widget
buildCalendar
(
StateSetter
setState
)
{
final
firstDayOfMonth
=
DateTime
(
displayedMonth
.
year
,
displayedMonth
.
month
,
1
);
final
lastDayOfMonth
=
DateTime
(
displayedMonth
.
year
,
displayedMonth
.
month
+
1
,
0
);
final
firstDayOfWeek
=
firstDayOfMonth
.
weekday
;
final
daysInMonth
=
lastDayOfMonth
.
day
;
final
daysBefore
=
(
firstDayOfWeek
-
1
)
%
7
;
List
<
Widget
>
dayWidgets
=
[];
final
weekdays
=
[
'Mon'
,
'Tue'
,
'Wed'
,
'Thu'
,
'Fri'
,
'Sat'
,
'Sun'
];
dayWidgets
.
addAll
(
weekdays
.
map
((
day
)
=>
Center
(
child:
Text
(
day
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
Colors
.
grey
[
700
],
),
),
)));
for
(
int
i
=
0
;
i
<
daysBefore
;
i
++)
{
dayWidgets
.
add
(
Container
());
}
for
(
int
day
=
1
;
day
<=
daysInMonth
;
day
++)
{
final
currentDate
=
DateTime
(
displayedMonth
.
year
,
displayedMonth
.
month
,
day
);
bool
isSelected
=
false
;
bool
isInRange
=
false
;
bool
isOutsideRange
=
currentDate
.
isBefore
(
DateTime
(
2020
))
||
currentDate
.
isAfter
(
DateTime
(
2100
));
if
(
tempStartDate
!=
null
&&
tempEndDate
!=
null
)
{
isSelected
=
currentDate
.
isAtSameMomentAs
(
tempStartDate
!)
||
currentDate
.
isAtSameMomentAs
(
tempEndDate
!);
isInRange
=
currentDate
.
isAfter
(
tempStartDate
!)
&&
currentDate
.
isBefore
(
tempEndDate
!)
&&
!
isSelected
;
}
else
if
(
tempStartDate
!=
null
)
{
isSelected
=
currentDate
.
isAtSameMomentAs
(
tempStartDate
!);
}
dayWidgets
.
add
(
GestureDetector
(
onTap:
isOutsideRange
?
null
:
()
{
setState
(()
{
if
(
tempStartDate
==
null
)
{
tempStartDate
=
currentDate
;
tempSelectedDateRange
=
null
;
}
else
if
(
tempEndDate
==
null
)
{
if
(
currentDate
.
isBefore
(
tempStartDate
!))
{
tempEndDate
=
tempStartDate
;
tempStartDate
=
currentDate
;
}
else
{
tempEndDate
=
currentDate
;
}
tempSelectedDateRange
=
DateTimeRange
(
start:
tempStartDate
!,
end:
tempEndDate
!);
}
else
{
tempStartDate
=
currentDate
;
tempEndDate
=
null
;
tempSelectedDateRange
=
null
;
}
});
},
child:
Container
(
margin:
const
EdgeInsets
.
all
(
2
),
decoration:
BoxDecoration
(
color:
isSelected
?
Colors
.
blue
[
600
]
:
isInRange
?
Colors
.
blue
[
100
]
:
null
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
Text
(
'
$day
'
,
style:
TextStyle
(
color:
isOutsideRange
?
Colors
.
grey
[
400
]
:
isSelected
?
Colors
.
white
:
Colors
.
black
,
fontWeight:
isSelected
?
FontWeight
.
bold
:
FontWeight
.
normal
,
),
),
),
),
),
);
}
return
Column
(
children:
[
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
GestureDetector
(
onTap:
displayedMonth
.
isAfter
(
DateTime
(
2020
))
?
()
{
setState
(()
{
displayedMonth
=
DateTime
(
displayedMonth
.
year
,
displayedMonth
.
month
-
1
);
});
}
:
null
,
child:
SvgPicture
.
asset
(
"assets/svg/arrow_left.svg"
),
),
Text
(
'
${_monthName(displayedMonth.month)}
${displayedMonth.year}
'
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w600
),
),
GestureDetector
(
onTap:
displayedMonth
.
isBefore
(
DateTime
(
2100
))
?
()
{
setState
(()
{
displayedMonth
=
DateTime
(
displayedMonth
.
year
,
displayedMonth
.
month
+
1
);
});
}
:
null
,
child:
SvgPicture
.
asset
(
"assets/svg/arrow_right_new.svg"
),
),
],
),
SizedBox
(
height:
280
,
child:
GridView
.
count
(
crossAxisCount:
7
,
childAspectRatio:
1.2
,
children:
dayWidgets
,
physics:
const
NeverScrollableScrollPhysics
(),
),
),
],
);
}
return
await
showModalBottomSheet
<
Map
<
String
,
dynamic
>>(
context:
context
,
isScrollControlled:
true
,
shape:
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
20
)),
),
builder:
(
BuildContext
context
)
{
return
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setState
)
{
return
SafeArea
(
child:
Padding
(
padding:
EdgeInsets
.
only
(
top:
16
,
left:
16
,
right:
16
,
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Filter Attendance'
,
style:
TextStyle
(
color:
AppColors
.
app_blue
,
fontSize:
18
,
fontFamily:
"JakartaSemiBold"
,
),
),
const
SizedBox
(
height:
20
),
/// Type filter
const
Text
(
"Type"
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
8
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
value:
tempSelectedType
,
items:
typeItems
.
map
((
type
)
=>
DropdownMenuItem
<
String
>(
value:
type
,
child:
Text
(
type
)))
.
toList
(),
onChanged:
(
value
)
{
setState
(()
{
tempSelectedType
=
value
;
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
const
SizedBox
(
height:
20
),
/// Date range filter
const
Text
(
"Date Range"
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
8
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
tempSelectedValue
??
'Select Item'
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w400
,
color:
Colors
.
black
,
),
),
items:
filterItems
.
map
((
String
item
)
=>
DropdownMenuItem
<
String
>(
value:
item
,
child:
Text
(
item
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w400
,
color:
Colors
.
black
,
),
)
)
)
.
toList
(),
value:
tempSelectedValue
,
onChanged:
(
String
?
value
)
{
if
(
value
==
null
)
return
;
setState
(()
{
tempSelectedValue
=
value
;
if
(
value
!=
'Custom'
)
{
tempSelectedDateRange
=
getDateRange
(
value
);
tempStartDate
=
null
;
tempEndDate
=
null
;
}
else
{
tempSelectedDateRange
=
null
;
tempStartDate
=
null
;
tempEndDate
=
null
;
}
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
if
(
tempSelectedValue
==
'Custom'
)
...[
const
SizedBox
(
height:
16
),
Container
(
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
[
300
]!),
borderRadius:
BorderRadius
.
circular
(
12
),
),
padding:
const
EdgeInsets
.
all
(
12
),
child:
buildCalendar
(
setState
),
),
if
(
tempSelectedDateRange
!=
null
)
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
12.0
),
child:
Text
(
'Selected:
${formatDate(tempSelectedDateRange!.start)}
to
${formatDate(tempSelectedDateRange!.end)}
'
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
[
600
]),
),
),
],
const
SizedBox
(
height:
20
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
end
,
children:
[
TextButton
(
onPressed:
()
=>
Navigator
.
pop
(
context
),
child:
Text
(
'Cancel'
,
style:
TextStyle
(
color:
Colors
.
grey
[
600
])),
),
const
SizedBox
(
width:
8
),
ElevatedButton
(
onPressed:
()
{
Navigator
.
pop
(
context
,
{
'type'
:
tempSelectedType
,
// if you store type separately
'selectedValue'
:
tempSelectedValue
,
// could be null
'dateRange'
:
tempSelectedDateRange
,
// could be null
'formatted'
:
tempSelectedDateRange
!=
null
?
getFormattedDateRange
(
tempSelectedDateRange
)
:
null
,
});
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
blue
[
600
],
foregroundColor:
Colors
.
white
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
),
),
child:
const
Text
(
'Apply'
),
),
],
),
],
),
),
),
);
},
);
},
);
}
String
_monthName
(
int
month
)
{
const
months
=
[
'January'
,
'February'
,
'March'
,
'April'
,
'May'
,
'June'
,
'July'
,
'August'
,
'September'
,
'October'
,
'November'
,
'December'
];
return
months
[
month
-
1
];
}
}
lib/screens/hrm/AddLeaveRequestScreen.dart
0 → 100644
View file @
f6fbe101
import
'dart:io'
;
import
'package:connectivity_plus/connectivity_plus.dart'
;
import
'package:flutter/material.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonServices.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
import
'../../Notifiers/hrmProvider/leaveApplicationListProvider.dart'
;
class
AddLeaveRequest
extends
StatefulWidget
{
final
String
pageTitleName
;
const
AddLeaveRequest
({
super
.
key
,
required
this
.
pageTitleName
});
@override
State
<
AddLeaveRequest
>
createState
()
=>
_AddLeaveRequestState
();
}
class
_AddLeaveRequestState
extends
State
<
AddLeaveRequest
>
{
Dropdowntheme
ddtheme
=
Dropdowntheme
();
List
<
FocusNode
>
focusNodes
=
List
.
generate
(
6
,
(
index
)
=>
FocusNode
());
Map
_source
=
{
ConnectivityResult
.
mobile
:
true
};
final
MyConnectivity
_connectivity
=
MyConnectivity
.
instance
;
String
?
leaveType
;
List
<
String
>
leaveTypes
=
[
"Normal"
,
"Medical"
];
// Validation error messages
String
?
fromDateError
;
String
?
fromTimeError
;
String
?
toDateError
;
String
?
toTimeError
;
String
?
leaveTypeError
;
String
?
reasonError
;
@override
void
initState
()
{
super
.
initState
();
_connectivity
.
initialise
();
_connectivity
.
myStream
.
listen
((
source
)
{
setState
(()
=>
_source
=
source
);
});
// Add listener to reason controller to clear error when user starts typing
final
provider
=
Provider
.
of
<
LeaveApplicationListProvider
>(
context
,
listen:
false
);
provider
.
reasonController
.
addListener
(()
{
if
(
reasonError
!=
null
&&
provider
.
reasonController
.
text
.
isNotEmpty
)
{
setState
(()
=>
reasonError
=
null
);
}
});
}
@override
void
dispose
()
{
focusNodes
.
map
((
e
)
=>
e
.
dispose
());
_connectivity
.
disposeStream
();
super
.
dispose
();
}
Future
<
bool
>
_onBackPressed
(
BuildContext
context
)
async
{
Navigator
.
pop
(
context
,
true
);
return
true
;
}
// Function to validate all fields
bool
validateForm
(
LeaveApplicationListProvider
provider
)
{
String
?
newFromDateError
=
provider
.
fromDateField
.
text
.
isEmpty
?
"From date is required"
:
null
;
String
?
newFromTimeError
=
provider
.
fromTimeField
.
text
.
isEmpty
?
"From time is required"
:
null
;
String
?
newToDateError
=
provider
.
toDateField
.
text
.
isEmpty
?
"To date is required"
:
null
;
String
?
newToTimeError
=
provider
.
toTimeField
.
text
.
isEmpty
?
"To time is required"
:
null
;
String
?
newLeaveTypeError
=
leaveType
==
null
?
"Leave type is required"
:
null
;
String
?
newReasonError
=
provider
.
reasonController
.
text
.
isEmpty
?
"Reason is required"
:
null
;
// Only update if there are actual changes to avoid unnecessary rebuilds
if
(
fromDateError
!=
newFromDateError
||
fromTimeError
!=
newFromTimeError
||
toDateError
!=
newToDateError
||
toTimeError
!=
newToTimeError
||
leaveTypeError
!=
newLeaveTypeError
||
reasonError
!=
newReasonError
)
{
setState
(()
{
fromDateError
=
newFromDateError
;
fromTimeError
=
newFromTimeError
;
toDateError
=
newToDateError
;
toTimeError
=
newToTimeError
;
leaveTypeError
=
newLeaveTypeError
;
reasonError
=
newReasonError
;
});
}
return
newFromDateError
==
null
&&
newFromTimeError
==
null
&&
newToDateError
==
null
&&
newToTimeError
==
null
&&
newLeaveTypeError
==
null
&&
newReasonError
==
null
;
}
@override
Widget
build
(
BuildContext
context
)
{
final
leaveProvider
=
Provider
.
of
<
LeaveApplicationListProvider
>(
context
);
switch
(
_source
.
keys
.
toList
()[
0
])
{
case
ConnectivityResult
.
mobile
:
case
ConnectivityResult
.
wifi
:
connection
=
'Online'
;
break
;
case
ConnectivityResult
.
none
:
default
:
connection
=
'Offline'
;
}
return
(
connection
==
"Online"
)
?
Platform
.
isAndroid
?
WillPopScope
(
onWillPop:
()
=>
_onBackPressed
(
context
),
child:
SafeArea
(
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
,
leaveProvider
)),
)
:
_scaffold
(
context
,
leaveProvider
)
:
NoNetwork
(
context
);
}
Widget
_scaffold
(
BuildContext
context
,
LeaveApplicationListProvider
provider
)
{
return
Scaffold
(
resizeToAvoidBottomInset:
true
,
backgroundColor:
AppColors
.
scaffold_bg_color
,
appBar:
appbarNew
(
context
,
widget
.
pageTitleName
,
0xFFFFFFFF
),
body:
SingleChildScrollView
(
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
margin:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
white
,
borderRadius:
BorderRadius
.
circular
(
20
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
/// From Date
TextWidget
(
context
,
"From Date"
),
GestureDetector
(
onTap:
()
{
provider
.
showDatePickerDialog
(
context
,
isFromDate:
true
);
if
(
fromDateError
!=
null
)
{
setState
(()
=>
fromDateError
=
null
);
}
},
child:
textFieldNew
(
context
,
provider
.
fromDateField
,
"Select Date"
,
enabled:
false
),
),
errorWidget
(
context
,
fromDateError
),
/// From Time
TextWidget
(
context
,
"From Time"
),
GestureDetector
(
onTap:
()
async
{
TimeOfDay
?
picked
=
await
showTimePicker
(
context:
context
,
initialTime:
TimeOfDay
.
now
(),
);
if
(
picked
!=
null
)
{
provider
.
fromTimeField
.
text
=
picked
.
format
(
context
);
if
(
fromTimeError
!=
null
)
{
setState
(()
=>
fromTimeError
=
null
);
}
}
},
child:
textFieldNew
(
context
,
provider
.
fromTimeField
,
"Select Time"
,
enabled:
false
),
),
errorWidget
(
context
,
fromTimeError
),
/// To Date
TextWidget
(
context
,
"To Date"
),
GestureDetector
(
onTap:
()
{
provider
.
showDatePickerDialog
(
context
,
isFromDate:
false
);
if
(
toDateError
!=
null
)
{
setState
(()
=>
toDateError
=
null
);
}
},
child:
textFieldNew
(
context
,
provider
.
toDateField
,
"Select Date"
,
enabled:
false
),
),
errorWidget
(
context
,
toDateError
),
/// To Time
TextWidget
(
context
,
"To Time"
),
GestureDetector
(
onTap:
()
async
{
TimeOfDay
?
picked
=
await
showTimePicker
(
context:
context
,
initialTime:
TimeOfDay
.
now
(),
);
if
(
picked
!=
null
)
{
provider
.
toTimeField
.
text
=
picked
.
format
(
context
);
if
(
toTimeError
!=
null
)
{
setState
(()
=>
toTimeError
=
null
);
}
}
},
child:
textFieldNew
(
context
,
provider
.
toTimeField
,
"Select Time"
,
enabled:
false
),
),
errorWidget
(
context
,
toTimeError
),
/// Leave Type
TextWidget
(
context
,
"Leave Type"
),
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
6
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Leave Type"
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
)),
value:
leaveType
,
items:
leaveTypes
.
map
((
e
)
=>
DropdownMenuItem
(
value:
e
,
child:
Text
(
e
)))
.
toList
(),
onChanged:
(
val
)
{
setState
(()
{
leaveType
=
val
;
if
(
leaveTypeError
!=
null
)
{
leaveTypeError
=
null
;
}
});
},
),
),
),
errorWidget
(
context
,
leaveTypeError
),
/// Reason
TextWidget
(
context
,
"Reason"
),
textFieldNew
(
context
,
provider
.
reasonController
,
"Enter Reason"
,
maxLines:
2
),
errorWidget
(
context
,
reasonError
),
const
SizedBox
(
height:
70
),
],
),
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation
.
centerFloat
,
bottomNavigationBar:
InkResponse
(
onTap:
()
{
if
(
validateForm
(
provider
))
{
provider
.
addLeaveRequest
(
context
,
fromDate:
provider
.
fromDateField
.
text
,
fromTime:
provider
.
fromTimeField
.
text
,
toDate:
provider
.
toDateField
.
text
,
toTime:
provider
.
toTimeField
.
text
,
leaveType:
leaveType
!,
reason:
provider
.
reasonController
.
text
,
);
// Reset after submit
setState
(()
{
provider
.
fromDateField
.
clear
();
provider
.
fromTimeField
.
clear
();
provider
.
toDateField
.
clear
();
provider
.
toTimeField
.
clear
();
provider
.
reasonController
.
clear
();
leaveType
=
null
;
fromDateError
=
null
;
fromTimeError
=
null
;
toDateError
=
null
;
toTimeError
=
null
;
leaveTypeError
=
null
;
reasonError
=
null
;
});
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Leave request submitted successfully!"
),
backgroundColor:
Colors
.
black87
,
),
);
}
},
child:
Container
(
height:
45
,
alignment:
Alignment
.
center
,
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
10
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
5
),
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
provider
.
isSubmitting
?
const
CircularProgressIndicator
(
color:
Colors
.
white
)
:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
),
),
),
),
);
}
Widget
textFieldNew
(
BuildContext
context
,
TextEditingController
controller
,
String
hint
,
{
bool
enabled
=
true
,
int
maxLines
=
1
,
})
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
6
),
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
TextFormField
(
controller:
controller
,
enabled:
enabled
,
maxLines:
maxLines
,
style:
TextStyle
(
color:
Colors
.
black
,
// Entered text color
fontSize:
14
,
// Optional: adjust font size
),
decoration:
InputDecoration
(
hintText:
hint
,
hintStyle:
TextStyle
(
color:
Colors
.
grey
.
shade500
,
// Customize this color
fontSize:
14
,
// Optional: tweak font size
),
border:
InputBorder
.
none
,
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
),
),
);
}
Widget
errorText
(
String
msg
)
{
return
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
12
,
left:
4
),
child:
Text
(
msg
,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
),
),
);
}
bool
_validateForm
(
LeaveApplicationListProvider
provider
)
{
bool
isValid
=
true
;
setState
(()
{
fromDateError
=
provider
.
fromDateField
.
text
.
isEmpty
?
"From date is required"
:
null
;
fromTimeError
=
provider
.
fromTimeField
.
text
.
isEmpty
?
"From time is required"
:
null
;
toDateError
=
provider
.
toDateField
.
text
.
isEmpty
?
"To date is required"
:
null
;
toTimeError
=
provider
.
toTimeField
.
text
.
isEmpty
?
"To time is required"
:
null
;
leaveTypeError
=
leaveType
==
null
?
"Please select leave type"
:
null
;
reasonError
=
provider
.
reasonController
.
text
.
isEmpty
?
"Reason is required"
:
null
;
if
(
fromDateError
!=
null
||
fromTimeError
!=
null
||
toDateError
!=
null
||
toTimeError
!=
null
||
leaveTypeError
!=
null
||
reasonError
!=
null
)
{
isValid
=
false
;
}
});
return
isValid
;
}
}
lib/screens/hrm/AddLiveAttendance.dart
View file @
f6fbe101
...
...
@@ -2,9 +2,11 @@ import 'dart:io';
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:geocoding/geocoding.dart'
;
import
'package:geolocator/geolocator.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
...
...
@@ -17,6 +19,10 @@ class AddLiveAttendanceScreen extends StatefulWidget {
}
class
_AddLiveAttendanceScreenState
extends
State
<
AddLiveAttendanceScreen
>
{
String
?
typeError
;
String
?
locationError
;
String
?
proofError
;
String
?
selectedType
;
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
TextEditingController
locationController
=
TextEditingController
();
...
...
@@ -25,9 +31,9 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
final
List
<
String
>
types
=
[
"Check In"
,
"Check Out"
];
final
ImagePicker
picker
=
ImagePicker
();
XFile
?
proofFile
;
// store selected proof
XFile
?
proofFile
;
bool
isSubmitting
=
false
;
// computed labels
String
get
locationHeading
=>
selectedType
==
null
?
"Location"
:
"
$selectedType
Location"
;
...
...
@@ -40,21 +46,49 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
bool
get
isSubmitEnabled
=>
selectedType
!=
null
&&
locationController
.
text
.
trim
().
isNotEmpty
&&
proofFile
!=
null
;
// proof is required
proofFile
!=
null
;
@override
void
initState
()
{
super
.
initState
();
locationController
.
addListener
(()
{
setState
(()
{});
_autoFetchLocation
();
}
Future
<
void
>
_autoFetchLocation
()
async
{
String
loc
=
await
getCurrentLocation
();
setState
(()
{
locationController
.
text
=
loc
;
});
}
@override
void
dispose
()
{
locationController
.
dispose
();
descriptionController
.
dispose
();
super
.
dispose
();
Future
<
String
>
getCurrentLocation
()
async
{
try
{
LocationPermission
permission
=
await
Geolocator
.
checkPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
permission
=
await
Geolocator
.
requestPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
return
"Location permission denied"
;
}
}
if
(
permission
==
LocationPermission
.
deniedForever
)
{
return
"Location permissions permanently denied"
;
}
Position
position
=
await
Geolocator
.
getCurrentPosition
(
desiredAccuracy:
LocationAccuracy
.
high
);
List
<
Placemark
>
placemarks
=
await
placemarkFromCoordinates
(
position
.
latitude
,
position
.
longitude
);
if
(
placemarks
.
isNotEmpty
)
{
Placemark
place
=
placemarks
.
first
;
return
"
${place.name}
,
${place.locality}
,
${place.administrativeArea}
,
${place.country}
"
;
}
else
{
return
"
${position.latitude}
,
${position.longitude}
"
;
}
}
catch
(
e
)
{
return
"Error:
$e
"
;
}
}
void
_showPicker
(
BuildContext
context
)
{
...
...
@@ -95,26 +129,71 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
);
}
Future
<
String
>
getCurrentLocation
()
async
{
var
status
=
await
Permission
.
location
.
request
();
if
(
status
.
isGranted
)
{
Position
position
=
await
Geolocator
.
getCurrentPosition
(
desiredAccuracy:
LocationAccuracy
.
high
,
);
return
"
${position.latitude.toStringAsFixed(7)}
,
${position.longitude.toStringAsFixed(7)}
"
;
}
else
{
return
"Permission denied"
;
void
submitAttendance
(
BuildContext
context
)
async
{
setState
(()
{
typeError
=
null
;
locationError
=
null
;
proofError
=
null
;
});
bool
hasError
=
false
;
if
(
selectedType
==
null
)
{
typeError
=
"Please select a type"
;
hasError
=
true
;
}
if
(
locationController
.
text
.
trim
().
isEmpty
)
{
locationError
=
"Please enter a location"
;
hasError
=
true
;
}
if
(
proofFile
==
null
)
{
proofError
=
"Please attach proof"
;
hasError
=
true
;
}
/// New: submit function
void
submitAttendance
()
{
print
(
"==== Attendance Submitted ===="
);
print
(
"Type:
$selectedType
"
);
print
(
"Location:
${locationController.text}
"
);
print
(
"Description:
${descriptionController.text}
"
);
print
(
"Proof:
${proofFile?.path ?? 'No file'}
"
);
print
(
"============================="
);
if
(
hasError
)
{
setState
(()
{});
return
;
}
setState
(()
=>
isSubmitting
=
true
);
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
await
provider
.
addAttendanceRequest
(
context
,
process:
"Live"
,
type:
selectedType
??
""
,
loc:
locationController
.
text
,
checkDate:
DateTime
.
now
().
toString
().
split
(
" "
).
first
,
checkInTime:
selectedType
==
"Check In"
?
TimeOfDay
.
now
().
format
(
context
)
:
null
,
checkInLoc:
selectedType
==
"Check In"
?
locationController
.
text
:
null
,
checkInProof:
selectedType
==
"Check In"
?
File
(
proofFile
!.
path
)
:
null
,
checkOutTime:
selectedType
==
"Check Out"
?
TimeOfDay
.
now
().
format
(
context
)
:
null
,
checkOutLoc:
selectedType
==
"Check Out"
?
locationController
.
text
:
null
,
checkOutProof:
selectedType
==
"Check Out"
?
File
(
proofFile
!.
path
)
:
null
,
note:
descriptionController
.
text
,
);
setState
(()
{
isSubmitting
=
false
;
selectedType
=
null
;
locationController
.
clear
();
descriptionController
.
clear
();
proofFile
=
null
;
});
_showSnack
(
context
,
"Attendance Submitted Successfully!"
);
_autoFetchLocation
();
}
void
_showSnack
(
BuildContext
context
,
String
msg
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
msg
),
backgroundColor:
Colors
.
black87
),
);
}
@override
...
...
@@ -123,7 +202,8 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
backgroundColor:
Colors
.
white
,
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
const
Color
(
0xFEFFFFFF
),
backgroundColor:
Colors
.
white
,
elevation:
0
,
title:
Row
(
children:
[
InkResponse
(
...
...
@@ -138,7 +218,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
"Add Live Attendance"
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"
Plus
Jakarta
Sans
"
,
fontFamily:
"Jakarta
Medium
"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
...
...
@@ -155,7 +235,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const
Text
(
"Type"
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"
Plus
Jakarta
Sans
"
,
fontFamily:
"Jakarta
Medium
"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
Container
(
...
...
@@ -167,101 +247,113 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
child:
DropdownButtonHideUnderline
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Type"
,
hint:
const
Text
(
"Select Type"
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"
Plus
Jakarta
Sans
"
,
fontFamily:
"Jakarta
Medium
"
,
fontWeight:
FontWeight
.
w400
),
),
value:
selectedType
,
items:
types
.
map
((
e
)
=>
DropdownMenuItem
<
String
>(
.
map
((
e
)
=>
DropdownMenuItem
<
String
>(
value:
e
,
child:
Text
(
e
,
style:
TextStyle
(
fontSize:
14
,
),
style:
const
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaMedium"
),
overflow:
TextOverflow
.
ellipsis
,
)
)
,
))
.
toList
(),
onChanged:
(
val
)
=>
setState
(()
=>
selectedType
=
val
),
// buttonStyleData: ddtheme.buttonStyleData,
iconStyleData:
ddtheme
.
iconStyleData
,
// menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
),
if
(
typeError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
typeError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
const
SizedBox
(
height:
16
),
/// Location
field
/// Location
Text
(
locationHeading
,
style:
const
TextStyle
(
fontSize:
15
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w500
)),
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
TextField
(
controller:
locationController
,
readOnly:
true
,
onTap:
()
async
{
String
loc
=
await
getCurrentLocation
();
locationController
.
text
=
loc
;
},
decoration:
_inputDecoration
(
"Tap to get location"
),
decoration:
_inputDecoration
(
"Enter location"
),
),
if
(
locationError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
locationError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
const
SizedBox
(
height:
16
),
/// Description
Text
(
descriptionHeading
,
style:
const
TextStyle
(
fontSize:
15
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w500
)
),
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
TextField
(
controller:
descriptionController
,
maxLines:
3
,
decoration:
_inputDecoration
(
"Write Description"
),
),
const
SizedBox
(
height:
20
),
/// Attach Proof
SizedBox
(
InkResponse
(
onTap:
()
=>
_showPicker
(
context
),
child:
Container
(
width:
double
.
infinity
,
child:
OutlinedButton
(
onPressed:
()
=>
_showPicker
(
context
),
style:
OutlinedButton
.
styleFrom
(
backgroundColor:
Colors
.
blue
.
shade50
,
side:
BorderSide
(
color:
Colors
.
blue
.
shade200
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
14
),
),
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
decoration:
BoxDecoration
(
color:
Colors
.
blue
.
shade50
,
border:
Border
.
all
(
color:
Colors
.
blue
.
shade200
),
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Text
(
proofButtonText
,
child:
Center
(
child:
Text
(
proofButtonText
,
style:
const
TextStyle
(
fontSize:
16
,
color:
Colors
.
blue
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w500
)),
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w500
,
),
),
),
),
),
if
(
proofError
!=
null
)
...[
const
SizedBox
(
height:
4
),
Text
(
proofError
!,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
13
,
fontFamily:
"JakartaMedium"
)),
],
/// Show proof preview
if
(
proofFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Row
(
...
...
@@ -269,9 +361,10 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const
Icon
(
Icons
.
check_circle
,
color:
Colors
.
green
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
"Attached:
${proofFile!.name}
"
,
overflow:
TextOverflow
.
ellipsis
)),
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
),
...
...
@@ -282,23 +375,30 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
const
SizedBox
(
height:
24
),
/// Submit button
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
isSubmitEnabled
?
submitAttendance
:
null
,
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
isSubmitEnabled
?
Colors
.
blue
:
Colors
.
grey
.
shade400
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
/// 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:
const
Text
(
"Submit"
,
child:
isSubmitting
?
const
CircularProgressIndicator
(
color:
Colors
.
white
,
strokeWidth:
2
)
:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
16
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w500
)),
),
),
),
),
],
...
...
@@ -306,13 +406,13 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
),
);
}
InputDecoration
_inputDecoration
(
String
hint
)
{
return
InputDecoration
(
hintText:
hint
,
hintStyle:
const
TextStyle
(
fontSize:
14
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"JakartaMedium"
,
fontWeight:
FontWeight
.
w400
,
color:
Colors
.
grey
,
),
...
...
@@ -324,7 +424,7 @@ class _AddLiveAttendanceScreenState extends State<AddLiveAttendanceScreen> {
),
focusedBorder:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
borderSide:
const
BorderSide
(
color:
Colors
.
blue
),
borderSide:
BorderSide
(
color:
App
Colors
.
app_
blue
),
),
);
}
...
...
lib/screens/hrm/AddManualAttendance.dart
View file @
f6fbe101
import
'package:flutter/cupertino.dart'
;
import
'dart:io'
;
import
'package:connectivity_plus/connectivity_plus.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'package:geolocator/geolocator.dart'
;
import
'package:geocoding/geocoding.dart'
;
import
'../../Models/ordersModels/commonResponse.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonServices.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../../Utils/dropdownTheme.dart'
;
class
AddManualAttendanceScreen
extends
StatefulWidget
{
...
...
@@ -15,10 +27,15 @@ class AddManualAttendanceScreen extends StatefulWidget {
}
class
_AddManualAttendanceScreenState
extends
State
<
AddManualAttendanceScreen
>
{
final
TextEditingController
dateController
=
TextEditingController
();
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
ImagePicker
picker
=
ImagePicker
();
// Connectivity
Map
_source
=
{
ConnectivityResult
.
mobile
:
true
};
final
MyConnectivity
_connectivity
=
MyConnectivity
.
instance
;
String
connection
=
"Online"
;
//
Separate controllers for each section
//
Controllers
final
checkInTime
=
TextEditingController
();
final
checkInLocation
=
TextEditingController
();
final
checkInDescription
=
TextEditingController
();
...
...
@@ -29,325 +46,524 @@ class _AddManualAttendanceScreenState extends State<AddManualAttendanceScreen> {
final
checkOutDescription
=
TextEditingController
();
XFile
?
checkOutProof
;
final
ImagePicker
picker
=
ImagePicker
();
String
?
selectedType
;
final
List
<
String
>
types
=
[
"Check In"
,
"Check Out"
,
"Check In/Out"
];
DateTime
?
_date
;
bool
get
isSubmitEnabled
{
if
(
selectedType
==
"Check In"
)
{
return
checkInLocation
.
text
.
trim
().
isNotEmpty
;
}
else
if
(
selectedType
==
"Check Out"
)
{
return
checkOutLocation
.
text
.
trim
().
isNotEmpty
;
}
else
if
(
selectedType
==
"Check In/Out"
)
{
return
checkInLocation
.
text
.
trim
().
isNotEmpty
&&
checkOutLocation
.
text
.
trim
().
isNotEmpty
;
}
return
false
;
}
// Errors
String
?
dateError
,
typeError
;
String
?
checkInTimeError
,
checkInLocError
,
checkInDescError
,
checkInProofError
;
String
?
checkOutTimeError
,
checkOutLocError
,
checkOutDescError
,
checkOutProofError
;
// In your Attendancelistprovider class
CommonResponse
?
get
addResponse
=>
addResponse
;
String
?
get
errorMessage
=>
errorMessage
;
@override
void
initState
()
{
super
.
initState
();
checkInLocation
.
addListener
(()
=>
setState
(()
{}));
checkOutLocation
.
addListener
(()
=>
setState
(()
{}));
_connectivity
.
initialise
();
_connectivity
.
myStream
.
listen
((
src
)
{
setState
(()
=>
_source
=
src
);
});
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
_fetchInitialLocation
();
});
}
// ===== Print all submitted values =====
void
_submitForm
()
{
print
(
"===== Manual Attendance Submitted ====="
);
print
(
"Date:
${dateController.text}
"
);
print
(
"Type:
$selectedType
"
);
print
(
"Time:
${checkInTime.text}
"
);
print
(
"Location:
${checkInLocation.text}
"
);
print
(
"Description:
${checkInDescription.text}
"
);
print
(
"Proof:
${checkInProof != null ? checkOutProof!.path : 'No file attached'}
"
);
print
(
"======================================="
);
print
(
"Date:
${dateController.text}
"
);
print
(
"Type:
$selectedType
"
);
print
(
"Time:
${checkOutTime.text}
"
);
print
(
"Location:
${checkOutLocation.text}
"
);
print
(
"Description:
${checkOutDescription.text}
"
);
print
(
"Proof:
${checkInProof != null ? checkOutProof!.path : 'No file attached'}
"
);
@override
void
dispose
()
{
_connectivity
.
disposeStream
();
super
.
dispose
();
}
// ===== Pick File =====
Future
<
void
>
_pickFile
(
bool
isCheckIn
)
async
{
final
XFile
?
file
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
if
(
file
!=
null
)
{
Future
<
void
>
_fetchInitialLocation
()
async
{
String
loc
=
await
getCurrentLocation
();
setState
(()
{
if
(
isCheckIn
)
{
checkInProof
=
file
;
}
else
{
checkOutProof
=
file
;
}
checkInLocation
.
text
=
loc
;
checkOutLocation
.
text
=
loc
;
});
}
Future
<
String
>
getCurrentLocation
()
async
{
try
{
LocationPermission
permission
=
await
Geolocator
.
checkPermission
();
if
(
permission
==
LocationPermission
.
denied
)
{
permission
=
await
Geolocator
.
requestPermission
();
if
(
permission
==
LocationPermission
.
denied
)
return
"Permission denied"
;
}
if
(
permission
==
LocationPermission
.
deniedForever
)
{
return
"Permission permanently denied"
;
}
void
setDate
(
DateTime
newDate
)
{
_date
=
newDate
;
dateController
.
text
=
"
${newDate.day}
-
${newDate.month}
-
${newDate.year}
"
;
Position
pos
=
await
Geolocator
.
getCurrentPosition
(
desiredAccuracy:
LocationAccuracy
.
high
);
List
<
Placemark
>
placemarks
=
await
placemarkFromCoordinates
(
pos
.
latitude
,
pos
.
longitude
);
if
(
placemarks
.
isNotEmpty
)
{
Placemark
p
=
placemarks
.
first
;
return
"
${p.locality}
,
${p.administrativeArea}
,
${p.country}
"
;
}
return
"
${pos.latitude}
,
${pos.longitude}
"
;
}
catch
(
e
)
{
return
"Error:
$e
"
;
}
}
Future
<
void
>
_pickTime
(
TextEditingController
controller
)
async
{
final
TimeOfDay
?
picked
=
await
showTimePicker
(
context:
context
,
initialTime:
TimeOfDay
.
now
());
if
(
picked
!=
null
)
{
controller
.
text
=
picked
.
format
(
context
);
}
if
(
picked
!=
null
)
controller
.
text
=
picked
.
format
(
context
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
Future
<
void
>
_pickFile
(
bool
isCheckIn
)
async
{
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
backgroundColor:
const
Color
(
0xFEFFFFFF
),
title:
Row
(
context:
context
,
builder:
(
_
)
{
return
SafeArea
(
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
),
ListTile
(
leading:
const
Icon
(
Icons
.
camera_alt
),
title:
const
Text
(
"Capture photo from camera"
),
onTap:
()
async
{
final
XFile
?
file
=
await
picker
.
pickImage
(
source
:
ImageSource
.
camera
);
if
(
file
!=
null
)
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
file
;
else
checkOutProof
=
file
;
});
}
Navigator
.
pop
(
context
);
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
"Select photo from gallery"
),
onTap:
()
async
{
final
XFile
?
file
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
if
(
file
!=
null
)
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
file
;
else
checkOutProof
=
file
;
});
}
Navigator
.
pop
(
context
);
},
),
const
SizedBox
(
width:
10
),
Text
(
"Add Manual Attendance"
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
)),
],
),
);
},
);
}
void
_submitForm
(
BuildContext
context
)
async
{
// reset errors first
dateError
=
null
;
typeError
=
null
;
checkInTimeError
=
checkInLocError
=
checkInDescError
=
checkInProofError
=
null
;
checkOutTimeError
=
checkOutLocError
=
checkOutDescError
=
checkOutProofError
=
null
;
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
// --- Date Validation ---
if
(
provider
.
dateController
.
text
.
isEmpty
)
{
dateError
=
"Please select a date"
;
}
else
{
try
{
final
enteredDate
=
DateFormat
(
"dd MMM yyyy"
).
parse
(
provider
.
dateController
.
text
);
provider
.
setSelectedDate
(
enteredDate
);
if
(!
provider
.
isDateValid
())
{
dateError
=
"Date must be today or yesterday"
;
}
}
catch
(
e
)
{
dateError
=
"Invalid date format (use dd MMM yyyy)"
;
}
}
// --- Type Validation ---
if
(
selectedType
==
null
)
{
typeError
=
"Please select type"
;
}
// --- Check In Validations ---
if
(
selectedType
==
"Check In"
||
selectedType
==
"Check In/Out"
)
{
if
(
checkInTime
.
text
.
isEmpty
)
checkInTimeError
=
"Please select check-in time"
;
if
(
checkInLocation
.
text
.
isEmpty
)
checkInLocError
=
"Please enter check-in location"
;
if
(
checkInDescription
.
text
.
isEmpty
)
checkInDescError
=
"Please enter description"
;
if
(
checkInProof
==
null
)
checkInProofError
=
"Please attach check-in proof"
;
}
// --- Check Out Validations ---
if
(
selectedType
==
"Check Out"
||
selectedType
==
"Check In/Out"
)
{
if
(
checkOutTime
.
text
.
isEmpty
)
checkOutTimeError
=
"Please select check-out time"
;
if
(
checkOutLocation
.
text
.
isEmpty
)
checkOutLocError
=
"Please enter check-out location"
;
if
(
checkOutDescription
.
text
.
isEmpty
)
checkOutDescError
=
"Please enter description"
;
if
(
checkOutProof
==
null
)
checkOutProofError
=
"Please attach check-out proof"
;
}
// --- Stop if any error ---
if
([
dateError
,
typeError
,
checkInTimeError
,
checkInLocError
,
checkInDescError
,
checkInProofError
,
checkOutTimeError
,
checkOutLocError
,
checkOutDescError
,
checkOutProofError
].
any
((
e
)
=>
e
!=
null
))
{
setState
(()
{});
return
;
}
// --- Build data according to type ---
String
?
finalCheckInTime
;
String
?
finalCheckInLoc
;
File
?
finalCheckInProof
;
String
?
finalCheckOutTime
;
String
?
finalCheckOutLoc
;
File
?
finalCheckOutProof
;
String
?
finalNote
;
if
(
selectedType
==
"Check In"
)
{
finalCheckInTime
=
checkInTime
.
text
;
finalCheckInLoc
=
checkInLocation
.
text
;
finalCheckInProof
=
File
(
checkInProof
!.
path
);
finalNote
=
checkInDescription
.
text
;
}
else
if
(
selectedType
==
"Check Out"
)
{
finalCheckOutTime
=
checkOutTime
.
text
;
finalCheckOutLoc
=
checkOutLocation
.
text
;
finalCheckOutProof
=
File
(
checkOutProof
!.
path
);
finalNote
=
checkOutDescription
.
text
;
}
else
if
(
selectedType
==
"Check In/Out"
)
{
finalCheckInTime
=
checkInTime
.
text
;
finalCheckInLoc
=
checkInLocation
.
text
;
finalCheckInProof
=
File
(
checkInProof
!.
path
);
finalCheckOutTime
=
checkOutTime
.
text
;
finalCheckOutLoc
=
checkOutLocation
.
text
;
finalCheckOutProof
=
File
(
checkOutProof
!.
path
);
finalNote
=
"
${checkInDescription.text}
/
${checkOutDescription.text}
"
;
}
// --- Submit to provider ---
await
provider
.
addAttendanceRequest
(
context
,
process:
"Manual"
,
type:
selectedType
!,
loc:
selectedType
==
"Check In"
?
checkInLocation
.
text
:
selectedType
==
"Check Out"
?
checkOutLocation
.
text
:
"
${checkInLocation.text}
,
${checkOutLocation.text}
"
,
checkDate:
provider
.
dateController
.
text
,
checkInTime:
finalCheckInTime
,
checkInLoc:
finalCheckInLoc
,
checkInProof:
finalCheckInProof
,
checkOutTime:
finalCheckOutTime
,
checkOutLoc:
finalCheckOutLoc
,
checkOutProof:
finalCheckOutProof
,
note:
finalNote
,
);
// Check the response from provider
if
(
provider
.
addResponse
!=
null
&&
provider
.
addResponse
!.
error
==
"0"
)
{
// Success case
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
provider
.
addResponse
!.
message
??
"Attendance Submitted Successfully"
)),
);
// --- Reset fields ---
setState
(()
{
selectedType
=
null
;
provider
.
dateController
.
clear
();
checkInTime
.
clear
();
checkInLocation
.
clear
();
checkInDescription
.
clear
();
checkInProof
=
null
;
checkOutTime
.
clear
();
checkOutLocation
.
clear
();
checkOutDescription
.
clear
();
checkOutProof
=
null
;
});
_fetchInitialLocation
();
}
else
{
// Error case - show appropriate message
String
errorMessage
=
provider
.
errorMessage
??
"Failed to submit attendance"
;
// Handle specific server error for Check Out without Check In
if
(
errorMessage
.
contains
(
"Check In is not Available"
))
{
errorMessage
=
"Cannot submit Check Out without a Check In record for this date"
;
}
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
errorMessage
),
backgroundColor:
Colors
.
red
),
);
}
}
Future
<
bool
>
_onBackPressed
(
BuildContext
context
)
async
{
Navigator
.
pop
(
context
,
true
);
return
true
;
}
@override
Widget
build
(
BuildContext
context
)
{
switch
(
_source
.
keys
.
toList
()[
0
])
{
case
ConnectivityResult
.
mobile
:
case
ConnectivityResult
.
wifi
:
connection
=
'Online'
;
break
;
default
:
connection
=
'Offline'
;
}
return
(
connection
==
"Online"
)
?
Platform
.
isAndroid
?
WillPopScope
(
onWillPop:
()
=>
_onBackPressed
(
context
),
child:
SafeArea
(
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
),
),
)
:
_scaffold
(
context
)
:
NoNetwork
(
context
);
}
Widget
_scaffold
(
BuildContext
context
)
{
return
Consumer
<
Attendancelistprovider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
backgroundColor:
Colors
.
white
,
appBar:
appbar2New
(
context
,
"Add Manual Attendance"
,
()
{},
SizedBox
.
shrink
(),
0xFFFFFFFF
,
),
body:
Padding
(
padding:
const
EdgeInsets
.
all
(
18
),
body:
Scrollbar
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
12
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
/// Date
_buildLabel
(
"Date"
),
const
SizedBox
(
height:
6
),
TextField
(
controller:
dateController
,
readOnly:
true
,
onTap:
()
=>
setDate
(
DateTime
.
now
()),
decoration:
_inputDecoration
(
"Select Date"
)
.
copyWith
(
suffixIcon:
const
Icon
(
Icons
.
calendar_today
)),
),
const
SizedBox
(
height:
16
),
/// Type Dropdown
_buildLabel
(
"Type"
),
const
SizedBox
(
height:
6
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
8
),
// TextWidget(context, "Date"),
GestureDetector
(
onTap:
()
=>
provider
.
showDatePickerDialog
(
context
),
child:
AbsorbPointer
(
child:
textControllerWidget
(
context
,
provider
.
dateController
,
"Date"
,
"Select Date"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
child:
DropdownButtonHideUnderline
(
),
),
errorWidget
(
context
,
dateError
),
TextWidget
(
context
,
"Type"
),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Type"
),
value:
selectedType
,
items:
types
.
map
((
e
)
=>
DropdownMenuItem
<
String
>
(
.
map
((
e
)
=>
DropdownMenuItem
(
value:
e
,
child:
Text
(
e
),
))
.
toList
(),
onChanged:
(
val
)
=>
setState
(()
=>
selectedType
=
val
),
// buttonStyleData: ddtheme.buttonStyleData,
value:
selectedType
,
onChanged:
(
val
)
{
setState
(()
=>
selectedType
=
val
);
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
//
menuItemStyleData: ddtheme.menuItemStyleData,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
const
SizedBox
(
height:
20
),
/// Conditional Sections
if
(
selectedType
==
"Check In"
)
_buildSection
(
title:
"Check In"
,
timeController:
checkInTime
,
locationController:
checkInLocation
,
descriptionController:
checkInDescription
,
proofFile:
checkInProof
,
onPickProof:
()
=>
_pickFile
(
true
),
),
if
(
selectedType
==
"Check Out"
)
_buildSection
(
title:
"Check Out"
,
timeController:
checkOutTime
,
locationController:
checkOutLocation
,
descriptionController:
checkOutDescription
,
proofFile:
checkOutProof
,
onPickProof:
()
=>
_pickFile
(
false
),
),
if
(
selectedType
==
"Check In/Out"
)
...[
_buildSection
(
title:
"Check In"
,
timeController:
checkInTime
,
locationController:
checkInLocation
,
descriptionController:
checkInDescription
,
proofFile:
checkInProof
,
onPickProof:
()
=>
_pickFile
(
true
),
),
_buildSection
(
title:
"Check Out"
,
timeController:
checkOutTime
,
locationController:
checkOutLocation
,
descriptionController:
checkOutDescription
,
proofFile:
checkOutProof
,
onPickProof:
()
=>
_pickFile
(
false
),
),
]
,
errorWidget
(
context
,
typeError
)
,
const
SizedBox
(
height:
24
),
if
(
selectedType
==
"Check In"
||
selectedType
==
"Check In/Out"
)
_buildSection
(
"Check In"
),
if
(
selectedType
==
"Check Out"
||
selectedType
==
"Check In/Out"
)
_buildSection
(
"Check Out"
),
/// Submit Button
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
backgroundColor:
isSubmitEnabled
?
Colors
.
blue
:
Colors
.
grey
.
shade400
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
onPressed:
isSubmitEnabled
?
()
{
_submitForm
();
print
(
"Submit pressed for
$selectedType
"
);
}
:
null
,
child:
const
Text
(
"Submit"
,
SizedBox
(
height:
80
),
],
),
),
),
bottomNavigationBar:
InkResponse
(
onTap:
provider
.
isSubmitting
?
null
:
()
=>
_submitForm
(
context
),
child:
Container
(
height:
45
,
alignment:
Alignment
.
center
,
margin:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
provider
.
isSubmitting
?
CircularProgressIndicator
.
adaptive
(
valueColor:
AlwaysStoppedAnimation
(
AppColors
.
white
),
)
:
const
Text
(
"Submit"
,
style:
TextStyle
(
fontSize:
16
,
fontSize:
15
,
fontFamily:
"JakartaMedium"
,
color:
Colors
.
white
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w500
,
)),
),
),
],
),
),
),
);
},
);
}
/// Reusable Section
Widget
_buildSection
({
required
String
title
,
required
TextEditingController
timeController
,
required
TextEditingController
locationController
,
required
TextEditingController
descriptionController
,
required
XFile
?
proofFile
,
required
VoidCallback
onPickProof
,
})
{
Widget
_buildSection
(
String
title
)
{
final
isCheckIn
=
title
==
"Check In"
;
final
timeCtrl
=
isCheckIn
?
checkInTime
:
checkOutTime
;
final
locCtrl
=
isCheckIn
?
checkInLocation
:
checkOutLocation
;
final
descCtrl
=
isCheckIn
?
checkInDescription
:
checkOutDescription
;
final
proofFile
=
isCheckIn
?
checkInProof
:
checkOutProof
;
final
proofError
=
isCheckIn
?
checkInProofError
:
checkOutProofError
;
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildLabel
(
"
$title
Time"
),
const
SizedBox
(
height:
6
),
TextField
(
controller:
timeController
,
readOnly:
true
,
onTap:
()
=>
_pickTime
(
timeController
),
decoration:
_inputDecoration
(
"Select Time"
)
.
copyWith
(
suffixIcon:
const
Icon
(
Icons
.
access_time
)),
),
const
SizedBox
(
height:
16
),
_buildLabel
(
"
$title
Location"
),
const
SizedBox
(
height:
6
),
TextField
(
controller:
locationController
,
decoration:
_inputDecoration
(
"Enter Location"
),
),
const
SizedBox
(
height:
16
),
_buildLabel
(
"
$title
Description"
),
const
SizedBox
(
height:
6
),
TextField
(
controller:
descriptionController
,
maxLines:
3
,
decoration:
_inputDecoration
(
"Write Description"
),
),
const
SizedBox
(
height:
18
),
/// Proof
SizedBox
(
// TextWidget(context, "$title Time"),
GestureDetector
(
onTap:
()
=>
_pickTime
(
timeCtrl
),
// ⏰ open time picker
child:
AbsorbPointer
(
child:
textControllerWidget
(
context
,
timeCtrl
,
"
$title
Time"
,
"Select Time"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
),
),
errorWidget
(
context
,
isCheckIn
?
checkInTimeError
:
checkOutTimeError
),
textControllerWidget
(
context
,
locCtrl
,
"
$title
Location"
,
"Enter Location"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorWidget
(
context
,
isCheckIn
?
checkInLocError
:
checkOutLocError
),
textControllerWidget
(
context
,
descCtrl
,
"
$title
Description"
,
"Enter Description"
,
(
v
)
{},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
done
,
),
errorWidget
(
context
,
isCheckIn
?
checkInDescError
:
checkOutDescError
),
InkResponse
(
onTap:
()
=>
_pickFile
(
isCheckIn
),
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
vertical:
10
),
height:
45
,
width:
double
.
infinity
,
child:
OutlinedButton
(
onPressed:
onPickProof
,
style:
OutlinedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
backgroundColor:
Colors
.
blue
.
shade50
,
side:
BorderSide
(
color:
Colors
.
blue
.
shade200
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
14
)),
),
child:
Text
(
"Attach
$title
Proof"
,
style:
const
TextStyle
(
fontSize:
16
,
color:
Colors
.
blue
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w500
)),
),
),
// Show proof preview
if
(
proofFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
"Attach
$title
Proof"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
if
(
proofFile
!=
null
)
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
const
Icon
(
Icons
.
check_circle
,
color:
Colors
.
green
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
"Attached:
${proofFile!.name}
"
,
overflow:
TextOverflow
.
ellipsis
)),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
),
onPressed:
()
=>
setState
(()
=>
proofFile
=
null
),
proofFile
.
name
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
12
),
),
],
),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
,
size:
18
),
onPressed:
()
{
setState
(()
{
if
(
isCheckIn
)
checkInProof
=
null
;
else
checkOutProof
=
null
;
});
},
)
],
const
SizedBox
(
height:
24
),
),
errorWidget
(
context
,
proofError
),
],
);
}
Widget
_buildLabel
(
String
text
)
=>
Text
(
text
,
style:
const
TextStyle
(
fontSize:
15
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w500
));
InputDecoration
_inputDecoration
(
String
hint
)
{
return
InputDecoration
(
hintText:
hint
,
hintStyle:
const
TextStyle
(
fontSize:
14
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
Colors
.
grey
),
filled:
true
,
fillColor:
Colors
.
grey
.
shade100
,
enabledBorder:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
borderSide:
BorderSide
.
none
),
focusedBorder:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
borderSide:
const
BorderSide
(
color:
Colors
.
blue
)),
);
}
}
lib/screens/hrm/AddTourExpBillScreen.dart
View file @
f6fbe101
import
'dart:io'
;
import
'package:connectivity_plus/connectivity_plus.dart'
;
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:
provider/provid
er.dart'
;
import
'package:
image_picker/image_pick
er.dart'
;
import
'package:intl/intl.dart'
;
import
'package:provider/provider.dart'
;
import
'package:file_picker/file_picker.dart'
;
import
'../../Notifiers/hrmProvider/tourExpensesProvider.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonServices.dart'
;
import
'../../Utils/commonWidgets.dart'
;
...
...
@@ -22,32 +25,32 @@ class AddBillScreen extends StatefulWidget {
}
class
_AddBillScreenState
extends
State
<
AddBillScreen
>
{
Dropdowntheme
ddtheme
=
Dropdowntheme
();
List
<
FocusNode
>
focusNodes
=
List
.
generate
(
8
,
(
index
)
=>
FocusNode
());
final
Dropdowntheme
ddtheme
=
Dropdowntheme
();
final
List
<
FocusNode
>
focusNodes
=
List
.
generate
(
8
,
(
index
)
=>
FocusNode
());
Map
_source
=
{
ConnectivityResult
.
mobile
:
true
};
final
MyConnectivity
_connectivity
=
MyConnectivity
.
instance
;
TextEditingController
placeController
=
TextEditingController
();
TextEditingController
dateController
=
TextEditingController
();
TextEditingController
noteController
=
TextEditingController
();
String
?
selectedDAAmount
;
String
?
selectedTourType
;
final
TextEditingController
placeController
=
TextEditingController
();
final
TextEditingController
noteController
=
TextEditingController
();
List
<
Map
<
String
,
String
>>
travelExpenses
=
[
{
"title"
:
"Bike"
,
"amount"
:
"1800"
,
"icon"
:
"assets/svg/ic_bike.svg"
},
{
"title"
:
"Taxi"
,
"amount"
:
"300"
,
"icon"
:
"assets/svg/ic_taxi.svg"
},
];
// Validation errors
String
?
placeError
;
String
?
daAmountError
;
String
?
tourTypeError
;
String
?
tourDateError
;
String
?
noteError
;
List
<
Map
<
String
,
String
>>
hotelExpenses
=
[
{
"title"
:
"Hotel Sharada"
,
"amount"
:
"1800"
,
"icon"
:
"assets/svg/ic_hotel.svg"
},
{
"title"
:
"Hotel Nikitan"
,
"amount"
:
"1800"
,
"icon"
:
"assets/svg/ic_hotel.svg"
},
];
List
<
Map
<
String
,
String
>>
travelExpenses
=
[];
List
<
Map
<
String
,
String
>>
hotelExpenses
=
[];
List
<
Map
<
String
,
String
>>
otherExpenses
=
[];
List
<
File
>
travelImages
=
[];
List
<
File
>
hotelImages
=
[];
List
<
File
>
otherImages
=
[];
List
<
Map
<
String
,
String
>>
otherExpenses
=
[
{
"title"
:
"Book"
,
"amount"
:
"1800"
,
"icon"
:
"assets/svg/ic_book.svg"
},
]
;
String
?
selectedDAAmount
;
String
?
selectedTourType
;
String
?
selectedTravelType
;
@override
void
initState
()
{
...
...
@@ -56,14 +59,33 @@ class _AddBillScreenState extends State<AddBillScreen> {
_connectivity
.
myStream
.
listen
((
source
)
{
setState
(()
=>
_source
=
source
);
});
// Add listeners to clear errors when user starts typing
placeController
.
addListener
(()
{
if
(
placeError
!=
null
&&
placeController
.
text
.
isNotEmpty
)
{
setState
(()
=>
placeError
=
null
);
}
});
noteController
.
addListener
(()
{
if
(
noteError
!=
null
&&
noteController
.
text
.
isNotEmpty
)
{
setState
(()
=>
noteError
=
null
);
}
});
Future
.
microtask
(()
{
final
provider
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
);
provider
.
fetchTourExpensesAddView
(
context
,
"0"
);
// fresh bill
});
}
@override
void
dispose
()
{
placeController
.
dispose
();
dateController
.
dispose
();
noteController
.
dispose
();
focusNodes
.
map
((
e
)
=>
e
.
dispose
());
for
(
var
node
in
focusNodes
)
{
node
.
dispose
();
}
_connectivity
.
disposeStream
();
super
.
dispose
();
}
...
...
@@ -73,6 +95,44 @@ class _AddBillScreenState extends State<AddBillScreen> {
return
true
;
}
// Function to validate all fields
bool
validateFields
()
{
String
?
newPlaceError
=
placeController
.
text
.
isEmpty
?
"Place of visit is required"
:
null
;
String
?
newDaAmountError
=
selectedDAAmount
==
null
?
"DA Amount is required"
:
null
;
String
?
newTourTypeError
=
selectedTourType
==
null
?
"Tour Type is required"
:
null
;
String
?
newTourDateError
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
).
dateController
.
text
.
isEmpty
?
"Tour Date is required"
:
null
;
String
?
newNoteError
=
noteController
.
text
.
isEmpty
?
"Note is required"
:
null
;
// Only update if there are actual changes to avoid unnecessary rebuilds
if
(
placeError
!=
newPlaceError
||
daAmountError
!=
newDaAmountError
||
tourTypeError
!=
newTourTypeError
||
tourDateError
!=
newTourDateError
||
noteError
!=
newNoteError
)
{
setState
(()
{
placeError
=
newPlaceError
;
daAmountError
=
newDaAmountError
;
tourTypeError
=
newTourTypeError
;
tourDateError
=
newTourDateError
;
noteError
=
newNoteError
;
});
}
return
newPlaceError
==
null
&&
newDaAmountError
==
null
&&
newTourTypeError
==
null
&&
newTourDateError
==
null
&&
newNoteError
==
null
;
}
// Format date to "02 Sep 2025" format
String
_formatDate
(
DateTime
date
)
{
const
months
=
[
'Jan'
,
'Feb'
,
'Mar'
,
'Apr'
,
'May'
,
'Jun'
,
'Jul'
,
'Aug'
,
'Sep'
,
'Oct'
,
'Nov'
,
'Dec'
];
return
'
${date.day.toString().padLeft(2, '0')}
${months[date.month - 1]}
${date.year}
'
;
}
@override
Widget
build
(
BuildContext
context
)
{
switch
(
_source
.
keys
.
toList
()[
0
])
{
...
...
@@ -90,99 +150,329 @@ class _AddBillScreenState extends State<AddBillScreen> {
?
WillPopScope
(
onWillPop:
()
=>
_onBackPressed
(
context
),
child:
SafeArea
(
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
),
),
top:
false
,
bottom:
true
,
child:
_scaffold
(
context
)),
)
:
_scaffold
(
context
)
:
NoNetwork
(
context
);
}
Future
<
File
?>
pickImage
(
BuildContext
context
)
async
{
final
ImagePicker
picker
=
ImagePicker
();
return
showModalBottomSheet
<
File
?>(
context:
context
,
builder:
(
ctx
)
{
return
SafeArea
(
child:
Wrap
(
children:
[
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
'Pick from Gallery'
),
onTap:
()
async
{
final
picked
=
await
picker
.
pickImage
(
source
:
ImageSource
.
gallery
);
Navigator
.
pop
(
ctx
,
picked
!=
null
?
File
(
picked
.
path
)
:
null
);
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
camera_alt
),
title:
const
Text
(
'Take a Photo'
),
onTap:
()
async
{
final
picked
=
await
picker
.
pickImage
(
source
:
ImageSource
.
camera
);
Navigator
.
pop
(
ctx
,
picked
!=
null
?
File
(
picked
.
path
)
:
null
);
},
),
],
),
);
},
);
}
Widget
_scaffold
(
BuildContext
context
)
{
return
Consumer
<
TourExpensesProvider
>(
builder:
(
context
,
provider
,
_
)
{
if
(
provider
.
isLoading
)
{
return
Scaffold
(
body:
Container
(
child:
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
)),),
);
}
if
(
provider
.
errorMessage
!=
null
)
{
return
Scaffold
(
body:
Center
(
child:
Text
(
provider
.
errorMessage
!)));
}
return
Scaffold
(
resizeToAvoidBottomInset:
true
,
backgroundColor:
AppColors
.
scaffold_bg_color
,
appBar:
appbarNew
(
context
,
widget
.
pageTitleName
,
0xFFFFFFFF
),
body:
SingleChildScrollView
(
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
title:
Text
(
widget
.
pageTitleName
,
style:
TextStyle
(
fontSize:
18
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
),
),
leading:
IconButton
(
icon:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
),
onPressed:
()
=>
Navigator
.
pop
(
context
),
),
),
body:
Scrollbar
(
thumbVisibility:
false
,
child:
SingleChildScrollView
(
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
12
),
margin:
const
EdgeInsets
.
all
(
12
),
padding:
EdgeInsets
.
symmetric
(
horizontal:
10
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
color:
App
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
20
),
),
margin:
EdgeInsets
.
only
(
top:
10
,
left:
10
,
right:
10
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
textControllerWidget
(
context
,
placeController
,
"Place of Visit"
,
"Enter Place"
,
(
value
)
{
// Clear error when user types
if
(
placeError
!=
null
&&
value
.
isNotEmpty
)
{
setState
(()
=>
placeError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
focusNodes
[
0
],
focusNodes
[
1
],
TextInputAction
.
next
,
),
errorWidget
(
context
,
placeError
),
/// Place of Visit
TextWidget
(
context
,
"Place of Visit"
),
textFieldNew
(
context
,
placeController
,
"Enter Place"
),
/// DA Amount
TextWidget
(
context
,
"DA Amount"
),
dropDownField
(
context
,
"Select DA Amount"
,
[
"100"
,
"200"
,
"300"
],
selectedDAAmount
,
(
val
)
{
setState
(()
=>
selectedDAAmount
=
val
);
}),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
'Select DA Amount'
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
items:
provider
.
daAmountList
.
map
((
item
)
=>
DropdownMenuItem
<
String
>(
value:
item
,
child:
Text
(
item
,
style:
const
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
))
.
toList
(),
value:
selectedDAAmount
,
onChanged:
(
String
?
value
)
{
setState
(()
{
selectedDAAmount
=
value
;
if
(
daAmountError
!=
null
)
daAmountError
=
null
;
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
errorWidget
(
context
,
daAmountError
),
/// Tour Type
TextWidget
(
context
,
"Tour Type"
),
dropDownField
(
context
,
"Select Tour"
,
[
"Business"
,
"Personal"
],
selectedTourType
,
(
val
)
{
setState
(()
=>
selectedTourType
=
val
);
}),
DropdownButtonHideUnderline
(
child:
Row
(
children:
[
Expanded
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
'Select Tour Type'
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
items:
provider
.
tourTypeList
.
map
((
item
)
=>
DropdownMenuItem
<
String
>(
value:
item
,
child:
Text
(
item
,
style:
const
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
))
.
toList
(),
value:
selectedTourType
,
onChanged:
(
String
?
value
)
{
setState
(()
{
selectedTourType
=
value
;
if
(
tourTypeError
!=
null
)
tourTypeError
=
null
;
});
},
buttonStyleData:
ddtheme
.
buttonStyleData
,
iconStyleData:
ddtheme
.
iconStyleData
,
menuItemStyleData:
ddtheme
.
menuItemStyleData
,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
],
),
),
errorWidget
(
context
,
tourTypeError
),
/// Tour Date
TextWidget
(
context
,
"Tour Date"
),
GestureDetector
(
onTap:
()
async
{
DateTime
?
picked
=
await
showDatePicker
(
context:
context
,
initialDate:
DateTime
.
now
(),
firstDate:
DateTime
(
2022
),
lastDate:
DateTime
(
2100
),
);
if
(
picked
!=
null
)
{
dateController
.
text
=
DateFormat
(
"dd MMM yyyy"
).
format
(
picked
);
setState
(()
{});
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
true
);
if
(
d
!=
null
)
{
provider
.
dateController
.
text
=
_formatDate
(
d
);
if
(
tourDateError
!=
null
)
{
setState
(()
=>
tourDateError
=
null
);
}
}
},
child:
textFieldNew
(
context
,
dateController
,
"Enter Date"
,
enabled:
false
),
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
TextFormField
(
controller:
provider
.
dateController
,
enabled:
false
,
decoration:
InputDecoration
(
hintText:
"Select Tour Date"
,
hintStyle:
TextStyle
(
fontWeight:
FontWeight
.
w400
,
color:
Color
(
0xFFB4BEC0
),
fontSize:
14
,
),
border:
InputBorder
.
none
,
contentPadding:
EdgeInsets
.
symmetric
(
horizontal:
15
),
),
),
),
),
errorWidget
(
context
,
tourDateError
),
/// Note
TextWidget
(
context
,
"Note"
),
textFieldNew
(
context
,
noteController
,
"Write Note"
,
maxLines:
3
),
textControllerWidget
(
context
,
noteController
,
"Note"
,
"Enter Note"
,
(
value
)
{
// Clear error when user types
if
(
noteError
!=
null
&&
value
.
isNotEmpty
)
{
setState
(()
=>
noteError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
focusNodes
[
2
],
focusNodes
[
3
],
TextInputAction
.
next
,
300
,
// Allow up to 300 characters
),
errorWidget
(
context
,
noteError
),
const
SizedBox
(
height:
16
),
/// Travel Expenses
/// Travel Expenses
Section
sectionHeader
(
"Travel Expenses"
,
onAddTap:
()
{
// TODO: Add Travel Expense
showAddTravelExpenseSheet
(
context
,
travelExpenses
,
()
=>
setState
(()
{}),
provider
.
travelTypeList
,
travelImages
,
);
}),
e
xpenseList
(
travelExpenses
),
if
(
travelExpenses
.
isNotEmpty
)
travelE
xpenseList
(
travelExpenses
),
/// Hotel Expenses
/// Hotel Expenses
Section
sectionHeader
(
"Hotel Expenses"
,
onAddTap:
()
{
// TODO: Add Hotel Expense
showAddHotelExpenseSheet
(
context
,
hotelExpenses
,
()
=>
setState
(()
{}),
provider
,
hotelImages
,
);
}),
e
xpenseList
(
hotelExpenses
),
if
(
hotelExpenses
.
isNotEmpty
)
hotelE
xpenseList
(
hotelExpenses
),
/// Other Expenses
/// Other Expenses
Section
sectionHeader
(
"Other Expenses"
,
onAddTap:
()
{
// TODO: Add Other Expense
showAddOtherExpenseSheet
(
context
,
otherExpenses
,
()
=>
setState
(()
{}),
provider
,
otherImages
,
);
}),
e
xpenseList
(
otherExpenses
),
if
(
otherExpenses
.
isNotEmpty
)
otherE
xpenseList
(
otherExpenses
),
const
SizedBox
(
height:
80
),
],
),
),
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation
.
centerFloat
,
bottomNavigationBar:
InkResponse
(
onTap:
()
{
// TODO: Submit API Call
onTap:
()
async
{
// Validate all fields first
if
(!
validateFields
())
{
return
;
}
final
provider
=
Provider
.
of
<
TourExpensesProvider
>(
context
,
listen:
false
);
provider
.
dateController
.
clear
();
tourDateError
=
null
;
final
success
=
await
provider
.
addTourBill
(
context:
context
,
placeOfVisit:
placeController
.
text
,
daAmount:
selectedDAAmount
??
""
,
tourType:
selectedTourType
??
""
,
tourDate:
provider
.
dateController
.
text
,
travelExpenses:
travelExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
hotelExpenses:
hotelExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
otherExpenses:
otherExpenses
.
map
((
e
)
=>
e
.
map
((
k
,
v
)
=>
MapEntry
(
k
,
v
as
dynamic
))).
toList
(),
travelImages:
travelImages
,
hotelImages:
hotelImages
,
otherImages:
otherImages
,
);
print
(
"image==================
$travelImages
"
);
if
(
success
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Tour Bill Submitted Successfully"
)),
);
Navigator
.
pop
(
context
,
true
);
}
else
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
provider
.
errorMessage
??
"Failed to submit bill"
)),
);
}
},
child:
Container
(
height:
45
,
...
...
@@ -204,47 +494,7 @@ class _AddBillScreenState extends State<AddBillScreen> {
),
),
);
}
/// --- Custom Widgets Below ---
Widget
textFieldNew
(
BuildContext
context
,
TextEditingController
controller
,
String
hint
,
{
bool
enabled
=
true
,
int
maxLines
=
1
})
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
TextFormField
(
controller:
controller
,
enabled:
enabled
,
maxLines:
maxLines
,
decoration:
_inputDecoration
(
hint
)
),
);
}
Widget
dropDownField
(
BuildContext
context
,
String
hint
,
List
<
String
>
items
,
String
?
value
,
Function
(
String
?)
onChanged
)
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
hint
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
)),
value:
value
,
items:
items
.
map
((
e
)
=>
DropdownMenuItem
(
value:
e
,
child:
Text
(
e
))).
toList
(),
onChanged:
onChanged
,
iconStyleData:
ddtheme
.
iconStyleData
,
// menuItemStyleData: ddtheme.menuItemStyleData,
dropdownStyleData:
ddtheme
.
dropdownStyleData
,
),
),
},
);
}
...
...
@@ -252,23 +502,31 @@ class _AddBillScreenState extends State<AddBillScreen> {
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
title
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
)),
Text
(
title
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
fontFamily:
"JakartaMedium"
,
)),
const
SizedBox
(
height:
6
),
Container
(
height:
45
,
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
.
shade400
,
width:
0.7
,
style:
BorderStyle
.
solid
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade400
,
width:
0.7
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
InkWell
(
onTap:
onAddTap
,
child:
Center
(
child:
const
Center
(
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
const
[
children:
[
Icon
(
Icons
.
add
,
color:
Colors
.
blue
),
SizedBox
(
width:
6
),
Text
(
"Add Expenses"
,
style:
TextStyle
(
color:
Colors
.
blue
,
fontSize:
14
)),
Text
(
"Add Expenses"
,
style:
TextStyle
(
color:
Colors
.
blue
,
fontSize:
14
,
fontFamily:
"JakartaMedium"
,
)),
],
),
),
...
...
@@ -279,9 +537,9 @@ class _AddBillScreenState extends State<AddBillScreen> {
);
}
Widget
e
xpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
Widget
travelE
xpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
84
,
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
...
...
@@ -289,20 +547,70 @@ class _AddBillScreenState extends State<AddBillScreen> {
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
1
20
,
margin:
const
EdgeInsets
.
only
(
right:
1
0
),
padding:
const
EdgeInsets
.
all
(
1
0
),
width:
2
0
0
,
margin:
const
EdgeInsets
.
only
(
right:
1
2
),
padding:
const
EdgeInsets
.
all
(
1
2
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/travel_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
SvgPicture
.
asset
(
exp
[
"icon"
]
??
"assets/svg/ic_default.svg"
,
height:
22
),
const
SizedBox
(
height:
6
),
Text
(
exp
[
"title"
]
??
"-"
,
style:
const
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
)),
Text
(
"₹
${exp["amount"]}
"
,
style:
const
TextStyle
(
fontSize:
12
,
color:
Colors
.
blue
)),
Text
(
exp
[
"travel_type"
]
??
"Travel"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
if
(
exp
[
"from"
]
!=
null
&&
exp
[
"to"
]
!=
null
)
...[
const
SizedBox
(
height:
2
),
Text
(
"
${exp["from"]}
→
${exp["to"]}
"
,
style:
TextStyle
(
fontSize:
12
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
],
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
...
...
@@ -310,21 +618,1134 @@ class _AddBillScreenState extends State<AddBillScreen> {
),
);
}
InputDecoration
_inputDecoration
(
String
hint
)
{
return
InputDecoration
(
hintText:
hint
,
hintStyle:
TextStyle
(
Widget
hotelExpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
itemCount:
items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
160
,
margin:
const
EdgeInsets
.
only
(
right:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/hotel_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
exp
[
"hotel_name"
]
??
"-"
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w400
,
color:
Color
(
0xFFB4BEC0
),
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
},
),
);
}
Widget
otherExpenseList
(
List
<
Map
<
String
,
String
>>
items
)
{
return
Container
(
height:
90
,
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
itemCount:
items
.
length
,
itemBuilder:
(
context
,
index
)
{
final
exp
=
items
[
index
];
return
Container
(
width:
160
,
margin:
const
EdgeInsets
.
only
(
right:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
Container
(
width:
36
,
height:
36
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
shape:
BoxShape
.
circle
,
),
filled:
true
,
fillColor:
Colors
.
grey
.
shade100
,
enabledBorder:
InputBorder
.
none
,
disabledBorder:
InputBorder
.
none
,
focusedBorder:
InputBorder
.
none
,
child:
Center
(
child:
SvgPicture
.
asset
(
"assets/svg/hrm/books_ic.svg"
,
height:
20
,
),
),
),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
exp
[
"description"
]
??
"-"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
,
color:
AppColors
.
semi_black
,
fontFamily:
"JakartaMedium"
,
),
overflow:
TextOverflow
.
ellipsis
,
),
const
SizedBox
(
height:
4
),
Text
(
"₹
${exp["amount"] ?? "0"}
"
,
style:
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
2
),
Text
(
exp
[
"date"
]
!=
null
?
exp
[
"date"
]!.
split
(
"T"
).
first
:
"-"
,
style:
TextStyle
(
fontSize:
12
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
],
),
);
},
),
);
}
Future
<
File
?>
pickFile
()
async
{
FilePickerResult
?
result
=
await
FilePicker
.
platform
.
pickFiles
(
type:
FileType
.
any
);
if
(
result
!=
null
&&
result
.
files
.
isNotEmpty
)
{
return
File
(
result
.
files
.
single
.
path
!);
}
return
null
;
}
// --- Travel Expense BottomSheet ---
Future
<
void
>
showAddTravelExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
travelExpenses
,
VoidCallback
onUpdated
,
List
<
String
>
travelTypes
,
List
<
File
>
travelImages
,
)
{
final
fromController
=
TextEditingController
();
final
toController
=
TextEditingController
();
final
fareController
=
TextEditingController
();
String
?
selectedTravelType
;
File
?
billFile
;
String
?
fromError
,
toError
,
typeError
,
fareError
,
billError
;
// Listeners to clear errors when user starts typing
fromController
.
addListener
(()
{
if
(
fromError
!=
null
&&
fromController
.
text
.
isNotEmpty
)
{
fromError
=
null
;
}
});
toController
.
addListener
(()
{
if
(
toError
!=
null
&&
toController
.
text
.
isNotEmpty
)
{
toError
=
null
;
}
});
fareController
.
addListener
(()
{
if
(
fareError
!=
null
&&
fareController
.
text
.
isNotEmpty
)
{
fareError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
// Function to update state and clear errors when fields change
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors if needed
bool
validateFields
()
{
String
?
newFromError
=
fromController
.
text
.
isEmpty
?
"From is required"
:
null
;
String
?
newToError
=
toController
.
text
.
isEmpty
?
"To is required"
:
null
;
String
?
newTypeError
=
selectedTravelType
==
null
?
"Please select type"
:
null
;
String
?
newFareError
=
fareController
.
text
.
isEmpty
?
"Fare is required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
// Only update if there are actual changes to avoid unnecessary rebuilds
if
(
fromError
!=
newFromError
||
toError
!=
newToError
||
typeError
!=
newTypeError
||
fareError
!=
newFareError
||
billError
!=
newBillError
)
{
updateState
(()
{
fromError
=
newFromError
;
toError
=
newToError
;
typeError
=
newTypeError
;
fareError
=
newFareError
;
billError
=
newBillError
;
});
}
return
newFromError
==
null
&&
newToError
==
null
&&
newTypeError
==
null
&&
newFareError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
)
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Travel Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
textControllerWidget
(
context
,
fromController
,
"From"
,
"Enter Starting Location"
,
(
value
)
{
// Clear error when user types
if
(
fromError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
fromError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
fromError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
toController
,
"To"
,
"Enter Destination Location"
,
(
value
)
{
// Clear error when user types
if
(
toError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
toError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
toError
),
const
SizedBox
(
height:
12
),
TextWidget
(
context
,
"Travel Type"
),
DropdownButtonHideUnderline
(
child:
Container
(
width:
double
.
infinity
,
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
Text
(
"Select Travel Type"
,
style:
TextStyle
(
fontSize:
14
,
color:
Color
(
0xFFB4BEC0
),
)
),
items:
travelTypes
.
map
((
t
)
=>
DropdownMenuItem
(
value:
t
,
child:
Text
(
t
,
style:
TextStyle
(
fontSize:
14
),
)
)
).
toList
(),
value:
selectedTravelType
,
onChanged:
(
val
)
{
updateState
(()
{
selectedTravelType
=
val
;
if
(
typeError
!=
null
)
typeError
=
null
;
});
},
buttonStyleData:
ButtonStyleData
(
height:
50
,
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
only
(
left:
14
,
right:
14
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
14
),
color:
AppColors
.
text_field_color
,
),
),
dropdownStyleData:
DropdownStyleData
(
maxHeight:
200
,
width:
MediaQuery
.
of
(
context
).
size
.
width
-
60
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
14
),
color:
Colors
.
white
,
),
offset:
const
Offset
(
0
,
-
5
),
scrollbarTheme:
ScrollbarThemeData
(
radius:
const
Radius
.
circular
(
40
),
thickness:
MaterialStateProperty
.
all
<
double
>(
6
),
thumbVisibility:
MaterialStateProperty
.
all
<
bool
>(
true
),
),
),
),
),
),
errorText
(
typeError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
fareController
,
"Fare Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error when user types
if
(
fareError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
fareError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
fareError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
4.0
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
flex:
5
,
child:
Text
(
"
${billFile!.path.split('/').last}
"
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
color:
AppColors
.
semi_black
,
fontSize:
11
,
fontWeight:
FontWeight
.
w600
,
),
),
),
Expanded
(
flex:
1
,
child:
InkResponse
(
onTap:
()
=>
updateState
(()
=>
billFile
=
null
),
child:
SvgPicture
.
asset
(
"assets/svg/ic_close.svg"
,
width:
15
,
height:
15
,
),
),
),
],
),
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
travelExpenses
.
add
({
"from"
:
fromController
.
text
,
"to"
:
toController
.
text
,
"travel_type"
:
selectedTravelType
!,
"amount"
:
fareController
.
text
,
});
travelImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Hotel Expense BottomSheet ---
Future
<
void
>
showAddHotelExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
hotelExpenses
,
VoidCallback
onUpdated
,
TourExpensesProvider
provider
,
List
<
File
>
hotelImages
,
)
{
final
hotelController
=
TextEditingController
();
final
amountController
=
TextEditingController
();
DateTime
?
fromDate
,
toDate
;
File
?
billFile
;
String
?
hotelError
,
fromDateError
,
toDateError
,
amountError
,
billError
;
// Listeners to clear errors when user starts typing
hotelController
.
addListener
(()
{
if
(
hotelError
!=
null
&&
hotelController
.
text
.
isNotEmpty
)
{
hotelError
=
null
;
}
});
amountController
.
addListener
(()
{
if
(
amountError
!=
null
&&
amountController
.
text
.
isNotEmpty
)
{
amountError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
// Function to update state and clear errors
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors
bool
validateFields
()
{
String
?
newHotelError
=
hotelController
.
text
.
isEmpty
?
"Hotel name required"
:
null
;
String
?
newFromDateError
=
fromDate
==
null
?
"From date required"
:
null
;
String
?
newToDateError
=
toDate
==
null
?
"To date required"
:
null
;
String
?
newAmountError
=
amountController
.
text
.
isEmpty
?
"Amount required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
if
(
hotelError
!=
newHotelError
||
fromDateError
!=
newFromDateError
||
toDateError
!=
newToDateError
||
amountError
!=
newAmountError
||
billError
!=
newBillError
)
{
updateState
(()
{
hotelError
=
newHotelError
;
fromDateError
=
newFromDateError
;
toDateError
=
newToDateError
;
amountError
=
newAmountError
;
billError
=
newBillError
;
});
}
return
newHotelError
==
null
&&
newFromDateError
==
null
&&
newToDateError
==
null
&&
newAmountError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
),
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Hotel Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
textControllerWidget
(
context
,
hotelController
,
"Hotel Name"
,
"Enter Hotel Name"
,
(
value
)
{
// Clear error
if
(
hotelError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
hotelError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
hotelError
),
const
SizedBox
(
height:
12
),
Text
(
"Stay Duration"
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
6
),
Row
(
children:
[
Expanded
(
child:
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
true
);
if
(
d
!=
null
)
{
updateState
(()
{
fromDate
=
d
;
if
(
fromDateError
!=
null
)
fromDateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Center
(
child:
Text
(
fromDate
==
null
?
"From Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
fromDate
!),
style:
TextStyle
(
fontSize:
14
,
color:
fromDate
==
null
?
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
false
);
if
(
d
!=
null
)
{
updateState
(()
{
toDate
=
d
;
if
(
toDateError
!=
null
)
toDateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Center
(
child:
Text
(
toDate
==
null
?
"To Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
toDate
!),
style:
TextStyle
(
fontSize:
14
,
color:
toDate
==
null
?
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
),
],
),
if
(
fromDateError
!=
null
)
errorText
(
fromDateError
),
if
(
toDateError
!=
null
)
errorText
(
toDateError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
amountController
,
"Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error
if
(
amountError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
amountError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
amountError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Row
(
children:
[
const
Icon
(
Icons
.
check_circle
,
color:
Colors
.
green
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
"Attached:
${billFile!.path.split('/').last}
"
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontFamily:
"JakartaMedium"
,
fontSize:
14
))),
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
red
),
onPressed:
()
=>
updateState
(()
=>
billFile
=
null
),
),
],
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
hotelExpenses
.
add
({
"hotel_name"
:
hotelController
.
text
,
"from_date"
:
fromDate
!.
toIso8601String
(),
"to_date"
:
toDate
!.
toIso8601String
(),
"amount"
:
amountController
.
text
,
});
hotelImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
// --- Other Expense BottomSheet ---
Future
<
void
>
showAddOtherExpenseSheet
(
BuildContext
context
,
List
<
Map
<
String
,
String
>>
otherExpenses
,
VoidCallback
onUpdated
,
TourExpensesProvider
provider
,
List
<
File
>
otherImages
,
)
{
final
titleController
=
TextEditingController
();
final
amountController
=
TextEditingController
();
File
?
billFile
;
DateTime
?
date
;
String
?
titleError
,
amountError
,
dateError
,
billError
;
// Listeners to clear errors when user starts typing
titleController
.
addListener
(()
{
if
(
titleError
!=
null
&&
titleController
.
text
.
isNotEmpty
)
{
titleError
=
null
;
}
});
amountController
.
addListener
(()
{
if
(
amountError
!=
null
&&
amountController
.
text
.
isNotEmpty
)
{
amountError
=
null
;
}
});
return
showModalBottomSheet
(
useSafeArea:
true
,
isDismissible:
true
,
isScrollControlled:
true
,
showDragHandle:
true
,
backgroundColor:
Colors
.
white
,
enableDrag:
true
,
context:
context
,
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setState
)
{
void
updateState
(
VoidCallback
fn
)
{
setState
(()
{
fn
();
});
}
// Function to validate fields and show errors
bool
validateFields
()
{
String
?
newDateError
=
date
==
null
?
"Date required"
:
null
;
String
?
newTitleError
=
titleController
.
text
.
isEmpty
?
"Title required"
:
null
;
String
?
newAmountError
=
amountController
.
text
.
isEmpty
?
"Amount required"
:
null
;
String
?
newBillError
=
billFile
==
null
?
"Attach bill required"
:
null
;
if
(
dateError
!=
newDateError
||
titleError
!=
newTitleError
||
amountError
!=
newAmountError
||
billError
!=
newBillError
)
{
updateState
(()
{
dateError
=
newDateError
;
titleError
=
newTitleError
;
amountError
=
newAmountError
;
billError
=
newBillError
;
});
}
return
newDateError
==
null
&&
newTitleError
==
null
&&
newAmountError
==
null
&&
newBillError
==
null
;
}
Widget
errorText
(
String
?
msg
)
=>
msg
==
null
?
const
SizedBox
()
:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
4
,
left:
4
),
child:
Text
(
msg
,
style:
TextStyle
(
color:
Colors
.
red
,
fontSize:
12
,
fontFamily:
"JakartaMedium"
,
),
),
);
return
SafeArea
(
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
15
,
vertical:
10
),
padding:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
,
),
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
"Add Other Expense"
,
style:
TextStyle
(
fontSize:
16
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
16
),
TextWidget
(
context
,
"Date"
),
GestureDetector
(
onTap:
()
async
{
final
d
=
await
provider
.
showDatePickerDialog
(
context
,
isFromDate:
false
);
if
(
d
!=
null
)
{
updateState
(()
{
date
=
d
;
if
(
dateError
!=
null
)
dateError
=
null
;
});
}
},
child:
Container
(
height:
50
,
decoration:
BoxDecoration
(
color:
AppColors
.
text_field_color
,
borderRadius:
BorderRadius
.
circular
(
14
),
),
child:
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
,
horizontal:
12
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Text
(
date
==
null
?
"Select Date"
:
DateFormat
(
"dd MMM yyyy"
).
format
(
date
!),
style:
TextStyle
(
fontSize:
14
,
color:
date
==
null
?
const
Color
(
0xFFB4BEC0
)
:
Colors
.
black
,
fontFamily:
"JakartaMedium"
,
),
),
),
),
),
errorText
(
dateError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
titleController
,
"Description"
,
"Enter Title"
,
(
value
)
{
// Clear error
if
(
titleError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
titleError
=
null
);
}
},
TextInputType
.
text
,
false
,
null
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
titleError
),
const
SizedBox
(
height:
12
),
textControllerWidget
(
context
,
amountController
,
"Amount"
,
"Enter Amount"
,
(
value
)
{
// Clear error
if
(
amountError
!=
null
&&
value
.
isNotEmpty
)
{
updateState
(()
=>
amountError
=
null
);
}
},
TextInputType
.
number
,
false
,
FilteringTextInputFormatter
.
digitsOnly
,
null
,
null
,
TextInputAction
.
next
,
),
errorText
(
amountError
),
const
SizedBox
(
height:
12
),
InkResponse
(
onTap:
()
async
{
final
f
=
await
pickImage
(
context
);
if
(
f
!=
null
)
{
updateState
(()
{
billFile
=
f
;
if
(
billError
!=
null
)
billError
=
null
;
});
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
Color
(
0xFFE6F6FF
),
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
AppColors
.
app_blue
,
width:
0.5
),
),
child:
Center
(
child:
Text
(
billFile
==
null
?
"Attach Bill"
:
"Bill Attached"
,
style:
TextStyle
(
fontFamily:
"JakartaMedium"
,
color:
AppColors
.
app_blue
,
),
),
),
),
),
errorText
(
billError
),
if
(
billFile
!=
null
)
...[
const
SizedBox
(
height:
10
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
4.0
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Expanded
(
flex:
5
,
child:
Text
(
"
${billFile!.path.split('/').last}
"
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
color:
AppColors
.
semi_black
,
fontSize:
11
,
fontWeight:
FontWeight
.
w600
,
),
),
),
Expanded
(
flex:
1
,
child:
InkResponse
(
onTap:
()
=>
updateState
(()
=>
billFile
=
null
),
child:
SvgPicture
.
asset
(
"assets/svg/ic_close.svg"
,
width:
15
,
height:
15
,
),
),
),
],
),
)
],
const
SizedBox
(
height:
20
),
InkResponse
(
onTap:
()
{
// Validate all fields
if
(
validateFields
())
{
otherExpenses
.
add
({
"description"
:
titleController
.
text
,
"amount"
:
amountController
.
text
,
"date"
:
date
!.
toIso8601String
(),
});
otherImages
.
add
(
billFile
!);
onUpdated
();
Navigator
.
pop
(
context
);
}
},
child:
Container
(
height:
45
,
decoration:
BoxDecoration
(
color:
AppColors
.
app_blue
,
borderRadius:
BorderRadius
.
circular
(
15
),
),
child:
Center
(
child:
Text
(
"Submit"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontFamily:
"JakartaMedium"
,
fontSize:
15
,
),
),
),
),
),
],
),
),
),
);
},
);
},
);
}
}
\ No newline at end of file
lib/screens/hrm/AttendanceRequestDetail.dart
View file @
f6fbe101
...
...
@@ -24,24 +24,6 @@ class _AttendanceRequestDetailScreenState
extends
State
<
AttendanceRequestDetailScreen
>
{
late
AttendanceDetailsProvider
provider
;
// @override
// void initState() {
// super.initState();
//
// /// fetch API after widget is built
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final home = Provider.of<HomescreenNotifier>(context, listen: false);
// provider = Provider.of<AttendanceDetailsProvider>(context, listen: false);
//
// provider.fetchAttendanceRequestDetail(
// context,
// widget.attendanceListId
// );
// });
// }
@override
Widget
build
(
BuildContext
context
)
{
return
ChangeNotifierProvider
(
...
...
@@ -49,6 +31,14 @@ class _AttendanceRequestDetailScreenState
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
,
...
...
@@ -61,10 +51,10 @@ class _AttendanceRequestDetailScreenState
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
SvgPicture
.
asset
(
"assets/svg/appbar_back_button.svg"
,
height:
25
,
height:
25
*
scaleFactor
,
),
),
SizedBox
(
width:
10
),
SizedBox
(
width:
10
*
scaleFactor
),
InkResponse
(
onTap:
()
=>
Navigator
.
pop
(
context
,
true
),
child:
Text
(
...
...
@@ -98,47 +88,46 @@ class _AttendanceRequestDetailScreenState
/// scr
return
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
16.0
),
padding:
EdgeInsets
.
all
(
16.0
*
scaleFactor
),
child:
Column
(
children:
[
Card
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
16
),
borderRadius:
BorderRadius
.
circular
(
16
*
scaleFactor
),
),
elevation:
2
,
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
padding:
EdgeInsets
.
all
(
16.0
*
scaleFactor
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
0.5
),
padding:
const
EdgeInsets
.
all
(
12
),
margin:
EdgeInsets
.
only
(
bottom:
0.5
*
scaleFactor
),
padding:
EdgeInsets
.
all
(
12
*
scaleFactor
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
borderRadius:
BorderRadius
.
circular
(
12
*
scaleFactor
),
),
child:
Row
(
children:
[
/// Left Avatar
Container
(
height:
48
,
width:
48
,
height:
48
*
scaleFactor
,
width:
48
*
scaleFactor
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
const
Color
(
0xFFEDF8FF
),
// icon bg
),
child:
Center
(
child:
SvgPicture
.
asset
(
height:
28
,
width:
28
,
height:
28
*
scaleFactor
,
width:
28
*
scaleFactor
,
"assets/svg/hrm/attendanceList.svg"
,
fit:
BoxFit
.
contain
,
),
),
),
const
SizedBox
(
width:
12
),
SizedBox
(
width:
12
*
scaleFactor
),
/// Middle text
Expanded
(
...
...
@@ -148,22 +137,24 @@ class _AttendanceRequestDetailScreenState
children:
[
Text
(
details
.
type
??
"-"
,
style:
const
TextStyle
(
style:
TextStyle
(
decoration:
TextDecoration
.
underline
,
decorationStyle:
TextDecorationStyle
.
dotted
,
decorationColor:
AppColors
.
grey_thick
,
height:
1.2
,
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
semi_black
,
),
),
const
SizedBox
(
height:
2
),
SizedBox
(
height:
2
*
scaleFactor
),
Text
(
details
.
date
??
"-"
,
style:
const
TextStyle
(
fontSize:
12
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
Color
(
0xff818181
),
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
app_blue
,
),
),
],
...
...
@@ -172,10 +163,12 @@ class _AttendanceRequestDetailScreenState
/// Right side (Live/Manual)
Container
(
height:
30
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
height:
30
*
scaleFactor
,
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
*
scaleFactor
,
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
6
),
borderRadius:
BorderRadius
.
circular
(
6
*
scaleFactor
),
color:
getDecorationColor
(
details
.
status
)
),
child:
Center
(
...
...
@@ -189,97 +182,100 @@ class _AttendanceRequestDetailScreenState
),
),
),
],
),
),
// Employee Details
_buildSectionHeader
(
"Employee Details"
),
_buildDetailTile
(
"Employee Name"
,
details
.
employeeName
),
_buildDetailTile
(
"Created Employee"
,
details
.
createdEmpName
),
_buildSectionHeader
(
"Employee Details"
,
scaleFactor
),
_buildDetailTile
(
"Employee Name"
,
details
.
employeeName
,
scaleFactor
),
_buildDetailTile
(
"Created Employee"
,
details
.
createdEmpName
,
scaleFactor
),
// Check In/Out
_buildSectionHeader
(
"Check In/Out Details"
),
_buildDate_TimeTile
(
"Check In Date & Time"
,
details
.
date
,
details
.
checkInTime
),
_buildDate_TimeTile
(
"Check Out Date & Time"
,
details
.
date
,
details
.
checkOutTime
),
_buildDetailTile
(
"Original Check In"
,
details
.
checkInTime
),
_buildDetailTile
(
"Original Check Out"
,
"--"
),
_buildDetailTile
(
"Original Check In Location"
,
details
.
checkInLocation
),
_buildDetailTile
(
"Original Check Out Location"
,
details
.
checkOutLocation
),
buildLocationTile
(
"Location"
,
details
.
location
),
_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
_buildSectionHeader
(
"Proofs"
),
_buildProofLink
(
context
,
"Check In Proof"
,
details
.
checkInProofDirFilePath
),
_buildProofLink
(
context
,
"Check Out Proof"
,
details
.
checkOutProofDirFilePath
),
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"
),
_buildDetailTile
(
"Level 1 Approved By"
,
details
.
level1EmpName
),
_buildDetailTile
(
"Level 2 Approved By"
,
details
.
level2EmpName
),
_buildDetailTile
(
"Level 1 Remark"
,
details
.
level1Remarks
),
_buildDetailTile
(
"Level 2 Remark"
,
details
.
level2Remarks
),
_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"
),
_buildDetailTile
(
"Check In Type"
,
details
.
checkInType
),
_buildDetailTile
(
"Check Out Type"
,
details
.
chechOutType
),
_buildDetailTile
(
"Check Out Time"
,
details
.
checkOutTime
),
_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
),
_buildDetailTile
(
"Attendance Type"
,
details
.
attendanceType
),
_buildDetailTile
(
"Note"
,
details
.
note
),
_buildDetailTile
(
"Created Datetime"
,
details
.
requestedDatetime
),
_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
,
)
SizedBox
(
height:
30
*
scaleFactor
)
,
],
),
);
},
),
);
},
)
);
}
/// 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 wraps
children:
[
// Label
Expanded
(
flex:
6
,
flex:
5
,
// keep same ratio as other tiles
child:
Text
(
label
,
style:
const
TextStyle
(
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
Color
(
0xff2D2D2D
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
semi_black
,
),
),
),
SizedBox
(
width:
4
,),
// Value
Expanded
(
flex:
0
,
child:
Text
(
value
??
"-"
,
flex:
5
,
// take remaining width
child:
Text
(
value
??
"-"
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
)
color:
Color
(
0xFF818181
),
),
softWrap:
true
,
overflow:
TextOverflow
.
visible
,
// wrap instead of clipping
),
),
],
...
...
@@ -287,37 +283,42 @@ class _AttendanceRequestDetailScreenState
);
}
/// for location
Widget
buildLocationTile
(
String
label
,
String
?
value
)
{
Widget
buildLocationTile
(
String
label
,
String
?
value
,
double
scaleFactor
)
{
return
FutureBuilder
<
String
>(
future:
getReadableLocation
(
value
),
builder:
(
context
,
snapshot
)
{
final
locationText
=
snapshot
.
data
??
"-"
;
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
3
),
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
// aligns top when wrapping
children:
[
// Label
Expanded
(
flex:
6
,
flex:
5
,
// ratio (adjust same as your Date/Time tile)
child:
Text
(
label
,
style:
const
TextStyle
(
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
Color
(
0xff2D2D2D
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
semi_black
,
),
),
),
// Value (Clickable Location)
Expanded
(
flex:
0
,
flex:
5
,
// take remaining space
child:
GestureDetector
(
onTap:
()
async
{
final
uri
=
Uri
.
parse
(
"https://www.google.com/maps/search/?api=1&query=
$value
"
);
final
uri
=
Uri
.
parse
(
"https://www.google.com/maps/search/?api=1&query=
$value
"
);
if
(
await
canLaunchUrl
(
uri
))
{
await
launchUrl
(
uri
,
mode:
LaunchMode
.
externalApplication
);
await
launchUrl
(
uri
,
mode:
LaunchMode
.
externalApplication
);
}
},
child:
Text
(
...
...
@@ -326,10 +327,10 @@ class _AttendanceRequestDetailScreenState
fontSize:
14
,
color:
Colors
.
blue
,
decoration:
TextDecoration
.
underline
,
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
),
softWrap:
true
,
overflow:
TextOverflow
.
visible
,
),
),
),
...
...
@@ -339,6 +340,7 @@ class _AttendanceRequestDetailScreenState
},
);
}
Future
<
String
>
getReadableLocation
(
String
?
value
)
async
{
if
(
value
==
null
)
return
"-"
;
try
{
...
...
@@ -353,52 +355,40 @@ class _AttendanceRequestDetailScreenState
}
}
/// for date and time
Widget
_buildDate_TimeTile
(
String
label
,
String
?
date
,
String
?
time
)
{
Widget
_buildDate_TimeTile
(
String
label
,
String
?
date
,
String
?
time
,
double
scaleFactor
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
3
),
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
// align top when wrapped
children:
[
// Label
Expanded
(
flex:
5
,
flex:
5
,
// adjust ratio
child:
Text
(
label
,
style:
const
TextStyle
(
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
Color
(
0xff2D2D2D
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
color:
AppColors
.
semi_black
,
),
),
),
SizedBox
(
width:
4
,),
// Value (date + time)
Expanded
(
flex:
0
,
child:
Row
(
children:
[
Text
(
'
$date
, '
??
"-"
,
flex:
5
,
// adjust ratio so both fill row
child:
Text
(
'
${date ?? "-"}
,
${time ?? "-"}
'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
)
),
Text
(
time
??
"-"
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Color
(
0xff818181
),
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
)
softWrap:
true
,
// allow wrapping
overflow:
TextOverflow
.
visible
,
),
],
)
),
],
),
...
...
@@ -406,51 +396,47 @@ class _AttendanceRequestDetailScreenState
}
///////////////////////
Widget
_buildSectionHeader
(
String
title
)
{
Widget
_buildSectionHeader
(
String
title
,
double
scaleFactor
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
8
),
padding:
EdgeInsets
.
symmetric
(
vertical:
9
*
scaleFactor
),
child:
Row
(
children:
[
Text
(
title
,
style:
const
TextStyle
(
fontStyle:
FontStyle
.
normal
,
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w600
,
style:
TextStyle
(
fontSize:
14
,
fontFamily:
"JakartaSemiBold"
,
),
),
const
SizedBox
(
width:
10
),
SizedBox
(
width:
10
*
scaleFactor
),
Expanded
(
child:
DottedLine
(
dashLength:
4
,
dashGapLength:
2
,
lineThickness:
1
,
dashColor:
Color
(
0xff888888
),
)
dashGapLength:
4
,
dashGapColor:
Colors
.
white
,
dashColor:
AppColors
.
grey_semi
,
dashLength:
2
,
lineThickness:
0.5
,
),
),
],
),
);
}
/// Proof section (image/file path)
Widget
_buildProofLink
(
BuildContext
context
,
String
label
,
String
?
filePath
)
{
Widget
_buildProofLink
(
BuildContext
context
,
String
label
,
String
?
filePath
,
double
scaleFactor
)
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
6
),
padding:
EdgeInsets
.
symmetric
(
vertical:
6
*
scaleFactor
),
child:
Row
(
children:
[
Expanded
(
flex:
5
,
child:
Text
(
label
,
style:
const
TextStyle
(
fontSize:
13
,
fontWeight:
FontWeight
.
w500
,
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
),
...
...
@@ -459,17 +445,13 @@ class _AttendanceRequestDetailScreenState
child:
filePath
!=
null
?
InkWell
(
onTap:
()
{
showDialog
(
context:
context
,
builder:
(
_
)
=>
Dialog
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Fileviewer
(
fileName:
label
,
fileUrl:
filePath
,),
),
print
(
"++++++++++++++++ImageUrel:
$filePath
"
);
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
Image
.
network
(
filePath
),
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
),
);
},
...
...
@@ -492,7 +474,6 @@ class _AttendanceRequestDetailScreenState
);
}
Color
getTextColor
(
value
)
{
var
color
=
AppColors
.
approved_text_color
;
switch
(
value
)
{
...
...
@@ -534,9 +515,4 @@ class _AttendanceRequestDetailScreenState
}
return
color
;
}
}
\ No newline at end of file
lib/screens/hrm/Attendancelist.dart
View file @
f6fbe101
import
'package:dropdown_button2/dropdown_button2.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:generp/Utils/GlobalConstants.dart'
;
import
'package:generp/screens/hrm/AddManualAttendance.dart'
;
import
'package:generp/screens/hrm/AttendanceRequestDetail.dart'
;
import
'package:provider/provider.dart'
;
import
'../../Notifiers/hrmProvider/attendanceListProvider.dart'
;
import
'../../Models/hrmModels/attendanceRequestListResponse.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'../../Utils/commonWidgets.dart'
;
import
'../CommonFilter2.dart'
;
import
'../commonDateRangeFilter.dart'
;
import
'AddLiveAttendance.dart'
;
import
'package:intl/intl.dart'
;
Map
<
String
,
String
>
getDateRange
(
String
selectedDate
)
{
final
now
=
DateTime
.
now
();
final
formatter
=
DateFormat
(
"yyyy-MM-dd"
);
late
DateTime
from
;
late
DateTime
to
;
switch
(
selectedDate
)
{
case
"All"
:
from
=
now
;
to
=
now
;
break
;
case
"Today"
:
from
=
now
;
to
=
now
;
break
;
case
"Yesterday"
:
from
=
now
.
subtract
(
const
Duration
(
days:
1
));
to
=
now
.
subtract
(
const
Duration
(
days:
1
));
break
;
case
"This Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
+
1
,
0
);
break
;
case
"Past 7 days"
:
from
=
now
.
subtract
(
const
Duration
(
days:
6
));
to
=
now
;
break
;
case
"Last Month"
:
from
=
DateTime
(
now
.
year
,
now
.
month
-
1
,
1
);
to
=
DateTime
(
now
.
year
,
now
.
month
,
0
);
break
;
case
"Custom"
:
// For custom, you should open a date picker dialog
from
=
now
;
// Placeholder
to
=
now
;
// Placeholder
break
;
default
:
from
=
now
;
to
=
now
;
}
return
{
"from"
:
formatter
.
format
(
from
),
"to"
:
formatter
.
format
(
to
),
};
}
class
Attendancelist
extends
StatefulWidget
{
const
Attendancelist
({
super
.
key
});
...
...
@@ -70,32 +21,29 @@ class Attendancelist extends StatefulWidget {
}
class
_AttendancelistState
extends
State
<
Attendancelist
>
{
String
selectedType
=
"All"
;
String
selectedDate
=
"Today"
;
final
List
<
String
>
typeOptions
=
[
"All"
,
"Check In"
,
"Check Out"
,
"Check In/Out"
];
final
List
<
String
>
dateOptions
=
[
"Today"
,
"Yesterday"
,
"This Month"
,
"Past 7 days"
,
"Last Month"
,
"Custom"
,
];
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// final provider = Provider.of<Attendancelistprovider>(context, listen: false);
// provider.fetchAttendanceRequests(context);
// });
// }
@override
Widget
build
(
BuildContext
context
)
{
final
dateRange
=
getDateRange
(
selectedDate
);
final
fromDate
=
dateRange
[
"from"
]!;
final
toDate
=
dateRange
[
"to"
]!;
final
type
=
(
selectedType
==
"All"
)
?
""
:
selectedType
;
return
SafeArea
(
top:
false
,
child:
ChangeNotifierProvider
(
create:
(
_
)
=>
Attendancelistprovider
()..
fetchAttendanceRequests
(
context
,
type
,
""
,
""
),
child:
Consumer
<
Attendancelistprovider
>(
create:
(
_
)
{
final
provider
=
Attendancelistprovider
();
Future
.
microtask
(()
{
provider
.
fetchAttendanceRequests
(
context
);
});
return
provider
;
},
builder:
(
context
,
child
)
{
return
Consumer
<
Attendancelistprovider
>(
builder:
(
context
,
provider
,
child
)
{
return
Scaffold
(
appBar:
AppBar
(
...
...
@@ -124,193 +72,19 @@ class _AttendancelistState extends State<Attendancelist> {
),
actions:
[
InkResponse
(
onTap:
()
{
showModalBottomSheet
(
context:
context
,
shape:
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
),
builder:
(
context
)
{
return
StatefulBuilder
(
builder:
(
context
,
setModalState
)
{
return
Padding
(
padding:
const
EdgeInsets
.
all
(
16
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Text
(
"Filter Attendance"
,
style:
TextStyle
(
fontSize:
17
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
blue
,
fontFamily:
"Plus Jakarta Sans"
),
),
const
SizedBox
(
height:
16
),
/// Type Dropdown
Text
(
"Type"
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w400
,
fontFamily:
"Plus Jakarta Sans"
)
),
SizedBox
(
height:
6
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Type"
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
value:
selectedType
,
items:
typeOptions
.
map
((
type
)
=>
DropdownMenuItem
<
String
>(
value:
type
,
child:
Text
(
type
),
))
.
toList
(),
onChanged:
(
val
)
{
setModalState
(()
{
selectedType
=
val
!;
});
},
buttonStyleData:
const
ButtonStyleData
(
height:
52
,
padding:
EdgeInsets
.
zero
,
),
dropdownStyleData:
DropdownStyleData
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
8
),
),
),
),
),
),
const
SizedBox
(
height:
18
),
/// Date Dropdown
const
Text
(
"Date"
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w400
,
fontFamily:
"Plus Jakarta Sans"
)),
const
SizedBox
(
height:
6
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
DropdownButtonHideUnderline
(
child:
DropdownButton2
<
String
>(
isExpanded:
true
,
hint:
const
Text
(
"Select Date"
,
style:
TextStyle
(
fontSize:
14
),
overflow:
TextOverflow
.
ellipsis
,
),
value:
selectedDate
,
items:
dateOptions
.
map
((
date
)
=>
DropdownMenuItem
<
String
>(
value:
date
,
child:
Text
(
date
),
))
.
toList
(),
onChanged:
(
val
)
{
setModalState
(()
{
selectedDate
=
val
!;
});
},
buttonStyleData:
const
ButtonStyleData
(
height:
52
,
padding:
EdgeInsets
.
zero
,
),
dropdownStyleData:
DropdownStyleData
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
8
),
),
),
),
),
),
const
SizedBox
(
height:
26
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceEvenly
,
children:
[
Expanded
(
child:
ElevatedButton
(
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
blue
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
),
),
onPressed:
()
{
// on apply
provider
.
fetchAttendanceRequests
(
context
,
selectedType
,
""
,
""
);
Navigator
.
pop
(
context
);
},
child:
const
Text
(
"Apply"
,
style:
TextStyle
(
fontFamily:
"Plus Jakarta Sans"
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w600
,
),
),
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
OutlinedButton
(
style:
OutlinedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
side:
BorderSide
(
color:
Colors
.
grey
.
shade400
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
),
),
),
onPressed:
()
{
// on cancel
Navigator
.
pop
(
context
);
},
child:
const
Text
(
"Cancel"
,
style:
TextStyle
(
fontFamily:
"Plus Jakarta Sans"
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w600
,
),
),
),
),
],
),
const
SizedBox
(
height:
22
),
onTap:
()
async
{
final
result
=
await
CommonFilter2
().
showFilterBottomSheet
(
context
);
if
(
result
!=
null
)
{
final
provider
=
Provider
.
of
<
Attendancelistprovider
>(
context
,
listen:
false
);
],
),
);
},
);
},
provider
.
updateFiltersFromSheet
(
context
,
type:
result
[
'type'
]
??
"All"
,
selectedValue:
result
[
'selectedValue'
]
??
"This Month"
,
customRange:
result
[
'dateRange'
],
);
}
},
child:
SvgPicture
.
asset
(
"assets/svg/filter_ic.svg"
,
...
...
@@ -319,23 +93,56 @@ class _AttendancelistState extends State<Attendancelist> {
),
const
SizedBox
(
width:
20
),
],
),
backgroundColor:
const
Color
(
0xFFF6F6F8
),
body:
Column
(
children:
[
/// Filter chips - show active filters
// if (provider.selectedType != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Wrap(
// spacing: 8,
// children: [
// if (provider.selectedType != "All")
// Chip(
// label: Text('Type: ${provider.selectedType}'),
// onDeleted: () {
// provider.setTypeFilter(context, "All");
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter(context, "This Month");
// },
// ),
// ],
// ),
// ),
/// Attendance list
Expanded
(
child:
Builder
(
builder:
(
context
)
{
if
(
provider
.
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
blue
,
));
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 requests found"
));
return
const
Center
(
child:
Text
(
"No attendance records found"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
grey
),
),
);
}
final
list
=
provider
.
response
!.
requestList
!;
...
...
@@ -344,7 +151,6 @@ class _AttendancelistState extends State<Attendancelist> {
itemCount:
list
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
list
[
index
];
final
initials
=
_generateInitials
(
item
);
return
InkWell
(
borderRadius:
BorderRadius
.
circular
(
16
),
...
...
@@ -399,22 +205,18 @@ class _AttendancelistState extends State<Attendancelist> {
item
.
type
??
"-"
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
15
,
color:
Color
(
0xff2D2D2D
),
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w400
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
semi_black
,
),
),
Text
(
item
.
date
??
"-"
,
style:
const
TextStyle
(
fontSize:
12.5
,
color:
Color
(
0xff818181
),
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w400
style:
TextStyle
(
fontFamily:
"JakartaRegular"
,
fontSize:
14
,
color:
AppColors
.
grey_semi
,
),
),
],
...
...
@@ -426,9 +228,8 @@ class _AttendancelistState extends State<Attendancelist> {
item
.
attendanceType
??
"-"
,
textAlign:
TextAlign
.
right
,
style:
TextStyle
(
fontFamily:
"
Plus
Jakarta
Sans
"
,
fontFamily:
"Jakarta
Medium
"
,
fontSize:
14
,
fontWeight:
FontWeight
.
w500
,
color:
(
item
.
attendanceType
??
""
).
toLowerCase
()
==
"live"
?
Colors
.
green
:
Colors
.
orange
,
...
...
@@ -443,12 +244,11 @@ class _AttendancelistState extends State<Attendancelist> {
},
),
)
],
),
bottomNavigationBar:
Container
(
alignment:
Alignment
.
bottomCenter
,
height:
6
5
,
height:
5
4
,
decoration:
const
BoxDecoration
(
color:
Colors
.
white
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
...
...
@@ -466,6 +266,7 @@ class _AttendancelistState extends State<Attendancelist> {
),
),
).
then
((
_
)
{
provider
.
fetchAttendanceRequests
(
context
);
});
},
child:
Row
(
...
...
@@ -488,13 +289,12 @@ class _AttendancelistState extends State<Attendancelist> {
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
AddManualAttendanceScreen
(),
builder:
(
context
)
=>
const
AddManualAttendanceScreen
(),
settings:
const
RouteSettings
(
name:
'AddManualAttendanceScreen'
),
),
).
then
((
_
)
{
provider
.
fetchAttendanceRequests
(
context
);
});
},
child:
Row
(
...
...
@@ -512,21 +312,12 @@ class _AttendancelistState extends State<Attendancelist> {
),
),
);
},
),
),
);
}
/// Generate avatar initials like L1A, L1R
String
_generateInitials
(
RequestList
item
)
{
final
attType
=
(
item
.
attendanceType
?.
isNotEmpty
??
false
)
?
item
.
attendanceType
![
0
].
toUpperCase
()
:
"U"
;
final
type
=
(
item
.
type
?.
isNotEmpty
??
false
)
?
item
.
type
![
0
].
toUpperCase
()
:
"X"
;
return
"
$attType$type
"
;
},
)
);
}
...
...
@@ -567,7 +358,6 @@ class _AttendancelistState extends State<Attendancelist> {
return
AppColors
.
rejected_text_color
;
case
'Updated'
:
return
AppColors
.
processed_text_color
;
}
return
color
;
}
...
...
@@ -590,7 +380,4 @@ class _AttendancelistState extends State<Attendancelist> {
return
"Requested"
;
}
}
}
\ No newline at end of file
lib/screens/hrm/HrmDashboardScreen.dart
View file @
f6fbe101
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/svg.dart'
;
import
'package:generp/screens/hrm/OrganizationStructureScreen.dart'
;
import
'package:generp/screens/hrm/RewardListScreen.dart'
;
import
'../../Utils/app_colors.dart'
;
import
'AttendanceRequestDetail.dart'
;
...
...
@@ -121,6 +122,15 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
color:
const
Color
(
0xffEDF8FF
),
borderRadius:
BorderRadius
.
circular
(
30
),
),
child:
InkWell
(
onTap:
()
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
OrganizationStructureScreen
(),
),
);
},
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
...
...
@@ -139,17 +149,25 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
],
),
),
),
],
),
),
/// Bottom Grid Section
Padding
(
padding:
const
EdgeInsets
.
all
(
15
),
// Bottom Grid Section
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
final
itemWidth
=
180.0
;
// Fixed desired width for each item
final
availableWidth
=
constraints
.
maxWidth
;
final
crossAxisCount
=
(
availableWidth
/
itemWidth
).
floor
().
clamp
(
2
,
4
);
return
Padding
(
padding:
const
EdgeInsets
.
all
(
14
),
child:
GridView
.
count
(
crossAxisCount:
2
,
// items per row
crossAxisCount:
crossAxisCount
,
crossAxisSpacing:
8.5
,
mainAxisSpacing:
16
,
childAspectRatio:
2.0
,
// tiles height
childAspectRatio:
1.7
,
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
children:
[
...
...
@@ -176,11 +194,10 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
context
)
=>
const
LeaveApplicationScreen
(),
builder:
(
context
)
=>
const
LeaveApplication
List
Screen
(),
),
);
},
),
_buildTile
(
label:
"Rewards List"
,
...
...
@@ -212,6 +229,8 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
),
],
),
);
},
),
],
),
...
...
@@ -228,77 +247,84 @@ class _HrmdashboardScreenState extends State<HrmdashboardScreen> {
}
/// Reusable Tile Widget (Row style)
/// Reusable Tile Widget (Row style) - Updated to match design
Widget
_buildTile
({
required
String
label
,
required
String
subtitle
,
required
String
assetIcon
,
// SVG/PNG asset
instead of IconData
required
String
assetIcon
,
// SVG/PNG asset
required
Color
txtColor
,
VoidCallback
?
onTap
,
})
{
return
InkWell
(
onTap:
onTap
,
borderRadius:
BorderRadius
.
circular
(
20
),
borderRadius:
BorderRadius
.
circular
(
14
),
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
vertical:
5
,
horizontal:
15
,
),
margin:
EdgeInsets
.
symmetric
(
vertical:
7
,
horizontal:
5
,
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
20
),
borderRadius:
BorderRadius
.
circular
(
14
),
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
14
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
/// Left side text
Expanded
(
flex:
2
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
label
,
style:
TextStyle
(
fontSize:
15
,
fontFamily:
"Plus Jakarta Sans"
,
fontStyle:
FontStyle
.
normal
,
fontWeight:
FontWeight
.
w500
,
color:
txtColor
,
fontSize:
14
,
color:
AppColors
.
app_blue
,
fontFamily:
"JakartaMedium"
,
),
),
const
SizedBox
(
height:
5
),
SizedBox
(
height:
4
),
Text
(
subtitle
,
style:
const
TextStyle
(
fontFamily:
"Plus Jakarta Sans"
,
fontWeight:
FontWeight
.
w400
,
style:
TextStyle
(
fontSize:
12
,
height:
1.4
,
color:
Color
(
0xff818181
)
,
color:
AppColors
.
grey_semi
,
fontFamily:
"JakartaMedium"
,
),
),
],
),
),
SizedBox
(
width:
10
),
/// Right side icon (SVG/PNG)
Expanded
(
child:
Align
(
alignment:
Alignment
.
centerRight
,
flex:
1
,
child:
Container
(
height:
4
8
,
width:
4
8
,
height:
4
2
,
width:
4
2
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
const
Color
(
0xFFEDF8FF
),
// icon bg
),
child:
Center
(
child:
SvgPicture
.
asset
(
height:
2
8
,
width:
2
8
,
height:
2
5
,
width:
2
5
,
assetIcon
,
fit:
BoxFit
.
contain
,
),
),
),
),
),
],
),
),
...
...
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