diff --git a/package-lock.json b/package-lock.json
index 6e4ca5a..f9fa5d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"react-native-calendars": "^1.1313.0",
"react-native-dialog": "^9.3.0",
"react-native-elements": "^3.4.3",
+ "react-native-fast-confetti": "^1.1.1",
"react-native-image-picker": "^8.2.1",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
@@ -154,7 +155,6 @@
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
"integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.3"
@@ -183,7 +183,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
"integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
@@ -205,7 +204,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz",
"integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1",
@@ -249,7 +247,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
"integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -293,7 +290,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
"integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.1"
@@ -333,7 +329,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
"integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-member-expression-to-functions": "^7.27.1",
@@ -351,7 +346,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
"integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -699,7 +693,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
"integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -817,7 +810,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -850,7 +842,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
"integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -934,7 +925,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
"integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.27.1",
@@ -968,7 +958,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
@@ -1271,7 +1260,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
"integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
@@ -1357,7 +1345,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
"integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1442,7 +1429,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
@@ -1664,7 +1650,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
"integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1713,7 +1698,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
"integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1745,7 +1729,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
"integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
@@ -1798,7 +1781,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
"integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
@@ -1928,6 +1910,26 @@
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
+ "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -3654,6 +3656,33 @@
}
}
},
+ "node_modules/@shopify/react-native-skia": {
+ "version": "2.2.19",
+ "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.2.19.tgz",
+ "integrity": "sha512-K/mui7BEPfOkC4EGLR3tGn7dUGnQQQUBlTPShelTvhCbhvBey1VMiXLJrf3uEkm0nox3PRLgPC8/WyW6kSlxUQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "canvaskit-wasm": "0.40.0",
+ "react-reconciler": "0.31.0"
+ },
+ "bin": {
+ "setup-skia-web": "scripts/setup-canvaskit.js"
+ },
+ "peerDependencies": {
+ "react": ">=19.0",
+ "react-native": ">=0.78",
+ "react-native-reanimated": ">=3.19.1"
+ },
+ "peerDependenciesMeta": {
+ "react-native": {
+ "optional": true
+ },
+ "react-native-reanimated": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -4127,6 +4156,13 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.21",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz",
+ "integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
@@ -5044,6 +5080,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvaskit-wasm": {
+ "version": "0.40.0",
+ "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz",
+ "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "@webgpu/types": "0.1.21"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -13439,6 +13485,21 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
+ "node_modules/react-native-fast-confetti": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/react-native-fast-confetti/-/react-native-fast-confetti-1.1.1.tgz",
+ "integrity": "sha512-5kJn+dgvEoeM9h4c4SqxeqMtYAfN8QhaB1JS/5yv+AGYjVnE1a4c0AAv4yFyetwvmWQ3b16C10a8eW0FlaFAyg==",
+ "license": "MIT",
+ "workspaces": [
+ "example"
+ ],
+ "peerDependencies": {
+ "@shopify/react-native-skia": "*",
+ "react": "*",
+ "react-native": "*",
+ "react-native-reanimated": "*"
+ }
+ },
"node_modules/react-native-gesture-handler": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
@@ -13556,6 +13617,36 @@
"react-native": "*"
}
},
+ "node_modules/react-native-reanimated": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.2.tgz",
+ "integrity": "sha512-qzmQiFrvjm62pRBcj97QI9Xckc3EjgHQoY1F2yjktd0kpjhoyePeuTEXjYRCAVIy7IV/1cfeSup34+zFThFoHQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "react-native-is-edge-to-edge": "^1.2.1",
+ "semver": "7.7.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0",
+ "react": "*",
+ "react-native": "*",
+ "react-native-worklets": ">=0.5.0"
+ }
+ },
+ "node_modules/react-native-reanimated/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
@@ -13683,6 +13774,44 @@
"@types/react": ">=16.8.24"
}
},
+ "node_modules/react-native-worklets": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.6.0.tgz",
+ "integrity": "sha512-yETMNuCcivdYWteuG4eRqgiAk2DzRCrVAaEBIEWPo4emrf3BNjadFo85L5QvyEusrX9QKE3ZEAx8U5A/nbyFgg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/plugin-transform-arrow-functions": "^7.0.0-0",
+ "@babel/plugin-transform-class-properties": "^7.0.0-0",
+ "@babel/plugin-transform-classes": "^7.0.0-0",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
+ "@babel/plugin-transform-optional-chaining": "^7.0.0-0",
+ "@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
+ "@babel/plugin-transform-template-literals": "^7.0.0-0",
+ "@babel/plugin-transform-unicode-regex": "^7.0.0-0",
+ "@babel/preset-typescript": "^7.16.7",
+ "convert-source-map": "^2.0.0",
+ "semver": "7.7.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-worklets/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -13704,6 +13833,29 @@
"node": ">=10"
}
},
+ "node_modules/react-reconciler": {
+ "version": "0.31.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz",
+ "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/react-reconciler/node_modules/scheduler": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -13822,14 +13974,12 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
"integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -13869,7 +14019,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz",
"integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
@@ -13887,14 +14036,12 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
"integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
- "devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.0.2"
@@ -13907,7 +14054,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
- "devOptional": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -15185,7 +15331,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
"integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -15195,7 +15340,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
"integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^2.0.0",
@@ -15209,7 +15353,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
"integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -15219,7 +15362,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
"integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
diff --git a/package.json b/package.json
index 8886c8e..8774c41 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"react-native-calendars": "^1.1313.0",
"react-native-dialog": "^9.3.0",
"react-native-elements": "^3.4.3",
+ "react-native-fast-confetti": "^1.1.1",
"react-native-image-picker": "^8.2.1",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index 56cd810..758e448 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -19,6 +19,7 @@ import ManageJoinedGroup from "../screens/management/ManageJoinedGroup";
import InviteScreen from "../screens/InviteScreen";
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
import TransactionsList from "../screens/transaction/TransactionList";
+import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
export type RootStackParamList = {
Splash: undefined;
@@ -91,6 +92,7 @@ const linking = {
+
);
diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx
index 617fc9d..c51885a 100644
--- a/src/screens/management/EditGroupScreen.tsx
+++ b/src/screens/management/EditGroupScreen.tsx
@@ -21,6 +21,7 @@ import { CompositeScreenProps } from '@react-navigation/native';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
+
// --- Type Definitions ---
interface Participant {
id: string;
@@ -30,6 +31,7 @@ interface Participant {
role: 'ORGANIZER' | 'USER';
}
+
export interface GroupDetails {
id: string;
groupName: string;
@@ -53,6 +55,7 @@ export interface GroupDetails {
participants: Participant[];
}
+
export interface EditGroupPayload {
groupName: string;
description: string;
@@ -66,18 +69,22 @@ export interface EditGroupPayload {
lateFeeValue: number | null;
}
+
// Correct props for a screen inside a nested Tab Navigator
type EditGroupScreenProps = CompositeScreenProps<
BottomTabScreenProps,
NativeStackScreenProps
>;
+
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
+
// --- Main Screen Component ---
const EditGroupScreen: React.FC = ({ route }) => {
const { groupId } = route.params;
+
const [editingCard, setEditingCard] = useState(null);
const [groupDetails, setGroupDetails] = useState(null);
const [editableData, setEditableData] = useState({
@@ -99,7 +106,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
const [inviting, setInviting] = useState(false);
const [error, setError] = useState(null);
- // ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
+
const initializeEditableData = (data: GroupDetails) => {
setEditableData({
groupName: data.groupName,
@@ -115,6 +122,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
});
};
+
const fetchGroupDetails = useCallback(async () => {
setLoading(true);
setError(null);
@@ -134,10 +142,12 @@ const EditGroupScreen: React.FC = ({ route }) => {
}
}, [groupId]);
+
useEffect(() => {
fetchGroupDetails();
}, [fetchGroupDetails]);
+
const handleCancel = () => {
if (groupDetails) {
initializeEditableData(groupDetails);
@@ -145,9 +155,11 @@ const EditGroupScreen: React.FC = ({ route }) => {
setEditingCard(null);
};
+
const handleSave = async () => {
if (!groupDetails) return;
+
if (editableData.maxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return;
@@ -166,9 +178,11 @@ const EditGroupScreen: React.FC = ({ route }) => {
}
};
+
const handleStartGroup = async () => {
if (!groupDetails) return;
+
Alert.alert(
"Confirm Start",
"Are you sure you want to start this group? This action cannot be undone.",
@@ -200,6 +214,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
+
const inviteLink = response.data;
if (inviteLink) {
@@ -220,12 +235,13 @@ const EditGroupScreen: React.FC = ({ route }) => {
}
};
+
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
setEditableData(prev => ({ ...prev, [field]: value }));
};
- // ... (renderField and renderCard functions also remain the same)
- const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => {
+
+ const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable: boolean) => {
const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => {
@@ -276,6 +292,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
const isEditingThisCard = editingCard === cardKey;
+
return (
@@ -288,7 +305,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
{fields.map(({ field, label, displayValue }) =>
- renderField(field, label, displayValue(), isEditingThisCard)
+ renderField(field, label, displayValue(), isEditingThisCard, isEditable)
)}
{isEditingThisCard && (
@@ -305,16 +322,18 @@ const EditGroupScreen: React.FC = ({ route }) => {
);
}
+
if (loading) return ;
if (error) return {error};
if (!groupDetails) return null;
+
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
+
return (
- // Use AppView as the root container with flex: 1
Manage Group
@@ -325,6 +344,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
)}
+
= ({ route }) => {
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description },
], 'groupInfo')}
- {renderCard('Financial Rules', [
- { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
- { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
- { field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
- { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
- ], 'financialRules')}
+
+ {groupDetails.status === 'ACTIVE' ?
+ renderCard('Financial Rules', [
+ { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
+ { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
+ { field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
+ ], 'financialRules')
+ :
+ renderCard('Financial Rules', [
+ { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
+ { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
+ { field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
+ { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
+ ], 'financialRules')
+ }
+
{renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
- { field: 'customCycleDays', label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays ?? ''} days` },
+ ...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []),
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')}
- {/* Participants Card ... */}
Participants ({groupDetails.totalParticipants})
{groupDetails.participants.length > 0 ? (
@@ -369,8 +398,7 @@ const EditGroupScreen: React.FC = ({ route }) => {
- {/* --- FLOATING BUTTON AREA --- */}
- {/* This View is a sibling of ScrollView, allowing it to float on top */}
+
{canStartGroup && (
= ({ route }) => {
);
};
-// --- Styles ---
+
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
- // Added more paddingBottom to ensure content doesn't hide behind the button AND the tab bar
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
@@ -437,15 +464,13 @@ const styles = StyleSheet.create({
cancelButton: { backgroundColor: '#E5E7EB' },
inviteButton: { backgroundColor: colors.primary },
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
-
- // --- FLOATING BUTTON STYLES ---
floatingButtonContainer: {
position: 'absolute',
- bottom: 85, // Positioned above the tab bar
+ bottom: 85,
left: 20,
right: 20,
- alignItems: 'center', // Center the button horizontally
- zIndex: 1, // Ensure it's on top
+ alignItems: 'center',
+ zIndex: 1,
},
activateButton: {
backgroundColor: colors.pastelOrange,
@@ -457,7 +482,7 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
- width: '90%', // Adjust width as needed
+ width: '90%',
alignItems: 'center',
},
activateButtonText: {
@@ -467,4 +492,5 @@ const styles = StyleSheet.create({
},
});
+
export default EditGroupScreen;
diff --git a/src/screens/management/Payments.tsx b/src/screens/management/Payments.tsx
index 02bce8d..f6af32b 100644
--- a/src/screens/management/Payments.tsx
+++ b/src/screens/management/Payments.tsx
@@ -1,15 +1,15 @@
import React, { useEffect, useState, useCallback } from 'react';
-import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native';
-import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
-import { StackNavigationProp } from '@react-navigation/stack';
+import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity, Modal, TextInput } from 'react-native';
+import { useRoute, RouteProp } from '@react-navigation/native';
import { Ionicons } from "@react-native-vector-icons/ionicons";
import colors from '../../components/colors';
import AppView from '../../components/AppView';
import { apiCall } from '../../utils/ApiHelper';
-// --- TYPE DEFINITIONS ---
+// --- CONSTANTS ---
+const API_BASE_URL = 'https://exotic-humpback-mistakenly.ngrok-free.app/api/payments';
-// Generic API Response Wrapper
+// --- TYPE DEFINITIONS ---
interface ApiResponse {
status: 'success' | 'error';
data: T;
@@ -17,69 +17,54 @@ interface ApiResponse {
statusCode: number;
}
-// Type for an individual participant in the payment details
interface Participant {
userId: string;
- userName: string;
firstName: string;
lastName: string;
mobile: string;
contributionAmount: number;
paidAmount: number;
+ payAmount?: number;
status: 'SUCCESS' | 'PENDING';
lateFee: number;
- daysLate: number;
lateFeeWaived: boolean;
}
-// Fully typed payment details based on your JSON response
interface PaymentDetails {
groupId: string;
groupName: string;
currentCycle: number;
- cycleType: string;
contributionAmount: number;
- totalContributedAmount: number;
- lateFeeMode: string;
- lateFeeValue: number;
- notReceivedAmount: number;
- totalLateFee: number;
totalDueWithLateFee: number;
participants: Participant[];
}
-// Assuming your navigation stack params look something like this
-type PaymentStackParamList = {
- Payments: { groupId: string };
-};
-
+type PaymentStackParamList = { Payments: { groupId: string } };
type PaymentsRouteProp = RouteProp;
-type PaymentsNavigationProp = StackNavigationProp;
const Payments: React.FC = () => {
const route = useRoute();
- const navigation = useNavigation();
const { groupId } = route.params;
const [paymentDetails, setPaymentDetails] = useState(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
+
+ // State for the waiver modal
+ const [isModalVisible, setModalVisible] = useState(false);
+ const [currentParticipant, setCurrentParticipant] = useState(null);
+ const [waiverAmount, setWaiverAmount] = useState('');
const fetchPaymentDetails = useCallback(async () => {
if (!refreshing) setLoading(true);
try {
- const response = await apiCall(
- `https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
- 'GET'
- ) as ApiResponse;
-
+ const response = await apiCall(`${API_BASE_URL}/current/payment/details/${groupId}`, 'GET') as ApiResponse;
if (response && response.statusCode === 200) {
setPaymentDetails(response.data);
} else {
- Alert.alert("Error", response?.message || "Failed to fetch payment details.");
+ Alert.alert("Error", response?.message || "Failed to fetch details.");
}
} catch (error: any) {
- console.error("Fetch payment details error:", error);
Alert.alert("Error", error.message || "An unexpected error occurred.");
} finally {
setLoading(false);
@@ -91,205 +76,183 @@ const Payments: React.FC = () => {
fetchPaymentDetails();
}, [fetchPaymentDetails]);
- const onRefresh = () => {
+ const onRefresh = useCallback(() => {
setRefreshing(true);
+ fetchPaymentDetails();
+ }, [fetchPaymentDetails]);
+
+ const openWaiverDialog = (participant: Participant) => {
+ setCurrentParticipant(participant);
+ setWaiverAmount('');
+ setModalVisible(true);
+ };
+
+ // --- UPDATED: handleWaiveFee now uses the correct API endpoint ---
+ const handleWaiveFee = async () => {
+ if (!currentParticipant || !paymentDetails) return;
+
+ const amountToWaive = parseFloat(waiverAmount);
+
+ if (isNaN(amountToWaive) || amountToWaive <= 0) {
+ Alert.alert("Invalid Input", "Please enter a waiver amount greater than zero.");
+ return;
+ }
+
+ if (amountToWaive > currentParticipant.lateFee) {
+ Alert.alert("Invalid Amount", `Waiver cannot exceed the current late fee of ₹${currentParticipant.lateFee}.`);
+ return;
+ }
+
+ try {
+ // Construct the exact URL as provided
+ const url = `${API_BASE_URL}/groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
+
+ const response = await apiCall(url, 'PUT', {}); // Body is empty as amount is in URL
+
+ if (response && response.statusCode === 200) {
+ Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`);
+ setModalVisible(false);
+ onRefresh(); // Refresh data to reflect the change
+ } else {
+ throw new Error(response?.message || "Failed to apply waiver.");
+ }
+ } catch (error: any) {
+ Alert.alert("API Error", error.message);
+ }
};
- // --- ENHANCED: renderParticipant now shows late fee details ---
const renderParticipant = (participant: Participant) => {
const isPending = participant.status === 'PENDING';
+ const canWaive = isPending && participant.lateFee > 0;
+
return (
{participant.firstName} {participant.lastName}
{participant.mobile}
+
-
+
{participant.status}
- {isPending ?`₹${participant.payAmount}`:`₹${participant.paidAmount}`}
+ {isPending ? `₹${participant.payAmount || 0}` : `₹${participant.paidAmount}`}
-
- {/* --- NEW: Conditionally render late payment stats --- */}
- {isPending && participant.daysLate > 0 && (
-
- Late Fee: ₹{participant.lateFee}
- Days Late: {participant.daysLate}
-
+ {isPending && participant.lateFee > 0 && (
+ Late Fee: ₹{participant.lateFee}
+ )}
+ {participant.lateFee === 0 && participant.lateFeeWaived && Fee Waived}
+
+
+
+ {canWaive && (
+ openWaiverDialog(participant)}>
+
+
)}
);
};
-
- if (loading && !refreshing) {
- return ;
- }
-
- if (!paymentDetails) {
- return (
-
- No payment details found.
-
- Tap to retry
-
-
- );
- }
-
+
return (
+ setModalVisible(false)}
+ >
+
+
+ Waive Late Fee
+
+ For {currentParticipant?.firstName}. Current late fee is ₹{currentParticipant?.lateFee}.
+
+
+
+ setModalVisible(false)}>
+ Cancel
+
+
+ Apply Waiver
+
+
+
+
+
+
}
+ refreshControl={}
>
-
- {paymentDetails.groupName}
-
-
-
-
- Current Cycle
- {paymentDetails.currentCycle}
-
-
- Contribution
- ₹{paymentDetails.contributionAmount}
-
-
- Total Due
- ₹{paymentDetails.totalDueWithLateFee}
-
-
-
- Participants
- {paymentDetails.participants.map(renderParticipant)}
+ {loading && !refreshing ? (
+
+ ) : (
+ <>
+
+ {paymentDetails?.groupName}
+
+
+
+ Cycle
+ {paymentDetails?.currentCycle}
+
+
+ Contribution
+ ₹{paymentDetails?.contributionAmount}
+
+
+ Total Due
+ ₹{paymentDetails?.totalDueWithLateFee}
+
+
+ Participants
+ {paymentDetails?.participants.map(renderParticipant)}
+ >
+ )}
);
};
const styles = StyleSheet.create({
- centered: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#f4f6f8',
- },
- container: {
- flex: 1,
- backgroundColor: '#f4f6f8',
- },
- header: {
- padding: 20,
- paddingBottom: 15,
- },
- headerTitle: {
- color: '#000000',
- fontSize: 24,
- fontWeight: 'bold',
- },
- summaryContainer: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- padding: 20,
- backgroundColor: '#fff',
- marginHorizontal: 15,
- borderRadius: 12,
- elevation: 2,
- shadowColor: '#000',
- shadowOpacity: 0.05,
- shadowRadius: 10,
- },
- summaryBox: {
- alignItems: 'center',
- },
- summaryLabel: {
- fontSize: 14,
- color: '#6c757d',
- marginBottom: 4,
- },
- summaryValue: {
- fontSize: 18,
- fontWeight: '700',
- color: colors.primary,
- },
- sectionTitle: {
- fontSize: 20,
- fontWeight: 'bold',
- marginLeft: 20,
- marginTop: 20,
- marginBottom: 10,
- color: '#343a40',
- },
- participantCard: {
- backgroundColor: '#fff',
- marginHorizontal: 15,
- marginBottom: 10,
- borderRadius: 10,
- padding: 15,
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- elevation: 1,
- },
- participantInfo: {
- flex: 1,
- },
- participantName: {
- fontSize: 16,
- fontWeight: '600',
- color: '#212529',
- },
- participantMobile: {
- fontSize: 14,
- color: '#6c757d',
- marginTop: 2,
- },
- paymentStatus: {
- alignItems: 'flex-end',
- },
- statusText: {
- fontSize: 14,
- fontWeight: 'bold',
- textTransform: 'uppercase',
- },
- amountText: {
- fontSize: 14,
- color: '#495057',
- marginTop: 2,
- },
- // --- NEW STYLES ---
- lateFeeContainer: {
- marginTop: 8,
- paddingTop: 5,
- borderTopWidth: 1,
- borderTopColor: '#e9ecef',
- alignItems: 'flex-end',
- },
- lateFeeText: {
- fontSize: 13,
- color: colors.danger,
- fontWeight: '600',
- },
- retryButton: {
- marginTop: 20,
- paddingVertical: 10,
- paddingHorizontal: 20,
- borderRadius: 8,
- backgroundColor: colors.primary,
- },
- retryButtonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold',
- },
+ container: { flex: 1, backgroundColor: '#f4f6f8' },
+ header: { padding: 20, paddingBottom: 15 },
+ headerTitle: { color: '#000000', fontSize: 24, fontWeight: 'bold' },
+ summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
+ summaryBox: { alignItems: 'center' },
+ summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
+ summaryValue: { fontSize: 18, fontWeight: '700', color: colors.primary },
+ sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
+ participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
+ participantInfo: { flex: 1 },
+ participantName: { fontSize: 16, fontWeight: '600' },
+ participantMobile: { fontSize: 14, color: '#6c757d', marginTop: 2 },
+ paymentStatus: { alignItems: 'flex-end', marginHorizontal: 10 },
+ statusText: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase' },
+ amountText: { fontSize: 14, color: '#495057', marginTop: 2 },
+ lateFeeText: { fontSize: 13, color: colors.danger, fontWeight: '600', marginTop: 4 },
+ waivedText: { fontSize: 13, color: colors.success, fontWeight: '600', marginTop: 4 },
+ menuContainer: { justifyContent: 'center', alignItems: 'center', minWidth: 25 },
+ modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
+ modalContainer: { width: '85%', backgroundColor: 'white', borderRadius: 15, padding: 20, elevation: 10 },
+ modalTitle: { fontSize: 20, fontWeight: 'bold', color: '#333', marginBottom: 5 },
+ modalSubtitle: { fontSize: 14, color: '#666', marginBottom: 20 },
+ input: { width: '100%', borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 15, paddingVertical: 12, fontSize: 16, marginBottom: 20 },
+ modalButtonContainer: { flexDirection: 'row', justifyContent: 'flex-end' },
+ modalButton: { paddingVertical: 10, paddingHorizontal: 20, borderRadius: 8, marginLeft: 10 },
+ cancelButton: { backgroundColor: '#f0f0f0' },
+ cancelButtonText: { color: '#333', fontWeight: 'bold' },
+ saveButton: { backgroundColor: colors.primary },
+ saveButtonText: { color: 'white', fontWeight: 'bold' },
});
export default Payments;
-
diff --git a/src/screens/management/RandomSelectUserScreen.tsx b/src/screens/management/RandomSelectUserScreen.tsx
new file mode 100644
index 0000000..36a8c09
--- /dev/null
+++ b/src/screens/management/RandomSelectUserScreen.tsx
@@ -0,0 +1,248 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Animated, Easing, SafeAreaView, Dimensions, Alert } from 'react-native';
+import { Confetti, ConfettiMethods } from 'react-native-fast-confetti';
+
+import AppView from '../../components/AppView';
+
+
+// --- PROPS & TYPE DEFINITIONS ---
+export interface RandomWinnerScreenProps {
+ participants: Participant[];
+ excludedParticipantIds: string[];
+}
+
+export interface Participant {
+ id: string;
+ name: string;
+}
+
+// --- CONSTANTS ---
+const { width } = Dimensions.get('window');
+const ITEM_HEIGHT = 70; // Height of each name item
+const ANIMATION_DURATION = 8000; // 8 seconds for suspense
+const SHUFFLE_MULTIPLIER = 5; // Repeats the list for a longer scroll effect
+
+const RandomSelectUserScreen: React.FC = () => {
+ // --- MOCK DATA (Replace with navigation props) ---
+ const mockProps: RandomWinnerScreenProps = {
+ participants: [
+ { id: '1', name: 'Vinay M.' },
+ { id: '2', name: 'Nitesh Y.' },
+ { id: '3', name: 'Alice' },
+ { id: '4', name: 'Bob' },
+ { id: '5', name: 'Charlie' },
+ { id: '6', name: 'David' },
+ { id: '7', name: 'Eve' },
+ ],
+ excludedParticipantIds: ['4'], // Exclude Bob
+ };
+ // --- END MOCK DATA ---
+
+ const { participants, excludedParticipantIds } = mockProps;
+
+ const [eligibleParticipants, setEligibleParticipants] = useState([]);
+ const [isSelecting, setIsSelecting] = useState(false);
+ const [winner, setWinner] = useState(null);
+ const [animationList, setAnimationList] = useState([]);
+
+ const scrollY = useRef(new Animated.Value(0)).current;
+ const confettiRef = useRef(null);
+
+ // Filter participants
+ useEffect(() => {
+ const eligible = participants.filter(p => !excludedParticipantIds.includes(p.id));
+ setEligibleParticipants(eligible);
+ }, [participants, excludedParticipantIds]);
+
+ const handleStartSelection = () => {
+ if (eligibleParticipants.length < 2) {
+ Alert.alert('Not enough eligible participants.');
+ return;
+ }
+
+ setIsSelecting(true);
+ setWinner(null);
+ scrollY.setValue(0);
+
+ // 1. Secretly select winner
+ const winnerIndex = Math.floor(Math.random() * eligibleParticipants.length);
+ const selectedWinner = eligibleParticipants[winnerIndex];
+
+ // 2. Create long, shuffled list for animation
+ let listForAnimation: Participant[] = [];
+ for (let i = 0; i < SHUFFLE_MULTIPLIER; i++) {
+ const shuffled = [...eligibleParticipants].sort(() => Math.random() - 0.5);
+ listForAnimation.push(...shuffled);
+ }
+
+ // 3. Place winner at the end
+ listForAnimation.push(selectedWinner);
+ setAnimationList(listForAnimation);
+
+ // 4. Calculate final scroll position
+ const finalY = -(listForAnimation.length - 1) * ITEM_HEIGHT;
+
+ // 5. Start animation
+ Animated.timing(scrollY, {
+ toValue: finalY,
+ duration: ANIMATION_DURATION,
+ useNativeDriver: true,
+ easing: Easing.bezier(0.3, 0, 0.1, 1),
+ }).start(() => {
+ setWinner(selectedWinner);
+ confettiRef.current?.start();
+ });
+ };
+
+ const handleReset = () => {
+ setIsSelecting(false);
+ setWinner(null);
+ scrollY.setValue(0);
+ confettiRef.current?.stop();
+ };
+
+ const renderListItem = ({ item, index }: { item: Participant, index: number }) => (
+
+ {item.name}
+
+ );
+
+ return (
+
+
+
+ Select a Winner
+
+
+
+ {isSelecting || winner ? (
+
+ {animationList.map((item, index) => renderListItem({ item, index }))}
+
+ ) : (
+
+ Press Start to Select
+
+ )}
+
+
+ {winner && (
+
+ Winner is
+ {winner.name}!
+
+ )}
+
+ {!isSelecting && !winner && (
+
+ Start Selection
+
+ )}
+
+ {winner && (
+
+ Select Again
+
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#1C1C1E',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 40,
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: 'bold',
+ color: '#FFFFFF',
+ textAlign: 'center',
+ marginBottom: 30,
+ },
+ selectionMachine: {
+ height: ITEM_HEIGHT,
+ width: '90%',
+ backgroundColor: '#2C2C2E',
+ borderRadius: 20,
+ overflow: 'hidden',
+ justifyContent: 'center',
+ },
+ winnerPointer: {
+ position: 'absolute',
+ width: '105%',
+ height: ITEM_HEIGHT,
+ borderRadius: 20,
+ borderWidth: 3,
+ borderColor: '#FBC02D',
+ alignSelf: 'center',
+ zIndex: 1,
+ },
+ itemContainer: {
+ height: ITEM_HEIGHT,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ itemName: {
+ fontSize: 36,
+ fontWeight: 'bold',
+ color: '#FFF',
+ },
+ placeholderContainer: {
+ height: ITEM_HEIGHT,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ placeholderText: {
+ fontSize: 24,
+ color: '#666',
+ fontStyle: 'italic',
+ },
+ winnerBanner: {
+ marginTop: 30,
+ padding: 20,
+ backgroundColor: '#FBC02D',
+ borderRadius: 15,
+ alignItems: 'center',
+ },
+ winnerText: {
+ fontSize: 20,
+ color: '#000',
+ },
+ winnerName: {
+ fontSize: 40,
+ fontWeight: 'bold',
+ color: '#000',
+ },
+ startButton: {
+ backgroundColor: '#007AFF',
+ paddingVertical: 18,
+ paddingHorizontal: 40,
+ borderRadius: 30,
+ marginTop: 40,
+ },
+ startButtonText: {
+ color: '#FFFFFF',
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+ resetButton: {
+ backgroundColor: '#333',
+ paddingVertical: 18,
+ paddingHorizontal: 40,
+ borderRadius: 30,
+ marginTop: 40,
+ },
+ resetButtonText: {
+ color: '#FFFFFF',
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+});
+
+export default RandomSelectUserScreen;
+