From 6778e79dc8d5031685bc9f53572b3c71538eb5ab Mon Sep 17 00:00:00 2001 From: Abin Toppo Date: Fri, 26 Sep 2025 10:16:19 +0530 Subject: [PATCH] fund distribution --- package-lock.json | 200 ++++++++-- package.json | 1 + src/navigation/AppNavigator.tsx | 2 + src/screens/management/EditGroupScreen.tsx | 72 ++-- src/screens/management/Payments.tsx | 355 ++++++++---------- .../management/RandomSelectUserScreen.tsx | 248 ++++++++++++ 6 files changed, 630 insertions(+), 248 deletions(-) create mode 100644 src/screens/management/RandomSelectUserScreen.tsx 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; +