Compare commits
11 Commits
fa4bee4771
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7ba8e9d5 | ||
|
|
33099c4497 | ||
|
|
79b53b6303 | ||
|
|
8adab4c4af | ||
|
|
2fd5aff0c2 | ||
|
|
56e2c0ffaa | ||
|
|
3b3ed5e640 | ||
|
|
7cbf65e6c1 | ||
|
|
cefd2397fe | ||
|
|
1fff3b402c | ||
|
|
d5d07a901e |
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "finger_print_app",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "finger_print_app (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "finger_print_app (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
assets/images/edit2.svg
Normal file
4
assets/images/edit2.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.9098 10.8326C43.0998 11.121 43.1845 11.4662 43.1495 11.8098C43.1145 12.1534 42.962 12.4744 42.7178 12.7186L24.3318 31.1026C24.1437 31.2906 23.909 31.4252 23.6518 31.4926L15.9938 33.4926C15.7406 33.5587 15.4746 33.5573 15.2222 33.4888C14.9697 33.4202 14.7396 33.2868 14.5546 33.1018C14.3696 32.9169 14.2362 32.6867 14.1676 32.4342C14.0991 32.1818 14.0978 31.9158 14.1638 31.6626L16.1638 24.0066C16.2236 23.7774 16.3336 23.5642 16.4858 23.3826L34.9398 4.94063C35.221 4.65973 35.6023 4.50195 35.9998 4.50195C36.3973 4.50195 36.7785 4.65973 37.0598 4.94063L42.7178 10.5966C42.7877 10.6703 42.8519 10.7492 42.9098 10.8326ZM39.5358 11.6566L35.9998 8.12263L18.9638 25.1586L17.7138 29.9446L22.4998 28.6946L39.5358 11.6566Z" fill="#F7F9EC"/>
|
||||
<path d="M39.282 34.3202C39.8287 29.6481 40.0032 24.9399 39.804 20.2402C39.7993 20.1294 39.8177 20.0189 39.858 19.9156C39.8983 19.8123 39.9596 19.7185 40.038 19.6402L42.006 17.6722C42.0598 17.6181 42.1281 17.5807 42.2026 17.5644C42.2771 17.5482 42.3547 17.5539 42.4261 17.5807C42.4975 17.6075 42.5596 17.6544 42.605 17.7157C42.6504 17.7769 42.6772 17.85 42.682 17.9262C43.0514 23.5086 42.9109 29.1132 42.262 34.6702C41.79 38.7142 38.542 41.8842 34.516 42.3342C27.5267 43.1077 20.4734 43.1077 13.484 42.3342C9.46003 41.8842 6.21003 38.7142 5.73803 34.6702C4.91024 27.5809 4.91024 20.4194 5.73803 13.3302C6.21003 9.28615 9.45803 6.11615 13.484 5.66615C18.7889 5.08025 24.1334 4.93776 29.462 5.24015C29.5383 5.24563 29.6114 5.27284 29.6726 5.31858C29.7339 5.36431 29.7808 5.42664 29.8077 5.49819C29.8347 5.56975 29.8405 5.64752 29.8246 5.7223C29.8087 5.79709 29.7717 5.86575 29.718 5.92015L27.732 7.90415C27.6544 7.98185 27.5616 8.04272 27.4594 8.08298C27.3572 8.12323 27.2478 8.14202 27.138 8.13815C22.6915 7.98585 18.2399 8.1563 13.818 8.64815C12.5259 8.79117 11.3197 9.3656 10.3943 10.2787C9.46894 11.1917 8.87837 12.3901 8.71803 13.6802C7.91574 20.5368 7.91574 27.4635 8.71803 34.3202C8.87837 35.6102 9.46894 36.8086 10.3943 37.7216C11.3197 38.6347 12.5259 39.2091 13.818 39.3522C20.528 40.1022 27.472 40.1022 34.184 39.3522C35.4761 39.2091 36.6823 38.6347 37.6077 37.7216C38.5331 36.8086 39.1217 35.6102 39.282 34.3202Z" fill="#F7F9EC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
4
assets/images/faceLogin.svg
Normal file
4
assets/images/faceLogin.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="97" height="97" viewBox="0 0 97 97" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 33.0891C8.42033 24.6016 9.67729 19.3071 13.4441 15.5483C17.2029 11.7815 22.4975 10.5245 30.985 10.1042M84.7917 33.0891C84.3713 24.6016 83.1144 19.3071 79.3475 15.5483C75.5888 11.7815 70.2942 10.5245 61.8067 10.1042M61.8067 86.8959C70.2942 86.4755 75.5888 85.2186 79.3475 81.4517C83.1144 77.693 84.3713 72.3984 84.7917 63.9109M30.985 86.8959C22.4975 86.4755 17.2029 85.2186 13.4441 81.4517C9.67729 77.693 8.42033 72.3984 8 63.9109M68.625 68.7084L67.8086 65.277C67.4766 63.8846 66.7807 62.6053 65.7923 61.57C64.8038 60.5346 63.5581 59.7803 62.1826 59.3842L52.4583 56.5793V50.6502C56.0797 48.205 58.5208 43.6339 58.5208 38.3959C58.5208 30.5833 53.0888 24.25 46.3958 24.25C39.6988 24.25 34.2708 30.5833 34.2708 38.3959C34.2708 43.6339 36.708 48.205 40.3333 50.6502V56.5793L30.6697 59.4085C29.3354 59.7992 28.1243 60.5274 27.1537 61.5229C26.183 62.5183 25.4855 63.7473 25.1286 65.0911L24.1667 68.7084" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="4" y1="50" x2="86" y2="50" stroke="#059C26" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
assets/images/faceLogout.svg
Normal file
4
assets/images/faceLogout.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="97" height="97" viewBox="0 0 97 97" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 33.0891C8.42033 24.6016 9.67729 19.3071 13.4441 15.5483C17.2029 11.7815 22.4975 10.5245 30.985 10.1042M84.7917 33.0891C84.3713 24.6016 83.1144 19.3071 79.3475 15.5483C75.5888 11.7815 70.2942 10.5245 61.8067 10.1042M61.8067 86.8959C70.2942 86.4755 75.5888 85.2186 79.3475 81.4517C83.1144 77.693 84.3713 72.3984 84.7917 63.9109M30.985 86.8959C22.4975 86.4755 17.2029 85.2186 13.4441 81.4517C9.67729 77.693 8.42033 72.3984 8 63.9109M68.625 68.7084L67.8086 65.277C67.4766 63.8846 66.7807 62.6053 65.7923 61.57C64.8038 60.5346 63.5581 59.7803 62.1826 59.3842L52.4583 56.5793V50.6502C56.0797 48.205 58.5208 43.6339 58.5208 38.3959C58.5208 30.5833 53.0888 24.25 46.3958 24.25C39.6988 24.25 34.2708 30.5833 34.2708 38.3959C34.2708 43.6339 36.708 48.205 40.3333 50.6502V56.5793L30.6697 59.4085C29.3354 59.7992 28.1243 60.5274 27.1537 61.5229C26.183 62.5183 25.4855 63.7473 25.1286 65.0911L24.1667 68.7084" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="4" y1="50" x2="86" y2="50" stroke="#B10A0A" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
18
assets/images/logLoading.svg
Normal file
18
assets/images/logLoading.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="96" height="89" viewBox="0 0 96 89" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1218_149)">
|
||||
<mask id="mask0_1218_149" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="23" y="14" width="62" height="56">
|
||||
<path d="M76.4966 14.851L84.4026 20.397L50.1236 69.839H42.2176L23.1016 43.053L31.0076 35.678L46.1706 49.838L76.4966 14.851Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1218_149)">
|
||||
<path d="M18.0923 33.3968C17.6259 33.4009 17.1668 33.2817 16.7614 33.0511C16.464 32.884 16.203 32.6592 15.9936 32.39C15.7842 32.1208 15.6305 31.8125 15.5417 31.4832C15.4528 31.1539 15.4306 30.8101 15.4761 30.4721C15.5217 30.1341 15.6343 29.8085 15.8072 29.5145C19.1398 23.9522 29.2793 10.9693 51.4043 10.9693C60.9923 10.9693 69.4016 13.4877 76.3865 18.452C82.1355 22.5262 85.3575 27.1378 86.8924 29.359C87.0871 29.6389 87.2238 29.955 87.2945 30.2886C87.3651 30.6222 87.3684 30.9666 87.3039 31.3015C87.2395 31.6363 87.1087 31.9549 86.9193 32.2385C86.7299 32.522 86.4857 32.7648 86.201 32.9526C85.6192 33.3371 84.9106 33.48 84.2252 33.351C83.5398 33.2219 82.9317 32.831 82.5296 32.2612C79.7519 28.2666 71.2822 16.1116 51.4043 16.1116C32.0017 16.1116 23.2295 27.2969 20.3722 32.0935C20.1427 32.494 19.8103 32.826 19.4095 33.0551C19.0087 33.2843 18.554 33.4022 18.0923 33.3968Z" fill="white"/>
|
||||
<path d="M62.5512 84.2151C62.3306 84.2178 62.1105 84.1928 61.8961 84.1408C45.8469 80.1652 39.8541 64.1211 39.6104 63.4539L39.5723 63.307C39.441 62.8438 36.2207 51.8417 41.1643 45.3978C43.4304 42.4594 46.877 40.9521 51.4282 40.9521C55.6596 40.9521 58.7122 42.2675 60.8106 44.9899C62.5391 47.2128 63.2305 49.9542 63.8995 52.5954C65.3048 58.0955 66.3194 60.9839 72.16 61.2812C74.7252 61.4108 76.4105 59.9105 77.3663 58.6331C79.9505 55.1501 80.3999 49.472 78.4518 44.4593C75.949 37.9894 67.0661 25.7913 51.404 25.7913C44.7181 25.7913 38.575 27.9398 33.6487 31.9776C29.5711 35.3223 26.3406 40.0446 24.7849 44.8983C21.9 53.935 25.6837 68.1399 25.72 68.2696C25.8084 68.6002 25.8296 68.9451 25.7825 69.2841C25.7354 69.623 25.621 69.9491 25.4459 70.2431C25.2707 70.5371 25.0385 70.7931 24.7629 70.9959C24.4873 71.1987 24.1739 71.3443 23.8411 71.4241C23.1688 71.6029 22.4533 71.5112 21.8478 71.1687C21.2423 70.8262 20.795 70.2602 20.6019 69.5919C20.429 68.9437 16.3878 53.7932 19.7203 43.3495C23.3502 32.033 34.5527 20.6248 51.4092 20.6248C59.1996 20.6248 66.5597 23.2728 72.7028 28.2734C77.4597 32.1626 81.3454 37.3862 83.3781 42.608C85.9622 49.2749 85.2691 56.7576 81.6168 61.6459C79.183 64.9059 75.7173 66.6102 71.8783 66.4252C61.8771 65.924 60.0794 58.9425 58.7692 53.8503C57.421 48.6267 56.5585 46.1083 51.4092 46.1083C48.5814 46.1083 46.5953 46.8861 45.3594 48.4971C43.6741 50.7009 43.5427 54.1459 43.7294 56.6453C43.8519 58.3867 44.1472 60.1116 44.611 61.7945C45.0223 62.8317 50.379 75.9684 63.2081 79.1471C63.5397 79.2257 63.8523 79.3693 64.1279 79.5698C64.4035 79.7702 64.6365 80.0234 64.8134 80.3146C64.9903 80.6059 65.1076 80.9294 65.1584 81.2663C65.2092 81.6033 65.1926 81.947 65.1094 82.2775C64.9529 82.8353 64.6181 83.3267 64.1562 83.6765C63.6943 84.0264 63.1307 84.2155 62.5512 84.2151Z" fill="white"/>
|
||||
<path d="M41.9507 83.0294C41.591 83.0301 41.2348 82.9584 40.9034 82.8186C40.572 82.6787 40.2721 82.4735 40.0217 82.2152C33.5985 75.474 29.9652 67.936 28.5979 58.5104V58.4603C27.8305 52.2203 28.954 43.3859 34.4593 37.3119C38.5231 32.8298 44.2358 30.5516 51.4092 30.5516C59.8927 30.5516 66.5596 34.4961 70.7167 41.9408C73.7329 47.3494 74.331 52.7389 74.3466 52.9601C74.3782 53.3012 74.3422 53.6453 74.2407 53.9725C74.1392 54.2997 73.9741 54.6036 73.7549 54.867C73.5358 55.1303 73.2668 55.3478 72.9635 55.507C72.6601 55.6662 72.3283 55.7641 71.9871 55.7949C71.2987 55.8696 70.6085 55.6706 70.0655 55.2408C69.5225 54.811 69.1703 54.1849 69.085 53.4977C68.6297 50.2639 67.5832 47.1415 65.9978 44.2864C62.7759 38.6013 57.8738 35.7113 51.3902 35.7113C45.7898 35.7113 41.408 37.3966 38.3934 40.724C34.048 45.5206 33.2079 52.91 33.8042 57.7982C35.0021 66.1331 38.205 72.7637 43.8607 78.6891C44.0968 78.9346 44.2811 79.2252 44.4026 79.5434C44.5241 79.8616 44.5804 80.201 44.5682 80.5414C44.5559 80.8818 44.4753 81.2162 44.3313 81.5249C44.1872 81.8335 43.9825 82.11 43.7294 82.338C43.242 82.7798 42.6086 83.026 41.9507 83.0294Z" fill="white"/>
|
||||
<path d="M71.5411 75.6037C65.9234 75.6037 61.1476 74.048 57.3258 70.9557C49.6477 64.7693 48.7869 54.6938 48.7489 54.2686C48.6948 53.5707 48.9202 52.8798 49.3755 52.348C49.8308 51.8162 50.4787 51.4871 51.1766 51.433C51.8746 51.3789 52.5654 51.6043 53.0972 52.0596C53.629 52.5148 53.9581 53.1627 54.0122 53.8607C54.0313 54.0093 54.7987 62.2509 60.7172 66.9974C64.2191 69.7941 68.8999 70.9038 74.668 70.2557C75.3574 70.172 76.0518 70.3647 76.5995 70.7916C77.1473 71.2185 77.5037 71.8449 77.5909 72.5338C77.6287 72.8742 77.5983 73.2186 77.5015 73.5471C77.4047 73.8755 77.2435 74.1814 77.0273 74.4469C76.811 74.7124 76.5441 74.9321 76.242 75.0933C75.9399 75.2546 75.6087 75.354 75.2678 75.3859C74.0308 75.5302 72.7865 75.6029 71.5411 75.6037ZM75.9799 6.9142C73.7882 5.48818 66.0168 1.2464 51.4039 1.2464C36.0651 1.2464 28.2746 5.9324 26.5513 7.12335C26.4375 7.19322 26.331 7.27429 26.2333 7.36534C26.223 7.37532 26.2095 7.38142 26.1952 7.38263C25.9201 7.6229 25.6992 7.91908 25.5475 8.25141C25.3958 8.58374 25.3167 8.9446 25.3154 9.30992C25.3202 9.65238 25.3924 9.99053 25.5281 10.305C25.6637 10.6195 25.8601 10.9041 26.1059 11.1425C26.3517 11.381 26.6422 11.5686 26.9607 11.6946C27.2792 11.8206 27.6194 11.8825 27.9618 11.8768C28.5106 11.8764 29.0463 11.7081 29.4967 11.3945C29.571 11.3392 36.2759 6.41293 51.4091 6.41293C66.5423 6.41293 73.2835 11.3219 73.3561 11.3582C73.8163 11.6995 74.3751 11.8815 74.948 11.8768C75.2907 11.882 75.6311 11.8195 75.9496 11.6929C76.2682 11.5663 76.5585 11.378 76.8041 11.1389C77.0496 10.8997 77.2456 10.6145 77.3806 10.2994C77.5156 9.98436 77.5871 9.64575 77.5909 9.30301C77.5911 8.78962 77.4378 8.2879 77.1507 7.86225C76.8637 7.43661 76.456 7.10646 75.9799 6.9142Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1218_149">
|
||||
<rect width="96" height="89" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
3
assets/images/logSuccess.svg
Normal file
3
assets/images/logSuccess.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="96" height="89" viewBox="0 0 96 89" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70.395 17L78.301 22.546L44.022 71.988H36.116L17 45.202L24.906 37.827L40.069 51.987L70.395 17Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 223 B |
BIN
assets/images/tickmark3.png
Normal file
BIN
assets/images/tickmark3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,12 +1,29 @@
|
||||
import 'package:coda_project/data/datasources/attendance_remote_data_source.dart';
|
||||
import 'package:coda_project/data/repositories/attendance_repository_impl.dart';
|
||||
import 'package:coda_project/domain/repositories/attendance_repository.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../network/api_client.dart';
|
||||
import '../../data/datasources/auth_remote_data_source.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../data/datasources/vacation_remote_data_source.dart';
|
||||
import '../../data/datasources/advance_remote_data_source.dart';
|
||||
import '../../data/repositories/auth_repository_impl.dart';
|
||||
import '../../data/repositories/vacation_repository_impl.dart';
|
||||
import '../../data/repositories/advance_repository_impl.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../../domain/repositories/vacation_repository.dart';
|
||||
import '../../domain/repositories/advance_repository.dart';
|
||||
import '../../domain/usecases/login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||
import '../../domain/usecases/create_vacation_usecase.dart';
|
||||
import '../../domain/usecases/get_vacation_types_usecase.dart';
|
||||
import '../../domain/usecases/get_vacations_usecase.dart';
|
||||
import '../../domain/usecases/create_advance_usecase.dart';
|
||||
import '../../domain/usecases/get_advances_usecase.dart';
|
||||
import '../../presentation/blocs/login/login_bloc.dart';
|
||||
|
||||
final sl = GetIt.instance;
|
||||
|
||||
@@ -34,16 +51,50 @@ Future<void> initializeDependencies() async {
|
||||
|
||||
// Repositories
|
||||
sl.registerLazySingleton<AuthRepository>(
|
||||
() => AuthRepositoryImpl(
|
||||
remoteDataSource: sl(),
|
||||
localDataSource: sl(),
|
||||
),
|
||||
() => AuthRepositoryImpl(remoteDataSource: sl(), localDataSource: sl()),
|
||||
);
|
||||
|
||||
// Use cases
|
||||
sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
|
||||
|
||||
// Blocs will be registered here
|
||||
// Example:
|
||||
// sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
|
||||
// Blocs
|
||||
sl.registerFactory(() => LoginBloc(loginUseCase: sl()));
|
||||
|
||||
//Attendence
|
||||
sl.registerLazySingleton<AttendanceRemoteDataSource>(
|
||||
() => AttendanceRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<AttendanceRepository>(
|
||||
() => AttendanceRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => AttendanceLoginUsecase(repository: sl()));
|
||||
|
||||
sl.registerLazySingleton(() => AttendanceLogoutUseCase(repository: sl()));
|
||||
|
||||
// Vacation
|
||||
sl.registerLazySingleton<VacationRemoteDataSource>(
|
||||
() => VacationRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<VacationRepository>(
|
||||
() => VacationRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => CreateVacationUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetVacationTypesUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetVacationsUseCase(repository: sl()));
|
||||
|
||||
// Advance
|
||||
sl.registerLazySingleton<AdvanceRemoteDataSource>(
|
||||
() => AdvanceRemoteDataSourceImpl(apiClient: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<AdvanceRepository>(
|
||||
() => AdvanceRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => CreateAdvanceUseCase(repository: sl()));
|
||||
sl.registerLazySingleton(() => GetAdvancesUseCase(repository: sl()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import '../models/leave_request.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
|
||||
class RequestService {
|
||||
// Singleton implementation
|
||||
124
lib/data/datasources/advance_remote_data_source.dart
Normal file
124
lib/data/datasources/advance_remote_data_source.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/advance_request_dto.dart';
|
||||
import '../dto/advance_response_dto.dart';
|
||||
import '../dto/advances_list_response_dto.dart';
|
||||
|
||||
abstract class AdvanceRemoteDataSource {
|
||||
Future<AdvanceResponseDto> createAdvance(AdvanceRequestDto request);
|
||||
Future<AdvancesListResponseDto> getAdvances();
|
||||
}
|
||||
|
||||
class AdvanceRemoteDataSourceImpl implements AdvanceRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
AdvanceRemoteDataSourceImpl({required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<AdvanceResponseDto> createAdvance(AdvanceRequestDto request) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/SalaryInAdvance',
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AdvanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل إنشاء طلب السلفة',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل إنشاء طلب السلفة';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AdvancesListResponseDto> getAdvances() async {
|
||||
try {
|
||||
final response = await apiClient.get('/SalaryInAdvance');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AdvancesListResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب قائمة السلف',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب قائمة السلف';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
149
lib/data/datasources/attendance_remote_data_source.dart
Normal file
149
lib/data/datasources/attendance_remote_data_source.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/attendance_response_dto.dart';
|
||||
|
||||
abstract class AttendanceRemoteDataSource {
|
||||
Future<AttendanceResponseDto> login({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
});
|
||||
|
||||
Future<AttendanceResponseDto> logout({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
});
|
||||
}
|
||||
|
||||
class AttendanceRemoteDataSourceImpl implements AttendanceRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
AttendanceRemoteDataSourceImpl({required this.apiClient});
|
||||
@override
|
||||
Future<AttendanceResponseDto> login({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'EmployeeId': employeeId,
|
||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||
});
|
||||
|
||||
final response = await apiClient.post(
|
||||
'/Attendance/login',
|
||||
data: formData,
|
||||
options: Options(contentType: 'multipart/form-data'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AttendanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل تسجيل الدخول',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الدخول';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseDto> logout({
|
||||
required String employeeId,
|
||||
required File faceImage,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'EmployeeId': employeeId,
|
||||
'FaceImage': await MultipartFile.fromFile(faceImage.path),
|
||||
});
|
||||
|
||||
final response = await apiClient.post(
|
||||
'/Attendance/logout',
|
||||
data: formData,
|
||||
options: Options(contentType: 'multipart/form-data'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return AttendanceResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل تسجيل الخروج',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الخروج';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
@override
|
||||
Future<LoginResponseDto> login(LoginDto dto) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/Auth/login',
|
||||
data: dto.toJson(),
|
||||
);
|
||||
final response = await apiClient.post('/Auth/login', data: dto.toJson());
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
@@ -47,7 +44,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message = e.response?.data?['message'] ??
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل تسجيل الدخول';
|
||||
|
||||
@@ -57,8 +55,8 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
message.toString().toLowerCase().contains('incorrect')
|
||||
? 'رقم الهاتف أو كلمة المرور غير صحيحة'
|
||||
: message.toString().toLowerCase().contains('not found')
|
||||
? 'المستخدم غير موجود'
|
||||
: message;
|
||||
? 'المستخدم غير موجود'
|
||||
: message;
|
||||
|
||||
throw ServerException(
|
||||
message: customMessage,
|
||||
|
||||
@@ -4,11 +4,14 @@ abstract class UserLocalDataSource {
|
||||
Future<void> cacheUserToken(String token);
|
||||
Future<String?> getCachedUserToken();
|
||||
Future<void> clearCache();
|
||||
Future<void> cacheEmployeeId(String id);
|
||||
Future<String?> getCachedEmployeeId();
|
||||
}
|
||||
|
||||
class UserLocalDataSourceImpl implements UserLocalDataSource {
|
||||
final SharedPreferences sharedPreferences;
|
||||
static const String _tokenKey = 'user_token';
|
||||
static const String _employeeIdKey = 'employee_id';
|
||||
|
||||
UserLocalDataSourceImpl({required this.sharedPreferences});
|
||||
|
||||
@@ -25,5 +28,16 @@ class UserLocalDataSourceImpl implements UserLocalDataSource {
|
||||
@override
|
||||
Future<void> clearCache() async {
|
||||
await sharedPreferences.remove(_tokenKey);
|
||||
await sharedPreferences.remove(_employeeIdKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheEmployeeId(String id) async {
|
||||
await sharedPreferences.setString(_employeeIdKey, id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getCachedEmployeeId() async {
|
||||
return sharedPreferences.getString(_employeeIdKey);
|
||||
}
|
||||
}
|
||||
|
||||
178
lib/data/datasources/vacation_remote_data_source.dart
Normal file
178
lib/data/datasources/vacation_remote_data_source.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../dto/vacation_request_dto.dart';
|
||||
import '../dto/vacation_response_dto.dart';
|
||||
import '../dto/vacation_type_dto.dart';
|
||||
import '../dto/vacations_list_response_dto.dart';
|
||||
|
||||
abstract class VacationRemoteDataSource {
|
||||
Future<VacationResponseDto> createVacation(VacationRequestDto request);
|
||||
Future<VacationTypesResponseDto> getVacationTypes();
|
||||
Future<VacationsListResponseDto> getVacations();
|
||||
}
|
||||
|
||||
class VacationRemoteDataSourceImpl implements VacationRemoteDataSource {
|
||||
final ApiClient apiClient;
|
||||
|
||||
VacationRemoteDataSourceImpl({required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<VacationResponseDto> createVacation(VacationRequestDto request) async {
|
||||
try {
|
||||
final response = await apiClient.post(
|
||||
'/Vacation',
|
||||
data: request.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل إنشاء طلب الإجازة',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل إنشاء طلب الإجازة';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VacationTypesResponseDto> getVacationTypes() async {
|
||||
try {
|
||||
final response = await apiClient.get('/enums/vacation-types');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationTypesResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب أنواع الإجازات',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب أنواع الإجازات';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VacationsListResponseDto> getVacations() async {
|
||||
try {
|
||||
final response = await apiClient.get('/Vacation');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
return VacationsListResponseDto.fromJson(responseData);
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'استجابة غير صحيحة من الخادم',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw ServerException(
|
||||
message: 'فشل جلب قائمة الإجازات',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw NetworkException(message: 'انتهت مهلة الاتصال');
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw NetworkException(message: 'لا يوجد اتصال بالانترنيت');
|
||||
} else if (e.response?.statusCode == 500) {
|
||||
throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا');
|
||||
} else if (e.response != null) {
|
||||
final message =
|
||||
e.response?.data?['message'] ??
|
||||
e.response?.data?['error'] ??
|
||||
'فشل جلب قائمة الإجازات';
|
||||
|
||||
throw ServerException(
|
||||
message: message.toString(),
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} else {
|
||||
throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ServerException || e is NetworkException) {
|
||||
rethrow;
|
||||
}
|
||||
print('خطأ غير متوقع: $e');
|
||||
throw ServerException(message: 'خطأ غير متوقع');
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/data/dto/advance_request_dto.dart
Normal file
22
lib/data/dto/advance_request_dto.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
class AdvanceRequestDto {
|
||||
final String employeeId;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String reason;
|
||||
|
||||
AdvanceRequestDto({
|
||||
required this.employeeId,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'employeeId': employeeId,
|
||||
'date': date.toIso8601String(),
|
||||
'amount': amount,
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
}
|
||||
86
lib/data/dto/advance_response_dto.dart
Normal file
86
lib/data/dto/advance_response_dto.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
class AdvanceResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvanceDataDto? data;
|
||||
|
||||
AdvanceResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory AdvanceResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvanceResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? AdvanceDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdvanceDataDto {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final String reason;
|
||||
final int state;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
AdvanceDataDto({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.reason,
|
||||
required this.state,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
|
||||
factory AdvanceDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvanceDataDto(
|
||||
employeeId: json['employeeId']?.toString() ?? '',
|
||||
employeeFullName: json['employeeFullName'],
|
||||
date: _parseDateTime(json['date'])!,
|
||||
amount: (json['amount'] is int) ? (json['amount'] as int).toDouble() : (json['amount'] as num).toDouble(),
|
||||
submittedBy: json['submittedBy'],
|
||||
submittedByUser: json['submittedByUser'],
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
state: json['state'] ?? 0,
|
||||
id: json['id']?.toString() ?? '',
|
||||
createdAt: _parseDateTime(json['createdAt']),
|
||||
updatedAt: _parseDateTime(json['updatedAt']),
|
||||
deletedAt: _parseDateTime(json['deletedAt']),
|
||||
isDeleted: json['isDeleted'],
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
54
lib/data/dto/advances_list_response_dto.dart
Normal file
54
lib/data/dto/advances_list_response_dto.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'advance_response_dto.dart';
|
||||
|
||||
class AdvancesListResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvancesListDataDto? data;
|
||||
|
||||
AdvancesListResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory AdvancesListResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvancesListResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? AdvancesListDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancesListDataDto {
|
||||
final List<AdvanceDataDto> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
AdvancesListDataDto({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
factory AdvancesListDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return AdvancesListDataDto(
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List)
|
||||
.map((item) => AdvanceDataDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
pageNumber: json['pageNumber'] ?? 1,
|
||||
pageSize: json['pageSize'] ?? 15,
|
||||
totalCount: json['totalCount'] ?? 0,
|
||||
totalPages: json['totalPages'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/data/dto/attendance_response_dto.dart
Normal file
38
lib/data/dto/attendance_response_dto.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
class AttendanceResponseDto {
|
||||
final String id;
|
||||
final String employeeId;
|
||||
final DateTime? login;
|
||||
final DateTime? logout;
|
||||
|
||||
AttendanceResponseDto({
|
||||
required this.id,
|
||||
required this.employeeId,
|
||||
this.login,
|
||||
this.logout,
|
||||
});
|
||||
|
||||
factory AttendanceResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
final data = json['data'];
|
||||
|
||||
return AttendanceResponseDto(
|
||||
id: data['id']?.toString() ?? '',
|
||||
employeeId: data['employeeId']?.toString() ?? '',
|
||||
login: _parseDateTime(data['login']),
|
||||
logout: _parseDateTime(data['logout']),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -57,15 +57,17 @@ class LoginDataDto {
|
||||
return LoginDataDto(
|
||||
token: json['token'],
|
||||
id: json['id'],
|
||||
employeeId: json['employeeId'],
|
||||
employeeId:
|
||||
json['employeeId'] ?? json['EmployeeId'] ?? json['employee_id'],
|
||||
username: json['username'],
|
||||
fullName: json['fullName'],
|
||||
role: json['role'],
|
||||
email: json['email'],
|
||||
phoneNumber: json['phoneNumber'],
|
||||
permissions: json['permissions'] != null
|
||||
? List<String>.from(json['permissions'])
|
||||
: null,
|
||||
permissions:
|
||||
json['permissions'] != null
|
||||
? List<String>.from(json['permissions'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
25
lib/data/dto/vacation_request_dto.dart
Normal file
25
lib/data/dto/vacation_request_dto.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class VacationRequestDto {
|
||||
final String employeeId;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final int type;
|
||||
|
||||
VacationRequestDto({
|
||||
required this.employeeId,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'employeeId': employeeId,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate.toIso8601String(),
|
||||
'reason': reason,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
}
|
||||
89
lib/data/dto/vacation_response_dto.dart
Normal file
89
lib/data/dto/vacation_response_dto.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
class VacationResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final VacationDataDto? data;
|
||||
|
||||
VacationResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
required this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory VacationResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'] ?? '',
|
||||
data: json['data'] != null ? VacationDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationDataDto {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final int state;
|
||||
final int type;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
VacationDataDto({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.state,
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
|
||||
factory VacationDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationDataDto(
|
||||
employeeId: json['employeeId']?.toString() ?? '',
|
||||
employeeFullName: json['employeeFullName'],
|
||||
startDate: _parseDateTime(json['startDate'])!,
|
||||
endDate: _parseDateTime(json['endDate'])!,
|
||||
reason: json['reason']?.toString() ?? '',
|
||||
submittedBy: json['submittedBy'],
|
||||
submittedByUser: json['submittedByUser'],
|
||||
state: json['state'] ?? 0,
|
||||
type: json['type'] ?? 0,
|
||||
id: json['id']?.toString() ?? '',
|
||||
createdAt: _parseDateTime(json['createdAt']),
|
||||
updatedAt: _parseDateTime(json['updatedAt']),
|
||||
deletedAt: _parseDateTime(json['deletedAt']),
|
||||
isDeleted: json['isDeleted'],
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
print('Error parsing date: $value - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
lib/data/dto/vacation_type_dto.dart
Normal file
43
lib/data/dto/vacation_type_dto.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
class VacationTypesResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final List<VacationTypeDto> data;
|
||||
|
||||
VacationTypesResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory VacationTypesResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationTypesResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null
|
||||
? (json['data'] as List)
|
||||
.map((item) => VacationTypeDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationTypeDto {
|
||||
final int value;
|
||||
final String name;
|
||||
|
||||
VacationTypeDto({
|
||||
required this.value,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory VacationTypeDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationTypeDto(
|
||||
value: json['value'] ?? 0,
|
||||
name: json['name']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/data/dto/vacations_list_response_dto.dart
Normal file
54
lib/data/dto/vacations_list_response_dto.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'vacation_response_dto.dart';
|
||||
|
||||
class VacationsListResponseDto {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final VacationsListDataDto? data;
|
||||
|
||||
VacationsListResponseDto({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory VacationsListResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationsListResponseDto(
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
isSuccess: json['isSuccess'] ?? false,
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? VacationsListDataDto.fromJson(json['data']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VacationsListDataDto {
|
||||
final List<VacationDataDto> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
VacationsListDataDto({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
factory VacationsListDataDto.fromJson(Map<String, dynamic> json) {
|
||||
return VacationsListDataDto(
|
||||
items: json['items'] != null
|
||||
? (json['items'] as List)
|
||||
.map((item) => VacationDataDto.fromJson(item))
|
||||
.toList()
|
||||
: [],
|
||||
pageNumber: json['pageNumber'] ?? 1,
|
||||
pageSize: json['pageSize'] ?? 15,
|
||||
totalCount: json['totalCount'] ?? 0,
|
||||
totalPages: json['totalPages'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/data/repositories/advance_repository_impl.dart
Normal file
109
lib/data/repositories/advance_repository_impl.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../datasources/advance_remote_data_source.dart';
|
||||
import '../dto/advance_request_dto.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../domain/models/advances_list_response_model.dart';
|
||||
import '../../domain/repositories/advance_repository.dart';
|
||||
|
||||
class AdvanceRepositoryImpl implements AdvanceRepository {
|
||||
final AdvanceRemoteDataSource remoteDataSource;
|
||||
|
||||
AdvanceRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AdvanceResponseModel>> createAdvance(
|
||||
AdvanceRequestModel request,
|
||||
) async {
|
||||
try {
|
||||
final dto = AdvanceRequestDto(
|
||||
employeeId: request.employeeId,
|
||||
date: request.date,
|
||||
amount: request.amount,
|
||||
reason: request.reason,
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.createAdvance(dto);
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = AdvanceResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? AdvanceDataModel(
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
employeeFullName: responseDto.data!.employeeFullName,
|
||||
date: responseDto.data!.date,
|
||||
amount: responseDto.data!.amount,
|
||||
submittedBy: responseDto.data!.submittedBy,
|
||||
submittedByUser: responseDto.data!.submittedByUser,
|
||||
reason: responseDto.data!.reason,
|
||||
state: responseDto.data!.state,
|
||||
id: responseDto.data!.id,
|
||||
createdAt: responseDto.data!.createdAt,
|
||||
updatedAt: responseDto.data!.updatedAt,
|
||||
deletedAt: responseDto.data!.deletedAt,
|
||||
isDeleted: responseDto.data!.isDeleted,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AdvancesListResponseModel>> getAdvances() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getAdvances();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = AdvancesListResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? AdvancesListDataModel(
|
||||
items: responseDto.data!.items
|
||||
.map((dto) => AdvanceDataModel(
|
||||
employeeId: dto.employeeId,
|
||||
employeeFullName: dto.employeeFullName,
|
||||
date: dto.date,
|
||||
amount: dto.amount,
|
||||
submittedBy: dto.submittedBy,
|
||||
submittedByUser: dto.submittedByUser,
|
||||
reason: dto.reason,
|
||||
state: dto.state,
|
||||
id: dto.id,
|
||||
createdAt: dto.createdAt,
|
||||
updatedAt: dto.updatedAt,
|
||||
deletedAt: dto.deletedAt,
|
||||
isDeleted: dto.isDeleted,
|
||||
))
|
||||
.toList(),
|
||||
pageNumber: responseDto.data!.pageNumber,
|
||||
pageSize: responseDto.data!.pageSize,
|
||||
totalCount: responseDto.data!.totalCount,
|
||||
totalPages: responseDto.data!.totalPages,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
lib/data/repositories/attendance_repository_impl.dart
Normal file
41
lib/data/repositories/attendance_repository_impl.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import '../../domain/models/attendance_login_request.dart';
|
||||
import '../../domain/models/attendance_logout_request.dart';
|
||||
import '../../domain/models/attendance_response_model.dart';
|
||||
import '../../domain/repositories/attendance_repository.dart';
|
||||
import '../datasources/attendance_remote_data_source.dart';
|
||||
|
||||
class AttendanceRepositoryImpl implements AttendanceRepository {
|
||||
final AttendanceRemoteDataSource remoteDataSource;
|
||||
|
||||
AttendanceRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseModel> login(AttendanceLoginRequest request) async {
|
||||
final dto = await remoteDataSource.login(
|
||||
employeeId: request.employeeId,
|
||||
faceImage: request.faceImage,
|
||||
);
|
||||
|
||||
return AttendanceResponseModel(
|
||||
id: dto.id,
|
||||
employeeId: dto.employeeId,
|
||||
login: dto.login,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceResponseModel> logout(
|
||||
AttendanceLogoutRequest request,
|
||||
) async {
|
||||
final dto = await remoteDataSource.logout(
|
||||
employeeId: request.employeeId,
|
||||
faceImage: request.faceImage,
|
||||
);
|
||||
|
||||
return AttendanceResponseModel(
|
||||
id: dto.id,
|
||||
employeeId: dto.employeeId,
|
||||
logout: dto.logout,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import '../../core/error/failures.dart';
|
||||
import '../datasources/auth_remote_data_source.dart';
|
||||
import '../datasources/user_local_data_source.dart';
|
||||
import '../dto/login_dto.dart';
|
||||
import '../dto/login_response_dto.dart';
|
||||
import '../../domain/models/login_request.dart';
|
||||
import '../../domain/models/login_response_model.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
@@ -19,7 +18,9 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, LoginResponseModel>> login(LoginRequest request) async {
|
||||
Future<Either<Failure, LoginResponseModel>> login(
|
||||
LoginRequest request,
|
||||
) async {
|
||||
try {
|
||||
final dto = LoginDto(
|
||||
phoneNumber: request.phoneNumber,
|
||||
@@ -27,30 +28,38 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.login(dto);
|
||||
print("LOGIN RESPONSE DATA: ${responseDto.toJson()}"); // Debugging Log
|
||||
|
||||
// Cache the token locally
|
||||
if (responseDto.data?.token != null) {
|
||||
await localDataSource.cacheUserToken(responseDto.data!.token!);
|
||||
}
|
||||
if (responseDto.data?.employeeId != null) {
|
||||
print("AUTH_REPO: Caching EmployeeId: ${responseDto.data!.employeeId}");
|
||||
await localDataSource.cacheEmployeeId(responseDto.data!.employeeId!);
|
||||
} else {
|
||||
print("AUTH_REPO: EmployeeId is NULL in response!");
|
||||
}
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = LoginResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? LoginDataModel(
|
||||
token: responseDto.data!.token,
|
||||
id: responseDto.data!.id,
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
username: responseDto.data!.username,
|
||||
fullName: responseDto.data!.fullName,
|
||||
role: responseDto.data!.role,
|
||||
email: responseDto.data!.email,
|
||||
phoneNumber: responseDto.data!.phoneNumber,
|
||||
permissions: responseDto.data!.permissions,
|
||||
)
|
||||
: null,
|
||||
data:
|
||||
responseDto.data != null
|
||||
? LoginDataModel(
|
||||
token: responseDto.data!.token,
|
||||
id: responseDto.data!.id,
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
username: responseDto.data!.username,
|
||||
fullName: responseDto.data!.fullName,
|
||||
role: responseDto.data!.role,
|
||||
email: responseDto.data!.email,
|
||||
phoneNumber: responseDto.data!.phoneNumber,
|
||||
permissions: responseDto.data!.permissions,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
|
||||
142
lib/data/repositories/vacation_repository_impl.dart
Normal file
142
lib/data/repositories/vacation_repository_impl.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../datasources/vacation_remote_data_source.dart';
|
||||
import '../dto/vacation_request_dto.dart';
|
||||
import '../../domain/models/vacation_request.dart';
|
||||
import '../../domain/models/vacation_response_model.dart';
|
||||
import '../../domain/models/vacation_type_model.dart';
|
||||
import '../../domain/models/vacations_list_response_model.dart';
|
||||
import '../../domain/repositories/vacation_repository.dart';
|
||||
|
||||
class VacationRepositoryImpl implements VacationRepository {
|
||||
final VacationRemoteDataSource remoteDataSource;
|
||||
|
||||
VacationRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationResponseModel>> createVacation(
|
||||
VacationRequest request,
|
||||
) async {
|
||||
try {
|
||||
final dto = VacationRequestDto(
|
||||
employeeId: request.employeeId,
|
||||
startDate: request.startDate,
|
||||
endDate: request.endDate,
|
||||
reason: request.reason,
|
||||
type: request.type,
|
||||
);
|
||||
|
||||
final responseDto = await remoteDataSource.createVacation(dto);
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? VacationDataModel(
|
||||
employeeId: responseDto.data!.employeeId,
|
||||
employeeFullName: responseDto.data!.employeeFullName,
|
||||
startDate: responseDto.data!.startDate,
|
||||
endDate: responseDto.data!.endDate,
|
||||
reason: responseDto.data!.reason,
|
||||
submittedBy: responseDto.data!.submittedBy,
|
||||
submittedByUser: responseDto.data!.submittedByUser,
|
||||
state: responseDto.data!.state,
|
||||
type: responseDto.data!.type,
|
||||
id: responseDto.data!.id,
|
||||
createdAt: responseDto.data!.createdAt,
|
||||
updatedAt: responseDto.data!.updatedAt,
|
||||
deletedAt: responseDto.data!.deletedAt,
|
||||
isDeleted: responseDto.data!.isDeleted,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationTypesResponseModel>> getVacationTypes() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getVacationTypes();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationTypesResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data
|
||||
.map((dto) => VacationTypeModel(
|
||||
value: dto.value,
|
||||
name: dto.name,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, VacationsListResponseModel>> getVacations() async {
|
||||
try {
|
||||
final responseDto = await remoteDataSource.getVacations();
|
||||
|
||||
// Convert DTO to Model
|
||||
final responseModel = VacationsListResponseModel(
|
||||
statusCode: responseDto.statusCode,
|
||||
isSuccess: responseDto.isSuccess,
|
||||
message: responseDto.message,
|
||||
data: responseDto.data != null
|
||||
? VacationsListDataModel(
|
||||
items: responseDto.data!.items
|
||||
.map((dto) => VacationDataModel(
|
||||
employeeId: dto.employeeId,
|
||||
employeeFullName: dto.employeeFullName,
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
reason: dto.reason,
|
||||
submittedBy: dto.submittedBy,
|
||||
submittedByUser: dto.submittedByUser,
|
||||
state: dto.state,
|
||||
type: dto.type,
|
||||
id: dto.id,
|
||||
createdAt: dto.createdAt,
|
||||
updatedAt: dto.updatedAt,
|
||||
deletedAt: dto.deletedAt,
|
||||
isDeleted: dto.isDeleted,
|
||||
))
|
||||
.toList(),
|
||||
pageNumber: responseDto.data!.pageNumber,
|
||||
pageSize: responseDto.data!.pageSize,
|
||||
totalCount: responseDto.data!.totalCount,
|
||||
totalPages: responseDto.data!.totalPages,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
return Right(responseModel);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('خطأ غير متوقع: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
59
lib/domain/models/advance_request_model.dart
Normal file
59
lib/domain/models/advance_request_model.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
class AdvanceRequestModel {
|
||||
final String employeeId;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String reason;
|
||||
|
||||
AdvanceRequestModel({
|
||||
required this.employeeId,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
required this.reason,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvanceResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvanceDataModel? data;
|
||||
|
||||
AdvanceResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvanceDataModel {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final String reason;
|
||||
final int state;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
AdvanceDataModel({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.date,
|
||||
required this.amount,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.reason,
|
||||
required this.state,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
}
|
||||
31
lib/domain/models/advances_list_response_model.dart
Normal file
31
lib/domain/models/advances_list_response_model.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'advance_request_model.dart';
|
||||
|
||||
class AdvancesListResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final AdvancesListDataModel? data;
|
||||
|
||||
AdvancesListResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class AdvancesListDataModel {
|
||||
final List<AdvanceDataModel> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
AdvancesListDataModel({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
}
|
||||
8
lib/domain/models/attendance_login_request.dart
Normal file
8
lib/domain/models/attendance_login_request.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
class AttendanceLoginRequest {
|
||||
final String employeeId;
|
||||
final File faceImage;
|
||||
|
||||
AttendanceLoginRequest({required this.employeeId, required this.faceImage});
|
||||
}
|
||||
8
lib/domain/models/attendance_logout_request.dart
Normal file
8
lib/domain/models/attendance_logout_request.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
class AttendanceLogoutRequest {
|
||||
final String employeeId;
|
||||
final File faceImage;
|
||||
|
||||
AttendanceLogoutRequest({required this.employeeId, required this.faceImage});
|
||||
}
|
||||
13
lib/domain/models/attendance_response_model.dart
Normal file
13
lib/domain/models/attendance_response_model.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class AttendanceResponseModel {
|
||||
final String id;
|
||||
final String employeeId;
|
||||
final DateTime? login;
|
||||
final DateTime? logout;
|
||||
|
||||
AttendanceResponseModel({
|
||||
required this.id,
|
||||
required this.employeeId,
|
||||
this.login,
|
||||
this.logout,
|
||||
});
|
||||
}
|
||||
@@ -2,8 +2,5 @@ class LoginRequest {
|
||||
final String phoneNumber;
|
||||
final String password;
|
||||
|
||||
LoginRequest({
|
||||
required this.phoneNumber,
|
||||
required this.password,
|
||||
});
|
||||
LoginRequest({required this.phoneNumber, required this.password});
|
||||
}
|
||||
|
||||
15
lib/domain/models/vacation_request.dart
Normal file
15
lib/domain/models/vacation_request.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class VacationRequest {
|
||||
final String employeeId;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final int type;
|
||||
|
||||
VacationRequest({
|
||||
required this.employeeId,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
47
lib/domain/models/vacation_response_model.dart
Normal file
47
lib/domain/models/vacation_response_model.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
class VacationResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final VacationDataModel? data;
|
||||
|
||||
VacationResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
required this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationDataModel {
|
||||
final String employeeId;
|
||||
final String? employeeFullName;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String reason;
|
||||
final String? submittedBy;
|
||||
final String? submittedByUser;
|
||||
final int state;
|
||||
final int type;
|
||||
final String id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final bool? isDeleted;
|
||||
|
||||
VacationDataModel({
|
||||
required this.employeeId,
|
||||
this.employeeFullName,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.reason,
|
||||
this.submittedBy,
|
||||
this.submittedByUser,
|
||||
required this.state,
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.isDeleted,
|
||||
});
|
||||
}
|
||||
23
lib/domain/models/vacation_type_model.dart
Normal file
23
lib/domain/models/vacation_type_model.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
class VacationTypeModel {
|
||||
final int value;
|
||||
final String name;
|
||||
|
||||
VacationTypeModel({
|
||||
required this.value,
|
||||
required this.name,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationTypesResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final List<VacationTypeModel> data;
|
||||
|
||||
VacationTypesResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
required this.data,
|
||||
});
|
||||
}
|
||||
31
lib/domain/models/vacations_list_response_model.dart
Normal file
31
lib/domain/models/vacations_list_response_model.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'vacation_response_model.dart';
|
||||
|
||||
class VacationsListResponseModel {
|
||||
final int statusCode;
|
||||
final bool isSuccess;
|
||||
final String? message;
|
||||
final VacationsListDataModel? data;
|
||||
|
||||
VacationsListResponseModel({
|
||||
required this.statusCode,
|
||||
required this.isSuccess,
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class VacationsListDataModel {
|
||||
final List<VacationDataModel> items;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
|
||||
VacationsListDataModel({
|
||||
required this.items,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
});
|
||||
}
|
||||
11
lib/domain/repositories/advance_repository.dart
Normal file
11
lib/domain/repositories/advance_repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advance_request_model.dart';
|
||||
import '../models/advances_list_response_model.dart';
|
||||
|
||||
abstract class AdvanceRepository {
|
||||
Future<Either<Failure, AdvanceResponseModel>> createAdvance(
|
||||
AdvanceRequestModel request,
|
||||
);
|
||||
Future<Either<Failure, AdvancesListResponseModel>> getAdvances();
|
||||
}
|
||||
12
lib/domain/repositories/attendance_repository.dart
Normal file
12
lib/domain/repositories/attendance_repository.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import '../models/attendance_login_request.dart';
|
||||
import '../models/attendance_logout_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
|
||||
//in the following polymorphism is being used , a quich recap it is where th esame method but opperate in a different way
|
||||
|
||||
//one Repo two requests
|
||||
abstract class AttendanceRepository {
|
||||
Future<AttendanceResponseModel> login(AttendanceLoginRequest request);
|
||||
|
||||
Future<AttendanceResponseModel> logout(AttendanceLogoutRequest request);
|
||||
}
|
||||
14
lib/domain/repositories/vacation_repository.dart
Normal file
14
lib/domain/repositories/vacation_repository.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_request.dart';
|
||||
import '../models/vacation_response_model.dart';
|
||||
import '../models/vacation_type_model.dart';
|
||||
import '../models/vacations_list_response_model.dart';
|
||||
|
||||
abstract class VacationRepository {
|
||||
Future<Either<Failure, VacationResponseModel>> createVacation(
|
||||
VacationRequest request,
|
||||
);
|
||||
Future<Either<Failure, VacationTypesResponseModel>> getVacationTypes();
|
||||
Future<Either<Failure, VacationsListResponseModel>> getVacations();
|
||||
}
|
||||
15
lib/domain/usecases/attendance_login_usecase.dart
Normal file
15
lib/domain/usecases/attendance_login_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '../models/attendance_login_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
import '../repositories/attendance_repository.dart';
|
||||
|
||||
//always remmber that the usecase uses the repo
|
||||
|
||||
class AttendanceLoginUsecase {
|
||||
final AttendanceRepository repository;
|
||||
|
||||
AttendanceLoginUsecase({required this.repository});
|
||||
|
||||
Future<AttendanceResponseModel> call(AttendanceLoginRequest request) {
|
||||
return repository.login(request);
|
||||
}
|
||||
}
|
||||
13
lib/domain/usecases/attendance_logout_usecase.dart
Normal file
13
lib/domain/usecases/attendance_logout_usecase.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../models/attendance_logout_request.dart';
|
||||
import '../models/attendance_response_model.dart';
|
||||
import '../repositories/attendance_repository.dart';
|
||||
|
||||
class AttendanceLogoutUseCase {
|
||||
final AttendanceRepository repository;
|
||||
|
||||
AttendanceLogoutUseCase({required this.repository});
|
||||
|
||||
Future<AttendanceResponseModel> call(AttendanceLogoutRequest request) {
|
||||
return repository.logout(request);
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/create_advance_usecase.dart
Normal file
14
lib/domain/usecases/create_advance_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advance_request_model.dart';
|
||||
import '../repositories/advance_repository.dart';
|
||||
|
||||
class CreateAdvanceUseCase {
|
||||
final AdvanceRepository repository;
|
||||
|
||||
CreateAdvanceUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, AdvanceResponseModel>> call(AdvanceRequestModel request) {
|
||||
return repository.createAdvance(request);
|
||||
}
|
||||
}
|
||||
15
lib/domain/usecases/create_vacation_usecase.dart
Normal file
15
lib/domain/usecases/create_vacation_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_request.dart';
|
||||
import '../models/vacation_response_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class CreateVacationUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
CreateVacationUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationResponseModel>> call(VacationRequest request) {
|
||||
return repository.createVacation(request);
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_advances_usecase.dart
Normal file
14
lib/domain/usecases/get_advances_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/advances_list_response_model.dart';
|
||||
import '../repositories/advance_repository.dart';
|
||||
|
||||
class GetAdvancesUseCase {
|
||||
final AdvanceRepository repository;
|
||||
|
||||
GetAdvancesUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, AdvancesListResponseModel>> call() {
|
||||
return repository.getAdvances();
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_vacation_types_usecase.dart
Normal file
14
lib/domain/usecases/get_vacation_types_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacation_type_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class GetVacationTypesUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
GetVacationTypesUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationTypesResponseModel>> call() {
|
||||
return repository.getVacationTypes();
|
||||
}
|
||||
}
|
||||
14
lib/domain/usecases/get_vacations_usecase.dart
Normal file
14
lib/domain/usecases/get_vacations_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../models/vacations_list_response_model.dart';
|
||||
import '../repositories/vacation_repository.dart';
|
||||
|
||||
class GetVacationsUseCase {
|
||||
final VacationRepository repository;
|
||||
|
||||
GetVacationsUseCase({required this.repository});
|
||||
|
||||
Future<Either<Failure, VacationsListResponseModel>> call() {
|
||||
return repository.getVacations();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
|
||||
import 'core/di/injection_container.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
import 'presentation/screens/splash_screen.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
40
lib/presentation/blocs/login/login_bloc.dart
Normal file
40
lib/presentation/blocs/login/login_bloc.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../domain/usecases/login_usecase.dart';
|
||||
import 'login_event.dart';
|
||||
import 'login_state.dart';
|
||||
|
||||
class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
||||
final LoginUseCase loginUseCase;
|
||||
|
||||
LoginBloc({required this.loginUseCase}) : super(const LoginInitial()) {
|
||||
on<LoginSubmitted>(_onLoginSubmitted);
|
||||
on<LoginReset>(_onLoginReset);
|
||||
}
|
||||
|
||||
Future<void> _onLoginSubmitted(
|
||||
LoginSubmitted event,
|
||||
Emitter<LoginState> emit,
|
||||
) async {
|
||||
emit(const LoginLoading());
|
||||
|
||||
final result = await loginUseCase(event.request);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(LoginError(failure.message)),
|
||||
(response) {
|
||||
if (response.isSuccess) {
|
||||
emit(LoginSuccess(response));
|
||||
} else {
|
||||
emit(LoginError(response.message));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onLoginReset(
|
||||
LoginReset event,
|
||||
Emitter<LoginState> emit,
|
||||
) {
|
||||
emit(const LoginInitial());
|
||||
}
|
||||
}
|
||||
22
lib/presentation/blocs/login/login_event.dart
Normal file
22
lib/presentation/blocs/login/login_event.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/models/login_request.dart';
|
||||
|
||||
abstract class LoginEvent extends Equatable {
|
||||
const LoginEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginSubmitted extends LoginEvent {
|
||||
final LoginRequest request;
|
||||
|
||||
const LoginSubmitted(this.request);
|
||||
|
||||
@override
|
||||
List<Object> get props => [request];
|
||||
}
|
||||
|
||||
class LoginReset extends LoginEvent {
|
||||
const LoginReset();
|
||||
}
|
||||
35
lib/presentation/blocs/login/login_state.dart
Normal file
35
lib/presentation/blocs/login/login_state.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/models/login_response_model.dart';
|
||||
|
||||
abstract class LoginState extends Equatable {
|
||||
const LoginState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginInitial extends LoginState {
|
||||
const LoginInitial();
|
||||
}
|
||||
|
||||
class LoginLoading extends LoginState {
|
||||
const LoginLoading();
|
||||
}
|
||||
|
||||
class LoginSuccess extends LoginState {
|
||||
final LoginResponseModel response;
|
||||
|
||||
const LoginSuccess(this.response);
|
||||
|
||||
@override
|
||||
List<Object> get props => [response];
|
||||
}
|
||||
|
||||
class LoginError extends LoginState {
|
||||
final String message;
|
||||
|
||||
const LoginError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
84
lib/presentation/screens/about_screen.dart
Normal file
84
lib/presentation/screens/about_screen.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
/// -------------------- SETTINGS BAR --------------------
|
||||
SettingsBar(
|
||||
selectedIndex: 0,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
/// -------------------- CENTER CONTENT --------------------
|
||||
///
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"عن الشركة",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Container(
|
||||
width: 200,
|
||||
height: 1,
|
||||
color: const Color(0x8732C599),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
const Text(
|
||||
"شركة كودا ذ.م.م هي شركة عراقية رائدة تقدم حلولاً متميزة في البرمجيات المخصصة والتطبيقات وإدارة الخوادم والمواقع، ومنظومات الاتصال وخدمات الأمن السيبراني. تأسست عام 2008 ولديها عقود واتفاقيات عديدة مع مؤسسات حكومية عراقية والقطاع الخاص.\n\nوسعت الشركة شراكاتها مع وكالات عالمية مثل CODACS الأمريكية و Laipac الكندية و Teltonika اللتوانية وWolf Team الصينية وLibelium الإسبانية وOdoo العالمية وغيرها. مما أتاح لها تقديم أحدث التقنيات والمنتجات المبتكرة لعملائها.\n\nتوفر كودا حلولاً برمجية مخصصة للمؤسسات الكبيرة والدوائر الحكومية، مما يسهم في تبسيط الإجراءات وتقديم الخدمات للمواطنين بشكل إلكتروني وفعال.\n\nكما تقدم كودا مجموعة واسعة من المنتجات والخدمات الحديثة، بما في ذلك الشاشات الإعلانية، والبوابات الذكية، والمواقف الذكية، والأقفال، والمباني الذكية، وأجهزة الكشك (الخدمة الإلكترونية الذاتية)، وأجهزة محمولة تخصصية، وحساسات، وأجهزة نداء واستغاثة.\n\nبفضل هذه المنتجات والخدمات المبتكرة، تعزز كودا مكانتها الرائدة في السوق المحلي والإقليمي، وتساهم في تطوير التكنولوجيا وتعزيز جودة الحياة في العراق .",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white70,
|
||||
height:
|
||||
1.5, // Added line height for better readability
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Bottom padding
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:coda_project/screens/face_screen.dart';
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/face_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/login_animation.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../domain/models/attendance_login_request.dart';
|
||||
import '../../domain/models/attendance_logout_request.dart';
|
||||
import '../../domain/usecases/attendance_login_usecase.dart';
|
||||
import '../../domain/usecases/attendance_logout_usecase.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class AttendanceScreen extends StatelessWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
@@ -17,45 +22,43 @@ class AttendanceScreen extends StatelessWidget {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height
|
||||
SizedBox(height: MediaQuery.of(context).size.height),
|
||||
|
||||
),
|
||||
/// ------------------------------
|
||||
/// SETTINGS BAR (STATIC)
|
||||
/// ------------------------------
|
||||
SafeArea(
|
||||
child:SettingsBar(
|
||||
selectedIndex: 0,
|
||||
showBackButton: false,
|
||||
iconPaths: [
|
||||
'assets/images/user.svg',
|
||||
'assets/images/ball.svg',
|
||||
],
|
||||
onTap: (index) {
|
||||
if (index == 0) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserSettingsScreen(),
|
||||
),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
child: SettingsBar(
|
||||
selectedIndex: 0,
|
||||
showBackButton: false,
|
||||
iconPaths: ['assets/images/user.svg', 'assets/images/ball.svg'],
|
||||
onTap: (index) {
|
||||
if (index == 0) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserSettingsScreen(),
|
||||
),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// ------------------------------
|
||||
/// GREETING TEXT
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
top: screenHeight * 0.14, // moved down because settings bar now exists
|
||||
top:
|
||||
screenHeight *
|
||||
0.14, // moved down because settings bar now exists
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
@@ -65,9 +68,7 @@ class AttendanceScreen extends StatelessWidget {
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(color: Color(0x42000000), blurRadius: 6),
|
||||
],
|
||||
shadows: [Shadow(color: Color(0x42000000), blurRadius: 6)],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -77,7 +78,9 @@ class AttendanceScreen extends StatelessWidget {
|
||||
/// MAIN CARD AREA
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
top: screenHeight * 0.2, // pushed down because of settings bar + greeting
|
||||
top:
|
||||
screenHeight *
|
||||
0.2, // pushed down because of settings bar + greeting
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
@@ -110,7 +113,7 @@ class AttendanceScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: screenHeight * 0.5,
|
||||
height: screenHeight * 0.5,
|
||||
width: screenWidth * 0.7,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x92757575),
|
||||
@@ -139,15 +142,42 @@ class AttendanceScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/login.svg",
|
||||
icon: "assets/images/faceLogin.svg",
|
||||
label: "تسجيل الدخول",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OvalCameraCapturePage(
|
||||
onTap: () async {
|
||||
final employeeId =
|
||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
print("ATTENDANCE_SCREEN: Retrieved EmployeeId: $employeeId");
|
||||
if (employeeId == null) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(_) => OvalCameraCapturePage(
|
||||
isLogin: true,
|
||||
onCapture: (imageFile) async {
|
||||
final loginUseCase =
|
||||
sl<AttendanceLoginUsecase>();
|
||||
await loginUseCase(
|
||||
AttendanceLoginRequest(
|
||||
employeeId: employeeId,
|
||||
faceImage: imageFile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -180,17 +210,41 @@ class AttendanceScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/logout.svg",
|
||||
icon: "assets/images/faceLogout.svg",
|
||||
label: "تسجيل خروج",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LoginAnimationScreen(
|
||||
isLogin: false,
|
||||
isSuccess: true,
|
||||
onTap: () async {
|
||||
final employeeId =
|
||||
await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(_) => OvalCameraCapturePage(
|
||||
isLogin: false,
|
||||
onCapture: (imageFile) async {
|
||||
final logoutUseCase =
|
||||
sl<AttendanceLogoutUseCase>();
|
||||
await logoutUseCase(
|
||||
AttendanceLogoutRequest(
|
||||
employeeId: employeeId,
|
||||
faceImage: imageFile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -258,7 +312,7 @@ class _FingerButton extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(icon, width: 62, height: 62),
|
||||
SvgPicture.asset(icon, width: 75, height: 75),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
label,
|
||||
34
lib/presentation/screens/auth_screen.dart
Normal file
34
lib/presentation/screens/auth_screen.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/auth_form.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
|
||||
class AuthScreen extends StatelessWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<LoginBloc>(),
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo
|
||||
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
|
||||
// const SizedBox(height: 15),
|
||||
// Form - taking remaining space and centered
|
||||
Expanded(child: Center(child: const AuthForm())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
581
lib/presentation/screens/face_screen.dart
Normal file
581
lib/presentation/screens/face_screen.dart
Normal file
@@ -0,0 +1,581 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import '../../core/error/exceptions.dart';
|
||||
|
||||
class OvalCameraCapturePage extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
final Future<void> Function(File image) onCapture;
|
||||
|
||||
const OvalCameraCapturePage({
|
||||
super.key,
|
||||
this.isLogin = true,
|
||||
required this.onCapture,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
||||
}
|
||||
|
||||
class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
CameraController? _cameraController;
|
||||
bool _isCameraInitialized = false;
|
||||
String? _errorMessage;
|
||||
bool _isSuccess = false;
|
||||
bool _isLoading = false;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
|
||||
// Dispose existing controller if any
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
|
||||
// Get available cameras
|
||||
final cameras = await availableCameras();
|
||||
|
||||
// Check if cameras list is available
|
||||
if (cameras.isEmpty) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find front camera, fallback to first available camera
|
||||
CameraDescription? selectedCamera;
|
||||
try {
|
||||
selectedCamera = cameras.firstWhere(
|
||||
(cam) => cam.lensDirection == CameraLensDirection.front,
|
||||
);
|
||||
} catch (e) {
|
||||
// If no front camera found, use the first available camera
|
||||
if (cameras.isNotEmpty) {
|
||||
selectedCamera = cameras.first;
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة على هذا الجهاز";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_cameraController = CameraController(
|
||||
selectedCamera,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _cameraController!.initialize();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isCameraInitialized = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
_startScan();
|
||||
} on CameraException catch (e) {
|
||||
if (!mounted) return;
|
||||
String errorMessage;
|
||||
switch (e.code) {
|
||||
case 'CameraAccessDenied':
|
||||
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||
break;
|
||||
case 'CameraAccessDeniedWithoutPrompt':
|
||||
errorMessage = "تم رفض الوصول إلى الكاميرا. يرجى السماح بالوصول في الإعدادات";
|
||||
break;
|
||||
case 'CameraAccessRestricted':
|
||||
errorMessage = "الوصول إلى الكاميرا مقيد";
|
||||
break;
|
||||
case 'AudioAccessDenied':
|
||||
errorMessage = "تم رفض الوصول إلى الميكروفون";
|
||||
break;
|
||||
default:
|
||||
errorMessage = "خطأ في تهيئة الكاميرا: ${e.description ?? 'خطأ غير معروف'}";
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = errorMessage;
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = "حدث خطأ غير متوقع أثناء تهيئة الكاميرا";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
print("Error initializing camera: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startScan() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
// Simulate scanning delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (!mounted ||
|
||||
_cameraController == null ||
|
||||
!_cameraController!.value.isInitialized) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "الكاميرا غير مهيأة. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final xFile = await _cameraController!.takePicture();
|
||||
final file = File(xFile.path);
|
||||
|
||||
// Check if file exists and is readable
|
||||
if (!await file.exists()) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "فشل حفظ الصورة. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the onCapture callback which may throw exceptions
|
||||
await widget.onCapture(file);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
} on ServerException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.message;
|
||||
});
|
||||
}
|
||||
} on NetworkException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.message;
|
||||
});
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
if (mounted) {
|
||||
String errorMessage;
|
||||
switch (e.code) {
|
||||
case 'captureAlreadyActive':
|
||||
errorMessage = "جاري التقاط صورة بالفعل. يرجى الانتظار";
|
||||
break;
|
||||
case 'pictureTakingInProgress':
|
||||
errorMessage = "جاري التقاط صورة. يرجى الانتظار";
|
||||
break;
|
||||
default:
|
||||
errorMessage = "فشل التقاط الصورة: ${e.description ?? 'خطأ غير معروف'}";
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = errorMessage;
|
||||
});
|
||||
}
|
||||
} on FileSystemException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "فشل حفظ الصورة. يرجى التحقق من مساحة التخزين";
|
||||
});
|
||||
}
|
||||
print("File system error: $e");
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى";
|
||||
});
|
||||
}
|
||||
print("Unexpected error in _startScan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraController?.dispose();
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xff000000),
|
||||
|
||||
body:
|
||||
// Show error screen only if camera failed to initialize
|
||||
_errorMessage != null && !_isCameraInitialized
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 64,
|
||||
color: Colors.white70,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _initializeCamera,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white70),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إلغاء"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: _isCameraInitialized && _cameraController != null
|
||||
? Stack(
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height),
|
||||
|
||||
// Camera Preview
|
||||
Positioned(
|
||||
child: Center(child: CameraPreview(_cameraController!)),
|
||||
),
|
||||
// Oval overlay with dimmed background
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _OvalOverlayPainter()),
|
||||
),
|
||||
|
||||
// Top Text
|
||||
Positioned(
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily:
|
||||
'Cairo', // Assuming Cairo font based on Arabic text
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Text and Logo
|
||||
Positioned(
|
||||
bottom: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isSuccess
|
||||
? (widget.isLogin
|
||||
? "تم تسجيل دخولك بنجاح"
|
||||
: "تم تسجيل خروجك بنجاح")
|
||||
: _isLoading
|
||||
? (widget.isLogin
|
||||
? "يتم تسجيل الدخول ..."
|
||||
: "يتم تسجيل الخروج ...")
|
||||
: (widget.isLogin
|
||||
? "جاهز للتقاط الصورة"
|
||||
: "جاهز للتقاط الصورة"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Logo
|
||||
SizedBox(
|
||||
height: _isSuccess ? 80 : 60,
|
||||
width: _isSuccess ? 80 : 60,
|
||||
child: SvgPicture.asset(
|
||||
_isSuccess
|
||||
? 'assets/images/logSuccess.svg'
|
||||
: 'assets/images/logLoading.svg',
|
||||
// ignore: deprecated_member_use
|
||||
color: Colors.white,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Error overlay (shown when error occurs during scanning)
|
||||
if (_errorMessage != null && _isCameraInitialized)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isLoading = false;
|
||||
});
|
||||
_startScan();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white70),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إلغاء"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// // Capture button
|
||||
// Positioned(
|
||||
// bottom: 60,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: Center(
|
||||
// child: GestureDetector(
|
||||
// onTap: (){},
|
||||
// child: Container(
|
||||
// width: 72,
|
||||
// height: 72,
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// color: Colors.white,
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black26,
|
||||
// blurRadius: 8,
|
||||
// offset: Offset(0, 4),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Icon(Icons.camera_alt, color: Color(0xffE8001A), size: 36),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: CircularProgressIndicator(color: Color(0xffE8001A)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OvalOverlayPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final width = size.width * 0.75;
|
||||
final height = size.height * 0.4;
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final ovalRect = Rect.fromCenter(
|
||||
center: center,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
|
||||
// Create a path for the whole screen
|
||||
final screenPath =
|
||||
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
// Create a path for the oval
|
||||
final ovalPath = Path()..addOval(ovalRect);
|
||||
// Subtract the oval from the screen path
|
||||
final overlayPath = Path.combine(
|
||||
PathOperation.difference,
|
||||
screenPath,
|
||||
ovalPath,
|
||||
);
|
||||
|
||||
// Draw the dimmed area outside the oval with gradient
|
||||
final gradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromARGB(255, 41, 41, 41), // top dark gray
|
||||
Color.fromARGB(255, 0, 20, 15), // bottom deep green
|
||||
],
|
||||
);
|
||||
final paint =
|
||||
Paint()
|
||||
..shader = gradient.createShader(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(overlayPath, paint);
|
||||
|
||||
// Draw glowing circles effect (like AppBackground) - drawn after overlay
|
||||
// Top circle - positioned similar to AppBackground (top: -250, left: 100, right: -200)
|
||||
final topCircleCenter = Offset(size.width * 0.3, -250);
|
||||
final topCircleRadius = 150.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 160)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint =
|
||||
Paint()
|
||||
..color = Color.fromARGB(69 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 140 - (i * 20));
|
||||
canvas.drawCircle(
|
||||
topCircleCenter,
|
||||
topCircleRadius + (i * 30),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom circle - positioned similar to AppBackground (bottom: 100, left: -140, right: -120)
|
||||
final bottomCircleCenter = Offset(size.width * 0.2, size.height + 100);
|
||||
final bottomCircleRadius = 160.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 60)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint =
|
||||
Paint()
|
||||
..color = Color.fromARGB(83 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 180 - (i * 25));
|
||||
canvas.drawCircle(
|
||||
bottomCircleCenter,
|
||||
bottomCircleRadius + (i * 40),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw oval border
|
||||
final borderPaint =
|
||||
Paint()
|
||||
..color = Colors.greenAccent
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawOval(ovalRect, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/finance_summary_card.dart';
|
||||
import '../widgets/work_day_card.dart';
|
||||
@@ -1,15 +1,23 @@
|
||||
import 'package:coda_project/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/screens/user_settings_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../screens/request_leave_screen.dart';
|
||||
import '../screens/request_advance_scrren.dart';
|
||||
import '../models/leave_request.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import 'request_leave_screen.dart';
|
||||
import 'request_advance_scrren.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../domain/usecases/get_vacations_usecase.dart';
|
||||
import '../../domain/usecases/get_advances_usecase.dart';
|
||||
import '../../domain/models/vacations_list_response_model.dart';
|
||||
import '../../domain/models/vacation_response_model.dart';
|
||||
import '../../domain/models/advances_list_response_model.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class HolidayScreen extends StatefulWidget {
|
||||
final void Function(bool isScrollingDown)? onScrollEvent;
|
||||
@@ -24,8 +32,12 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
int activeTab = 0;
|
||||
|
||||
final RequestService _requestService = RequestService();
|
||||
final GetVacationsUseCase _getVacationsUseCase = sl<GetVacationsUseCase>();
|
||||
final GetAdvancesUseCase _getAdvancesUseCase = sl<GetAdvancesUseCase>();
|
||||
List<LeaveRequest> _leaveRequests = [];
|
||||
List<AdvanceRequest> _advanceRequests = [];
|
||||
bool _isLoadingVacations = false;
|
||||
bool _isLoadingAdvances = false;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showActions = true;
|
||||
@@ -62,11 +74,18 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
}
|
||||
|
||||
void _initializeData() async {
|
||||
_leaveRequests = await _requestService.getLeaveRequests();
|
||||
_advanceRequests = await _requestService.getAdvanceRequests();
|
||||
// Load from API
|
||||
_loadVacationsFromAPI();
|
||||
_loadAdvancesFromAPI();
|
||||
|
||||
// Also listen to local changes (for newly created requests)
|
||||
_requestService.leaveRequestsStream.listen((requests) {
|
||||
if (mounted) setState(() => _leaveRequests = requests);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Merge with API data if needed
|
||||
_leaveRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_requestService.advanceRequestsStream.listen((requests) {
|
||||
@@ -74,6 +93,140 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadVacationsFromAPI() async {
|
||||
setState(() {
|
||||
_isLoadingVacations = true;
|
||||
});
|
||||
|
||||
final result = await _getVacationsUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingVacations = false;
|
||||
});
|
||||
// Load from local service as fallback
|
||||
_requestService.getLeaveRequests().then((requests) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_leaveRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
if (mounted && response.data != null) {
|
||||
setState(() {
|
||||
_leaveRequests = response.data!.items
|
||||
.map((vacation) => _convertVacationToLeaveRequest(vacation))
|
||||
.toList();
|
||||
_isLoadingVacations = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
LeaveRequest _convertVacationToLeaveRequest(VacationDataModel vacation) {
|
||||
// Convert state (0=waiting, 1=approved, 2=denied) to status string
|
||||
String status = "waiting";
|
||||
if (vacation.state == 1) {
|
||||
status = "approved";
|
||||
} else if (vacation.state == 2) {
|
||||
status = "denied";
|
||||
}
|
||||
|
||||
// Convert type to Arabic name
|
||||
String leaveTypeName = _getArabicVacationTypeName(vacation.type);
|
||||
|
||||
// Check if it's timed leave (same day but different times)
|
||||
bool isTimedLeave = vacation.startDate.year == vacation.endDate.year &&
|
||||
vacation.startDate.month == vacation.endDate.month &&
|
||||
vacation.startDate.day == vacation.endDate.day &&
|
||||
vacation.startDate.hour != vacation.endDate.hour;
|
||||
|
||||
return LeaveRequest(
|
||||
id: vacation.id,
|
||||
leaveType: leaveTypeName,
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: vacation.startDate,
|
||||
toDate: vacation.endDate,
|
||||
fromTime: TimeOfDay.fromDateTime(vacation.startDate),
|
||||
toTime: TimeOfDay.fromDateTime(vacation.endDate),
|
||||
reason: vacation.reason,
|
||||
requestDate: vacation.createdAt ?? vacation.startDate,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
String _getArabicVacationTypeName(int type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return 'أجازة زمنية';
|
||||
case 2:
|
||||
return 'إجازة مرضية';
|
||||
case 3:
|
||||
return 'إجازة مدفوعة';
|
||||
case 4:
|
||||
return 'إجازة غير مدفوعة';
|
||||
default:
|
||||
return 'إجازة';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAdvancesFromAPI() async {
|
||||
setState(() {
|
||||
_isLoadingAdvances = true;
|
||||
});
|
||||
|
||||
final result = await _getAdvancesUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingAdvances = false;
|
||||
});
|
||||
// Load from local service as fallback
|
||||
_requestService.getAdvanceRequests().then((requests) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_advanceRequests = requests;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
if (mounted && response.data != null) {
|
||||
setState(() {
|
||||
_advanceRequests = response.data!.items
|
||||
.map((advance) => _convertAdvanceToAdvanceRequest(advance))
|
||||
.toList();
|
||||
_isLoadingAdvances = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
AdvanceRequest _convertAdvanceToAdvanceRequest(AdvanceDataModel advance) {
|
||||
// Convert state (0=waiting, 1=approved, 2=denied) to status string
|
||||
String status = "waiting";
|
||||
if (advance.state == 1) {
|
||||
status = "approved";
|
||||
} else if (advance.state == 2) {
|
||||
status = "denied";
|
||||
}
|
||||
|
||||
return AdvanceRequest(
|
||||
id: advance.id,
|
||||
amount: advance.amount,
|
||||
reason: advance.reason,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
@@ -259,6 +412,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
Widget _buildLeaveRequestsSliver() {
|
||||
if (_isLoadingVacations) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF8EFDC2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_leaveRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -287,6 +453,19 @@ class _HolidayScreenState extends State<HolidayScreen> {
|
||||
}
|
||||
|
||||
Widget _buildAdvanceRequestsSliver() {
|
||||
if (_isLoadingAdvances) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF8EFDC2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_advanceRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/floatingnavbar.dart';
|
||||
import '../screens/attendence_screen.dart';
|
||||
import '../screens/finance_screen.dart';
|
||||
import '../screens/holiday_screen.dart';
|
||||
import '../widgets/FloatingNavBar.dart';
|
||||
import 'attendence_screen.dart';
|
||||
import 'finance_screen.dart';
|
||||
import 'holiday_screen.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({super.key});
|
||||
@@ -15,15 +15,12 @@ class MainPage extends StatefulWidget {
|
||||
class _MainPageState extends State<MainPage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
// final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
|
||||
/// BACKGROUND
|
||||
const AppBackground(child: SizedBox()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:coda_project/screens/auth_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/auth_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/onboarding_page.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../models/advance_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../domain/usecases/create_advance_usecase.dart';
|
||||
import '../../domain/models/advance_request_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class RequestAdvanceScreen extends StatefulWidget {
|
||||
const RequestAdvanceScreen({super.key});
|
||||
@@ -23,16 +28,30 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
// Use case
|
||||
final CreateAdvanceUseCase _createAdvanceUseCase = sl<CreateAdvanceUseCase>();
|
||||
|
||||
String _getFailureMessage(Failure failure) {
|
||||
if (failure is ServerFailure) {
|
||||
return failure.message;
|
||||
} else if (failure is NetworkFailure) {
|
||||
return failure.message;
|
||||
}
|
||||
return 'حدث خطأ غير متوقع';
|
||||
}
|
||||
|
||||
// Method to save the advance request
|
||||
Future<void> _saveAdvanceRequest() async {
|
||||
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
|
||||
// Show an error message if fields are empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال جميع الحقول'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال جميع الحقول'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,45 +59,96 @@ class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
||||
final amount = double.tryParse(amountController.text);
|
||||
if (amount == null || amount <= 0) {
|
||||
// Show an error message if amount is invalid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new advance request with default status "waiting"
|
||||
final advanceRequest = AdvanceRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
// Get employee ID
|
||||
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the advance request
|
||||
await _requestService.addAdvanceRequest(advanceRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
// Create advance request model
|
||||
final advanceRequestModel = AdvanceRequestModel(
|
||||
employeeId: employeeId,
|
||||
date: DateTime.now(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
final result = await _createAdvanceUseCase(advanceRequestModel);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_getFailureMessage(failure)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) {
|
||||
// Also save locally for UI display
|
||||
final advanceRequest = AdvanceRequest(
|
||||
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
_requestService.addAdvanceRequest(advanceRequest);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ غير متوقع: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../models/leave_request.dart';
|
||||
import '../services/request_service.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
import '../../domain/usecases/create_vacation_usecase.dart';
|
||||
import '../../domain/usecases/get_vacation_types_usecase.dart';
|
||||
import '../../domain/models/vacation_request.dart';
|
||||
import '../../domain/models/vacation_type_model.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
|
||||
class RequestLeaveScreen extends StatefulWidget {
|
||||
const RequestLeaveScreen({super.key});
|
||||
@@ -33,6 +40,14 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
// Use cases
|
||||
final CreateVacationUseCase _createVacationUseCase = sl<CreateVacationUseCase>();
|
||||
final GetVacationTypesUseCase _getVacationTypesUseCase = sl<GetVacationTypesUseCase>();
|
||||
|
||||
// Vacation types from API
|
||||
List<VacationTypeModel> _vacationTypes = [];
|
||||
int? _selectedVacationTypeValue; // Store selected type value instead of string
|
||||
|
||||
/// PICK DATE
|
||||
Future<void> pickDate(bool isFrom) async {
|
||||
DateTime initial = isFrom ? fromDate! : toDate!;
|
||||
@@ -79,52 +94,211 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadVacationTypes();
|
||||
}
|
||||
|
||||
Future<void> _loadVacationTypes() async {
|
||||
final result = await _getVacationTypesUseCase();
|
||||
result.fold(
|
||||
(failure) {
|
||||
// Silently fail - user can still submit with default types
|
||||
print('Failed to load vacation types: ${_getFailureMessage(failure)}');
|
||||
},
|
||||
(response) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_vacationTypes = response.data;
|
||||
// Set default to SickLeave (value: 2) if available
|
||||
if (_vacationTypes.isNotEmpty) {
|
||||
final sickLeave = _vacationTypes.firstWhere(
|
||||
(type) => type.value == 2,
|
||||
orElse: () => _vacationTypes.first,
|
||||
);
|
||||
_selectedVacationTypeValue = sickLeave.value;
|
||||
leaveType = _getArabicName(sickLeave.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getArabicName(String apiName) {
|
||||
// Map API names to Arabic display names
|
||||
switch (apiName) {
|
||||
case 'TimeOff':
|
||||
return 'أجازة زمنية';
|
||||
case 'SickLeave':
|
||||
return 'إجازة مرضية';
|
||||
case 'PaidLeave':
|
||||
return 'إجازة مدفوعة';
|
||||
case 'UnpaidLeave':
|
||||
return 'إجازة غير مدفوعة';
|
||||
default:
|
||||
return apiName;
|
||||
}
|
||||
}
|
||||
|
||||
int _getVacationTypeValue() {
|
||||
// Use selected value if available, otherwise fallback to mapping
|
||||
if (_selectedVacationTypeValue != null) {
|
||||
return _selectedVacationTypeValue!;
|
||||
}
|
||||
// Fallback: Map display names to API type values
|
||||
if (leaveType.contains("مرضية") || leaveType == "SickLeave") {
|
||||
return 2; // SickLeave
|
||||
} else if (leaveType.contains("مدفوعة") && !leaveType.contains("غير")) {
|
||||
return 3; // PaidLeave
|
||||
} else if (leaveType.contains("غير مدفوعة") || leaveType == "UnpaidLeave") {
|
||||
return 4; // UnpaidLeave
|
||||
} else if (leaveType.contains("زمنية") || leaveType == "TimeOff") {
|
||||
return 1; // TimeOff
|
||||
}
|
||||
return 1; // Default to TimeOff
|
||||
}
|
||||
|
||||
String _getFailureMessage(Failure failure) {
|
||||
if (failure is ServerFailure) {
|
||||
return failure.message;
|
||||
} else if (failure is NetworkFailure) {
|
||||
return failure.message;
|
||||
}
|
||||
return 'حدث خطأ غير متوقع';
|
||||
}
|
||||
|
||||
// Method to save the leave request
|
||||
Future<void> _saveLeaveRequest() async {
|
||||
if (reasonController.text.isEmpty) {
|
||||
// Show an error message if reason is empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال السبب'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال السبب'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new leave request with default status "waiting"
|
||||
final leaveRequest = LeaveRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
leaveType: leaveType, // Use the current leaveType value
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: fromDate!,
|
||||
toDate: toDate!,
|
||||
fromTime: fromTime!,
|
||||
toTime: toTime!,
|
||||
reason: reasonController.text,
|
||||
requestDate: DateTime.now(),
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
// Get employee ID
|
||||
final employeeId = await sl<UserLocalDataSource>().getCachedEmployeeId();
|
||||
if (employeeId == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('خطأ: لم يتم العثور على رقم الموظف'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare dates - if timed leave, use same date with time differences
|
||||
DateTime finalStartDate = fromDate!;
|
||||
DateTime finalEndDate = toDate!;
|
||||
|
||||
if (isTimedLeave) {
|
||||
// For timed leave, use the same date but with different times
|
||||
finalStartDate = DateTime(
|
||||
fromDate!.year,
|
||||
fromDate!.month,
|
||||
fromDate!.day,
|
||||
fromTime!.hour,
|
||||
fromTime!.minute,
|
||||
);
|
||||
finalEndDate = DateTime(
|
||||
fromDate!.year,
|
||||
fromDate!.month,
|
||||
fromDate!.day,
|
||||
toTime!.hour,
|
||||
toTime!.minute,
|
||||
);
|
||||
} else {
|
||||
// For regular leave, use dates at midnight
|
||||
finalStartDate = DateTime(fromDate!.year, fromDate!.month, fromDate!.day);
|
||||
finalEndDate = DateTime(toDate!.year, toDate!.month, toDate!.day);
|
||||
}
|
||||
|
||||
// Get vacation type value
|
||||
final typeValue = _getVacationTypeValue();
|
||||
|
||||
// Show loading indicator
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Save the leave request
|
||||
await _requestService.addLeaveRequest(leaveRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
// Create vacation request
|
||||
final vacationRequest = VacationRequest(
|
||||
employeeId: employeeId,
|
||||
startDate: finalStartDate,
|
||||
endDate: finalEndDate,
|
||||
reason: reasonController.text,
|
||||
type: typeValue,
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
final result = await _createVacationUseCase(vacationRequest);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_getFailureMessage(failure)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) {
|
||||
// Also save locally for UI display
|
||||
final leaveRequest = LeaveRequest(
|
||||
id: response.data?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
leaveType: leaveType,
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: fromDate!,
|
||||
toDate: toDate!,
|
||||
fromTime: fromTime!,
|
||||
toTime: toTime!,
|
||||
reason: reasonController.text,
|
||||
requestDate: DateTime.now(),
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
_requestService.addLeaveRequest(leaveRequest);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('حدث خطأ: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // Close loading dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ غير متوقع: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,68 +390,51 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
],
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: leaveType,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.black,
|
||||
size: 28,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 17,
|
||||
),
|
||||
isExpanded: true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
leaveType = value!;
|
||||
// Set toggle based on selected value
|
||||
isTimedLeave = value == "أجازة زمنية";
|
||||
});
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مرضية ",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مرضية "),
|
||||
child: _vacationTypes.isEmpty
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مدفوعة"),
|
||||
)
|
||||
: DropdownButton<int>(
|
||||
value: _selectedVacationTypeValue,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.black,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة غير مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة غير مدفوعة"),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 17,
|
||||
),
|
||||
isExpanded: true,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedVacationTypeValue = value;
|
||||
final selectedType = _vacationTypes
|
||||
.firstWhere((t) => t.value == value);
|
||||
leaveType = _getArabicName(selectedType.name);
|
||||
// Set toggle based on selected value (TimeOff = 1)
|
||||
isTimedLeave = value == 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
items: _vacationTypes.map((type) {
|
||||
final arabicName = _getArabicName(type.name);
|
||||
return DropdownMenuItem<int>(
|
||||
value: type.value,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(arabicName),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "أجازة زمنية",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("أجازة زمنية"),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -293,9 +450,12 @@ class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isTimedLeave = !isTimedLeave;
|
||||
// Set leave type to "أجازة زمنية" when toggle is ON
|
||||
// Set leave type to TimeOff (value: 1) when toggle is ON
|
||||
if (isTimedLeave) {
|
||||
leaveType = "أجازة زمنية";
|
||||
final timeOffType = _vacationTypes
|
||||
.firstWhere((t) => t.value == 1, orElse: () => _vacationTypes.first);
|
||||
_selectedVacationTypeValue = timeOffType.value;
|
||||
leaveType = _getArabicName(timeOffType.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'onboarding_screen.dart';
|
||||
import 'main_screen.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@@ -14,13 +17,31 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
FlutterNativeSplash.remove();
|
||||
_checkTokenAndNavigate();
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
Future<void> _checkTokenAndNavigate() async {
|
||||
// Wait for splash screen display
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Check if token exists in cache
|
||||
final token = await sl<UserLocalDataSource>().getCachedUserToken();
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
// Token exists, navigate directly to MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OnboardingScreen()),
|
||||
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// No token, navigate to OnboardingScreen
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const OnboardingScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../screens/about_screen.dart';
|
||||
import '../screens/auth_screen.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'auth_screen.dart';
|
||||
import '../widgets/change_password_modal.dart';
|
||||
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../../data/datasources/user_local_data_source.dart';
|
||||
|
||||
class UserSettingsScreen extends StatefulWidget {
|
||||
const UserSettingsScreen({super.key});
|
||||
@@ -143,8 +144,8 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
width: 75,
|
||||
height: 30,
|
||||
width: 75,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
@@ -227,13 +228,18 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
_settingsRow(
|
||||
label: "تسجيل خروج",
|
||||
icon: "assets/images/logout2.svg",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AuthScreen(),
|
||||
),
|
||||
);
|
||||
onTap: () async {
|
||||
await sl<UserLocalDataSource>()
|
||||
.clearCache();
|
||||
if (context.mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AuthScreen(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
318
lib/presentation/widgets/auth_form.dart
Normal file
318
lib/presentation/widgets/auth_form.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../../domain/models/login_request.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
import '../blocs/login/login_event.dart';
|
||||
import '../blocs/login/login_state.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
final VoidCallback? onSubmit;
|
||||
|
||||
const AuthForm({super.key, this.onSubmit});
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
}
|
||||
|
||||
class _AuthFormState extends State<AuthForm> {
|
||||
bool _obscure = true;
|
||||
|
||||
// Text controllers
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
// Focus nodes for text fields
|
||||
late FocusNode _phoneNumberFocusNode;
|
||||
late FocusNode _passwordFocusNode;
|
||||
|
||||
void _handleLogin() {
|
||||
// Validate inputs
|
||||
if (_phoneNumberController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال رقم الهاتف');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passwordController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus any focused text field
|
||||
_phoneNumberFocusNode.unfocus();
|
||||
_passwordFocusNode.unfocus();
|
||||
|
||||
// Dispatch login event
|
||||
final request = LoginRequest(
|
||||
phoneNumber: _phoneNumberController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
context.read<LoginBloc>().add(LoginSubmitted(request));
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize focus nodes
|
||||
_phoneNumberFocusNode = FocusNode();
|
||||
_passwordFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up controllers and focus nodes
|
||||
_phoneNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneNumberFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get screen dimensions
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
// Calculate responsive dimensions
|
||||
final formWidth = screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.9;
|
||||
final formHeight =
|
||||
screenHeight > 800 ? screenHeight * 0.6 : screenHeight * 0.8;
|
||||
final borderWidth = formWidth + 20;
|
||||
final titleFontSize = screenWidth > 600 ? 28.0 : 24.0;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalSpacing = screenHeight > 800 ? 34.0 : 24.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 100.0 : 60.0;
|
||||
final bottomSpacing = screenHeight > 800 ? 40.0 : 20.0;
|
||||
final horizontalPadding = screenWidth > 600 ? 30.0 : 25.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
|
||||
return BlocListener<LoginBloc, LoginState>(
|
||||
listener: (context, state) {
|
||||
if (state is LoginSuccess) {
|
||||
// Call the onSubmit callback if provided
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!();
|
||||
}
|
||||
|
||||
// Navigate to the MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
_showError(state.message);
|
||||
}
|
||||
},
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FocusScope(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Border container - decorative element behind the form
|
||||
Container(
|
||||
width: borderWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: formHeight + 40,
|
||||
maxHeight:
|
||||
formHeight + 80, // Allows shrinking when keyboard opens
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xDD00C28E), width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
// Main form container
|
||||
Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Title
|
||||
Center(
|
||||
child: Text(
|
||||
"تسجيل دخول",
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
/// Phone Number Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"رقم الهاتف",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _phoneNumberController,
|
||||
hint: "رقم الهاتف",
|
||||
obscure: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
focusNode: _phoneNumberFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) {
|
||||
// Move focus to password field when next is pressed
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// Password Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _passwordController,
|
||||
hint: "كلمة المرور",
|
||||
obscure: _obscure,
|
||||
hasEye: true,
|
||||
focusNode: _passwordFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing), // Responsive spacing
|
||||
|
||||
BlocBuilder<LoginBloc, LoginState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is LoginLoading;
|
||||
return Center(
|
||||
child: OnboardingButton(
|
||||
text:
|
||||
isLoading
|
||||
? "جاري تسجيل الدخول..."
|
||||
: "تسجيل دخول",
|
||||
backgroundColor: const Color.fromARGB(
|
||||
239,
|
||||
35,
|
||||
87,
|
||||
74,
|
||||
),
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: bottomSpacing),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
TextEditingController? controller,
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
bool hasEye = false,
|
||||
TextInputType? keyboardType,
|
||||
FocusNode? focusNode,
|
||||
TextInputAction? textInputAction,
|
||||
Function(String)? onSubmitted,
|
||||
required double fontSize,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textAlign: TextAlign.right,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !obscure);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class ChangePasswordModal extends StatefulWidget {
|
||||
const ChangePasswordModal({super.key});
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import 'app_background.dart';
|
||||
|
||||
class LoginAnimationScreen extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import '../widgets/gradient_line.dart';
|
||||
import '../widgets/status_circle.dart';
|
||||
import 'gradient_line.dart';
|
||||
import 'status_circle.dart';
|
||||
|
||||
class WorkDayCard extends StatelessWidget {
|
||||
const WorkDayCard({super.key});
|
||||
@@ -1,75 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
/// -------------------- SETTINGS BAR --------------------
|
||||
SettingsBar(
|
||||
selectedIndex: 0,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
/// -------------------- CENTER CONTENT --------------------
|
||||
///
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"عن الشركة",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Container(
|
||||
width: 200,
|
||||
height: 1,
|
||||
color: Color(0x8732C599),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
const Text(
|
||||
"نص",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/auth_form.dart';
|
||||
|
||||
class AuthScreen extends StatelessWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo
|
||||
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
|
||||
// const SizedBox(height: 15),
|
||||
// Form - taking remaining space and centered
|
||||
Expanded(child: Center(child: const AuthForm())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OvalCameraCapturePage extends StatefulWidget {
|
||||
const OvalCameraCapturePage({super.key});
|
||||
|
||||
@override
|
||||
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
||||
}
|
||||
|
||||
class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
CameraController? _cameraController;
|
||||
bool _isCameraInitialized = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
// Dispose existing controller if any
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
|
||||
// Get available cameras
|
||||
final cameras = await availableCameras();
|
||||
|
||||
// Check if cameras list is available
|
||||
if (cameras.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find front camera, fallback to first available camera
|
||||
CameraDescription? selectedCamera;
|
||||
try {
|
||||
selectedCamera = cameras.firstWhere(
|
||||
(cam) => cam.lensDirection == CameraLensDirection.front,
|
||||
);
|
||||
} catch (e) {
|
||||
// If no front camera found, use the first available camera
|
||||
if (cameras.isNotEmpty) {
|
||||
selectedCamera = cameras.first;
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_cameraController = CameraController(
|
||||
selectedCamera,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _cameraController!.initialize();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isCameraInitialized = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
print("Error initializing camera: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xff000000),
|
||||
|
||||
body: _errorMessage != null
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 64,
|
||||
color: Colors.white70,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _initializeCamera,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: _isCameraInitialized && _cameraController != null
|
||||
? Stack(
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height ),
|
||||
// Camera Preview
|
||||
Positioned(
|
||||
child: Center(child: CameraPreview(_cameraController!)),
|
||||
),
|
||||
// Oval overlay with dimmed background
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _OvalOverlayPainter(),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// // Capture button
|
||||
// Positioned(
|
||||
// bottom: 60,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: Center(
|
||||
// child: GestureDetector(
|
||||
// onTap: (){},
|
||||
// child: Container(
|
||||
// width: 72,
|
||||
// height: 72,
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// color: Colors.white,
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black26,
|
||||
// blurRadius: 8,
|
||||
// offset: Offset(0, 4),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Icon(Icons.camera_alt, color: Color(0xffE8001A), size: 36),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
)
|
||||
: Center(child: CircularProgressIndicator(color: Color(0xffE8001A))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OvalOverlayPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final width = size.width * 0.75;
|
||||
final height = size.height * 0.4;
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final ovalRect = Rect.fromCenter(
|
||||
center: center,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
|
||||
// Create a path for the whole screen
|
||||
final screenPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
// Create a path for the oval
|
||||
final ovalPath = Path()..addOval(ovalRect);
|
||||
// Subtract the oval from the screen path
|
||||
final overlayPath = Path.combine(PathOperation.difference, screenPath, ovalPath);
|
||||
|
||||
// Draw the dimmed area outside the oval with gradient
|
||||
final gradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromARGB(255, 41, 41, 41), // top dark gray
|
||||
Color.fromARGB(255, 0, 20, 15), // bottom deep green
|
||||
],
|
||||
);
|
||||
final paint = Paint()
|
||||
..shader = gradient.createShader(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(overlayPath, paint);
|
||||
|
||||
// Draw glowing circles effect (like AppBackground) - drawn after overlay
|
||||
// Top circle - positioned similar to AppBackground (top: -250, left: 100, right: -200)
|
||||
final topCircleCenter = Offset(size.width * 0.3, -250);
|
||||
final topCircleRadius = 150.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 160)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint = Paint()
|
||||
..color = Color.fromARGB(69 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 140 - (i * 20));
|
||||
canvas.drawCircle(
|
||||
topCircleCenter,
|
||||
topCircleRadius + (i * 30),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom circle - positioned similar to AppBackground (bottom: 100, left: -140, right: -120)
|
||||
final bottomCircleCenter = Offset(size.width * 0.2, size.height + 100);
|
||||
final bottomCircleRadius = 160.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 60)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint = Paint()
|
||||
..color = Color.fromARGB(83 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 180 - (i * 25));
|
||||
canvas.drawCircle(
|
||||
bottomCircleCenter,
|
||||
bottomCircleRadius + (i * 40),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw oval border
|
||||
final borderPaint = Paint()
|
||||
..color = Colors.greenAccent
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawOval(ovalRect, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../core/di/injection_container.dart';
|
||||
import '../domain/usecases/login_usecase.dart';
|
||||
import '../domain/models/login_request.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
final VoidCallback? onSubmit;
|
||||
|
||||
const AuthForm({super.key, this.onSubmit});
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
}
|
||||
|
||||
class _AuthFormState extends State<AuthForm> {
|
||||
bool _obscure = true;
|
||||
bool _isLoading = false;
|
||||
|
||||
// Text controllers
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
// Focus nodes for text fields
|
||||
late FocusNode _phoneNumberFocusNode;
|
||||
late FocusNode _passwordFocusNode;
|
||||
|
||||
// Get LoginUseCase from dependency injection
|
||||
final LoginUseCase _loginUseCase = sl<LoginUseCase>();
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
// Validate inputs
|
||||
if (_phoneNumberController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال رقم الهاتف');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passwordController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus any focused text field
|
||||
_phoneNumberFocusNode.unfocus();
|
||||
_passwordFocusNode.unfocus();
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final request = LoginRequest(
|
||||
phoneNumber: _phoneNumberController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
final result = await _loginUseCase(request);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_showError(failure.message);
|
||||
},
|
||||
(response) {
|
||||
if (response.isSuccess) {
|
||||
// Call the onSubmit callback if provided
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!();
|
||||
}
|
||||
|
||||
// Navigate to the MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||
);
|
||||
} else {
|
||||
_showError(response.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_showError('حدث خطأ غير متوقع: $e');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize focus nodes
|
||||
_phoneNumberFocusNode = FocusNode();
|
||||
_passwordFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up controllers and focus nodes
|
||||
_phoneNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneNumberFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get screen dimensions
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
// Calculate responsive dimensions
|
||||
final formWidth = screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.9;
|
||||
final formHeight =
|
||||
screenHeight > 800 ? screenHeight * 0.6 : screenHeight * 0.8;
|
||||
final borderWidth = formWidth + 20;
|
||||
final titleFontSize = screenWidth > 600 ? 28.0 : 24.0;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalSpacing = screenHeight > 800 ? 34.0 : 24.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 100.0 : 60.0;
|
||||
final bottomSpacing = screenHeight > 800 ? 40.0 : 20.0;
|
||||
final horizontalPadding = screenWidth > 600 ? 30.0 : 25.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
|
||||
return Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FocusScope(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Border container - decorative element behind the form
|
||||
Container(
|
||||
width: borderWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: formHeight + 40,
|
||||
maxHeight:
|
||||
formHeight + 80, // Allows shrinking when keyboard opens
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xDD00C28E), width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
// Main form container
|
||||
Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Title
|
||||
Center(
|
||||
child: Text(
|
||||
"تسجيل دخول",
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
/// Phone Number Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"رقم الهاتف",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _phoneNumberController,
|
||||
hint: "رقم الهاتف",
|
||||
obscure: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
focusNode: _phoneNumberFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) {
|
||||
// Move focus to password field when next is pressed
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// Password Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _passwordController,
|
||||
hint: "كلمة المرور",
|
||||
obscure: _obscure,
|
||||
hasEye: true,
|
||||
focusNode: _passwordFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing), // Responsive spacing
|
||||
|
||||
Center(
|
||||
child: OnboardingButton(
|
||||
text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
|
||||
backgroundColor: const Color.fromARGB(239, 35, 87, 74),
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: bottomSpacing),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
TextEditingController? controller,
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
bool hasEye = false,
|
||||
TextInputType? keyboardType,
|
||||
FocusNode? focusNode,
|
||||
TextInputAction? textInputAction,
|
||||
Function(String)? onSubmitted,
|
||||
required double fontSize,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textAlign: TextAlign.right,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !obscure);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
pubspec.lock
44
pubspec.lock
@@ -33,6 +33,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -77,10 +85,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+3"
|
||||
version: "0.3.5"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -125,10 +133,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+1"
|
||||
version: "0.3.5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -206,6 +214,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -368,6 +384,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -646,5 +678,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ dependencies:
|
||||
dartz: ^0.10.1
|
||||
equatable: ^2.0.5
|
||||
shared_preferences: ^2.2.2
|
||||
flutter_bloc: ^8.1.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user