Phase 3: MVP Frontend - Admin, Staff Scanner, and Guest View
This commit is contained in:
Generated
+411
-1
@@ -8,12 +8,20 @@
|
|||||||
"name": "hospitality-web",
|
"name": "hospitality-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.90.10",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"html5-qrcode": "^2.3.8",
|
||||||
|
"lucide-react": "^0.554.0",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
@@ -1365,6 +1373,32 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz",
|
||||||
|
"integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.90.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz",
|
||||||
|
"integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1434,6 +1468,16 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode.react": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.6",
|
"version": "19.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||||
@@ -1809,6 +1853,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.22",
|
"version": "10.4.22",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||||
@@ -1847,6 +1897,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1922,6 +1983,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -1990,6 +2064,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -2004,6 +2090,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2026,6 +2121,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2051,6 +2156,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.259",
|
"version": "1.5.259",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
|
||||||
@@ -2058,6 +2186,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -2432,6 +2605,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@@ -2461,6 +2670,15 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -2471,6 +2689,43 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -2497,6 +2752,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@@ -2514,6 +2781,45 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hermes-estree": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@@ -2531,6 +2837,12 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html5-qrcode": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -2732,6 +3044,24 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.554.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz",
|
||||||
|
"integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -2756,6 +3086,27 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -2968,6 +3319,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -2978,6 +3335,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -3030,6 +3396,44 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||||
|
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
|
||||||
|
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.9.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -3133,6 +3537,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,20 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.90.10",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"html5-qrcode": "^2.3.8",
|
||||||
|
"lucide-react": "^0.554.0",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
|||||||
@@ -1,46 +1,47 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||||
|
import EventsPage from './pages/admin/EventsPage';
|
||||||
|
import EventDetailPage from './pages/admin/EventDetailPage';
|
||||||
|
import ScannerPage from './pages/staff/ScannerPage';
|
||||||
|
import GuestQrPage from './pages/guest/GuestQrPage';
|
||||||
|
|
||||||
interface Person {
|
const queryClient = new QueryClient();
|
||||||
id: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [people, setPeople] = useState<Person[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/people')
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
setPeople(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-8">
|
<QueryClientProvider client={queryClient}>
|
||||||
<h1 className="text-3xl font-bold mb-6 text-gray-800">Hospitality Admin</h1>
|
<BrowserRouter>
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<nav className="bg-white shadow-sm border-b">
|
||||||
<h2 className="text-xl font-semibold mb-4">People</h2>
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{people.length === 0 ? (
|
<div className="flex justify-between h-16">
|
||||||
<p className="text-gray-500">No people found.</p>
|
<div className="flex space-x-8">
|
||||||
) : (
|
<Link to="/" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900">
|
||||||
<ul className="space-y-2">
|
Admin
|
||||||
{people.map(person => (
|
</Link>
|
||||||
<li key={person.id} className="p-4 border rounded hover:bg-gray-50">
|
<Link to="/staff" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 hover:text-gray-900">
|
||||||
{person.firstName} {person.lastName} ({person.email})
|
Staff Scanner
|
||||||
</li>
|
</Link>
|
||||||
))}
|
<Link to="/guest" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 hover:text-gray-900">
|
||||||
</ul>
|
Guest View
|
||||||
)}
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<EventsPage />} />
|
||||||
|
<Route path="/events/:id" element={<EventDetailPage />} />
|
||||||
|
<Route path="/staff" element={<ScannerPage />} />
|
||||||
|
<Route path="/guest" element={<GuestQrPage />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { eventsApi } from '../lib/api';
|
||||||
|
import type { Event, CreateEventRequest } from '../lib/types';
|
||||||
|
|
||||||
|
export const useEvents = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['events'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await eventsApi.getAll();
|
||||||
|
return data as Event[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEvent = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['events', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await eventsApi.getById(id);
|
||||||
|
return data as Event;
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateEvent = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: CreateEventRequest) => {
|
||||||
|
const response = await eventsApi.create(data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['events'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { groupsApi, peopleApi } from '../lib/api';
|
||||||
|
import type { Group, CreateGroupRequest, CreatePersonRequest, AssignQuotaRequest } from '../lib/types';
|
||||||
|
|
||||||
|
export const useGroups = (eventId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['groups', eventId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await groupsApi.getByEventId(eventId);
|
||||||
|
return data as Group[];
|
||||||
|
},
|
||||||
|
enabled: !!eventId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGroup = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['groups', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await groupsApi.getById(id);
|
||||||
|
return data as Group;
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateGroup = (eventId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: CreateGroupRequest) => {
|
||||||
|
const response = await groupsApi.create(eventId, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['groups', eventId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreatePerson = (groupId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: CreatePersonRequest) => {
|
||||||
|
const response = await peopleApi.create(groupId, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['groups', groupId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAssignQuota = (personId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: AssignQuotaRequest) => {
|
||||||
|
const response = await peopleApi.assignQuota(personId, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['people', personId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { productsApi } from '../lib/api';
|
||||||
|
import type { Product, CreateProductRequest } from '../lib/types';
|
||||||
|
|
||||||
|
export const useProducts = (eventId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['products', eventId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await productsApi.getByEventId(eventId);
|
||||||
|
return data as Product[];
|
||||||
|
},
|
||||||
|
enabled: !!eventId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateProduct = (eventId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: CreateProductRequest) => {
|
||||||
|
const response = await productsApi.create(eventId, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['products', eventId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { qrCodeApi, transactionsApi } from '../lib/api';
|
||||||
|
import type { Person, Quota, CreateTransactionRequest } from '../lib/types';
|
||||||
|
|
||||||
|
export const usePersonByQrCode = (qrCode: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['qr', qrCode],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await qrCodeApi.getByQrCode(qrCode);
|
||||||
|
return data as Person;
|
||||||
|
},
|
||||||
|
enabled: !!qrCode && qrCode.length > 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useQuotasByQrCode = (qrCode: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['qr', qrCode, 'quotas'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await qrCodeApi.getQuotas(qrCode);
|
||||||
|
return data as Quota[];
|
||||||
|
},
|
||||||
|
enabled: !!qrCode && qrCode.length > 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateTransaction = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: CreateTransactionRequest) => {
|
||||||
|
const response = await transactionsApi.create(data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['qr', variables.qrCode] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['qr', variables.qrCode, 'quotas'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5163/api';
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event API
|
||||||
|
export const eventsApi = {
|
||||||
|
getAll: () => api.get('/events'),
|
||||||
|
getById: (id: string) => api.get(`/events/${id}`),
|
||||||
|
create: (data: any) => api.post('/events', data),
|
||||||
|
update: (id: string, data: any) => api.put(`/events/${id}`, data),
|
||||||
|
delete: (id: string) => api.delete(`/events/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group API
|
||||||
|
export const groupsApi = {
|
||||||
|
getByEventId: (eventId: string) => api.get(`/events/${eventId}/groups`),
|
||||||
|
getById: (id: string) => api.get(`/groups/${id}`),
|
||||||
|
create: (eventId: string, data: any) => api.post(`/events/${eventId}/groups`, data),
|
||||||
|
update: (id: string, data: any) => api.put(`/groups/${id}`, data),
|
||||||
|
delete: (id: string) => api.delete(`/groups/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// People API
|
||||||
|
export const peopleApi = {
|
||||||
|
getById: (id: string) => api.get(`/people/${id}`),
|
||||||
|
create: (groupId: string, data: any) => api.post(`/groups/${groupId}/people`, data),
|
||||||
|
update: (id: string, data: any) => api.put(`/people/${id}`, data),
|
||||||
|
delete: (id: string) => api.delete(`/people/${id}`),
|
||||||
|
assignQuota: (personId: string, data: any) => api.post(`/people/${personId}/quotas`, data),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Product API
|
||||||
|
export const productsApi = {
|
||||||
|
getByEventId: (eventId: string) => api.get(`/events/${eventId}/products`),
|
||||||
|
create: (eventId: string, data: any) => api.post(`/events/${eventId}/products`, data),
|
||||||
|
update: (id: string, data: any) => api.put(`/products/${id}`, data),
|
||||||
|
delete: (id: string) => api.delete(`/products/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// QR Code API
|
||||||
|
export const qrCodeApi = {
|
||||||
|
getByQrCode: (qrCode: string) => api.get(`/qr/${qrCode}`),
|
||||||
|
getQuotas: (qrCode: string) => api.get(`/qr/${qrCode}/quotas`),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transaction API
|
||||||
|
export const transactionsApi = {
|
||||||
|
create: (data: any) => api.post('/transactions', data),
|
||||||
|
getByPersonId: (personId: string) => api.get(`/transactions/person/${personId}`),
|
||||||
|
getByEventId: (eventId: string) => api.get(`/transactions/event/${eventId}`),
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
export enum ProductType {
|
||||||
|
Access = 0,
|
||||||
|
Drink = 1,
|
||||||
|
Meal = 2,
|
||||||
|
Special = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
location: string;
|
||||||
|
groupCount?: number;
|
||||||
|
productCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
id: string;
|
||||||
|
eventId: string;
|
||||||
|
name: string;
|
||||||
|
contactPersonName?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
peopleCount?: number;
|
||||||
|
people?: Person[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Person {
|
||||||
|
id: string;
|
||||||
|
groupId: string;
|
||||||
|
qrCode: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
quotas?: Quota[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
eventId: string;
|
||||||
|
name: string;
|
||||||
|
type: ProductType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Quota {
|
||||||
|
productId: string;
|
||||||
|
productName: string;
|
||||||
|
productType: string;
|
||||||
|
initialAmount: number;
|
||||||
|
usedAmount: number;
|
||||||
|
remainingAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: string;
|
||||||
|
personId: string;
|
||||||
|
personName: string;
|
||||||
|
productId: string;
|
||||||
|
productName: string;
|
||||||
|
amount: number;
|
||||||
|
timestamp: string;
|
||||||
|
staffId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request types
|
||||||
|
export interface CreateEventRequest {
|
||||||
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateGroupRequest {
|
||||||
|
name: string;
|
||||||
|
contactPersonName?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePersonRequest {
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProductRequest {
|
||||||
|
name: string;
|
||||||
|
type: ProductType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTransactionRequest {
|
||||||
|
qrCode: string;
|
||||||
|
productId: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignQuotaRequest {
|
||||||
|
productId: string;
|
||||||
|
initialAmount: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useEvent } from '../../hooks/useEvents';
|
||||||
|
import { useGroups, useCreateGroup, useCreatePerson } from '../../hooks/useGroups';
|
||||||
|
import { useProducts, useCreateProduct } from '../../hooks/useProducts';
|
||||||
|
import { ProductType, type CreateGroupRequest, type CreatePersonRequest, type CreateProductRequest } from '../../lib/types';
|
||||||
|
|
||||||
|
export default function EventDetailPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const { data: event } = useEvent(id!);
|
||||||
|
const { data: groups } = useGroups(id!);
|
||||||
|
const { data: products } = useProducts(id!);
|
||||||
|
const createGroup = useCreateGroup(id!);
|
||||||
|
const createProduct = useCreateProduct(id!);
|
||||||
|
|
||||||
|
const [showGroupForm, setShowGroupForm] = useState(false);
|
||||||
|
const [showProductForm, setShowProductForm] = useState(false);
|
||||||
|
const [groupForm, setGroupForm] = useState<CreateGroupRequest>({ name: '' });
|
||||||
|
const [productForm, setProductForm] = useState<CreateProductRequest>({ name: '', type: ProductType.Drink });
|
||||||
|
|
||||||
|
const handleCreateGroup = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await createGroup.mutateAsync(groupForm);
|
||||||
|
setGroupForm({ name: '' });
|
||||||
|
setShowGroupForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateProduct = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await createProduct.mutateAsync(productForm);
|
||||||
|
setProductForm({ name: '', type: ProductType.Drink });
|
||||||
|
setShowProductForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!event) return <div className="p-8">Loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">{event.name}</h1>
|
||||||
|
<p className="text-gray-600 mt-2">{event.location}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Groups Section */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Groups</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowGroupForm(!showGroupForm)}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 text-sm"
|
||||||
|
>
|
||||||
|
{showGroupForm ? 'Cancel' : 'Add Group'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showGroupForm && (
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||||
|
<form onSubmit={handleCreateGroup} className="space-y-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Group Name"
|
||||||
|
required
|
||||||
|
value={groupForm.name}
|
||||||
|
onChange={(e) => setGroupForm({ ...groupForm, name: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Contact Person (optional)"
|
||||||
|
value={groupForm.contactPersonName || ''}
|
||||||
|
onChange={(e) => setGroupForm({ ...groupForm, contactPersonName: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Contact Email (optional)"
|
||||||
|
value={groupForm.contactEmail || ''}
|
||||||
|
onChange={(e) => setGroupForm({ ...groupForm, contactEmail: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Create Group
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{groups?.map((group) => (
|
||||||
|
<div key={group.id} className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="font-semibold text-lg">{group.name}</h3>
|
||||||
|
{group.contactPersonName && (
|
||||||
|
<p className="text-sm text-gray-600">{group.contactPersonName}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-gray-500 mt-2">{group.peopleCount || 0} people</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products Section */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Products</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowProductForm(!showProductForm)}
|
||||||
|
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 text-sm"
|
||||||
|
>
|
||||||
|
{showProductForm ? 'Cancel' : 'Add Product'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showProductForm && (
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||||
|
<form onSubmit={handleCreateProduct} className="space-y-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Product Name"
|
||||||
|
required
|
||||||
|
value={productForm.name}
|
||||||
|
onChange={(e) => setProductForm({ ...productForm, name: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={productForm.type}
|
||||||
|
onChange={(e) => setProductForm({ ...productForm, type: parseInt(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value={ProductType.Access}>Access</option>
|
||||||
|
<option value={ProductType.Drink}>Drink</option>
|
||||||
|
<option value={ProductType.Meal}>Meal</option>
|
||||||
|
<option value={ProductType.Special}>Special</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Create Product
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{products?.map((product) => (
|
||||||
|
<div key={product.id} className="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h3 className="font-semibold text-lg">{product.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500">{ProductType[product.type]}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useEvents, useCreateEvent } from '../../hooks/useEvents';
|
||||||
|
import type { CreateEventRequest } from '../../lib/types';
|
||||||
|
|
||||||
|
export default function EventsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: events, isLoading } = useEvents();
|
||||||
|
const createEvent = useCreateEvent();
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<CreateEventRequest>({
|
||||||
|
name: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
location: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const result = await createEvent.mutateAsync(formData);
|
||||||
|
setShowForm(false);
|
||||||
|
setFormData({ name: '', startDate: '', endDate: '', location: '' });
|
||||||
|
navigate(`/events/${result.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create event:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div className="p-8">Loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Events</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(!showForm)}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{showForm ? 'Cancel' : 'Create Event'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm && (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Create New Event</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Event Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
required
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
required
|
||||||
|
value={formData.endDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Location</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.location}
|
||||||
|
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={createEvent.isPending}
|
||||||
|
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{createEvent.isPending ? 'Creating...' : 'Create Event'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{events?.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
onClick={() => navigate(`/events/${event.id}`)}
|
||||||
|
className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">{event.name}</h3>
|
||||||
|
<p className="text-gray-600 mb-4">{event.location}</p>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<p>{new Date(event.startDate).toLocaleDateString()}</p>
|
||||||
|
<div className="flex gap-4 mt-2">
|
||||||
|
<span>{event.groupCount || 0} groups</span>
|
||||||
|
<span>{event.productCount || 0} products</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { usePersonByQrCode } from '../../hooks/useQrCode';
|
||||||
|
|
||||||
|
export default function GuestQrPage() {
|
||||||
|
const [qrCode, setQrCode] = useState('');
|
||||||
|
const [showQr, setShowQr] = useState(false);
|
||||||
|
const { data: person } = usePersonByQrCode(qrCode);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowQr(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">Guest View</h1>
|
||||||
|
|
||||||
|
{!showQr ? (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Enter Your QR Code
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={qrCode}
|
||||||
|
onChange={(e) => setQrCode(e.target.value)}
|
||||||
|
placeholder="Your QR code (GUID)"
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg text-lg"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white px-4 py-3 rounded-lg hover:bg-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Show My QR Code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow text-center">
|
||||||
|
{person && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{person.name}</h2>
|
||||||
|
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<div className="bg-white p-6 rounded-lg border-4 border-gray-200">
|
||||||
|
<QRCodeSVG value={qrCode} size={256} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Your Quotas</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{person.quotas?.map((quota) => (
|
||||||
|
<div key={quota.productId} className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="font-semibold">{quota.productName}</h4>
|
||||||
|
<p className="text-sm text-gray-500">{quota.productType}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{quota.remainingAmount}</p>
|
||||||
|
<p className="text-xs text-gray-500">of {quota.initialAmount} remaining</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full"
|
||||||
|
style={{ width: `${(quota.remainingAmount / quota.initialAmount) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowQr(false)}
|
||||||
|
className="mt-6 text-blue-600 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
Enter Different Code
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { usePersonByQrCode, useCreateTransaction } from '../../hooks/useQrCode';
|
||||||
|
|
||||||
|
export default function ScannerPage() {
|
||||||
|
const [qrCode, setQrCode] = useState('');
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState('');
|
||||||
|
const { data: person, isLoading } = usePersonByQrCode(qrCode);
|
||||||
|
const createTransaction = useCreateTransaction();
|
||||||
|
|
||||||
|
const handleScan = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// QR code is set, person data will load automatically
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransaction = async (productId: string, amount: number) => {
|
||||||
|
try {
|
||||||
|
await createTransaction.mutateAsync({
|
||||||
|
qrCode,
|
||||||
|
productId,
|
||||||
|
amount,
|
||||||
|
});
|
||||||
|
alert('Transaction recorded!');
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error.response?.data || 'Failed to record transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">Staff Scanner</h1>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow mb-6">
|
||||||
|
<form onSubmit={handleScan} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Scan or Enter QR Code
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={qrCode}
|
||||||
|
onChange={(e) => setQrCode(e.target.value)}
|
||||||
|
placeholder="Enter QR code (GUID)"
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg text-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white px-4 py-3 rounded-lg hover:bg-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Lookup Person
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <div className="text-center py-8">Loading...</div>}
|
||||||
|
|
||||||
|
{person && (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">{person.name}</h2>
|
||||||
|
{person.email && <p className="text-gray-600 mb-4">{person.email}</p>}
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Available Quotas</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{person.quotas?.map((quota) => (
|
||||||
|
<div key={quota.productId} className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">{quota.productName}</h4>
|
||||||
|
<p className="text-sm text-gray-500">{quota.productType}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{quota.remainingAmount}</p>
|
||||||
|
<p className="text-xs text-gray-500">of {quota.initialAmount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleTransaction(quota.productId, 1)}
|
||||||
|
disabled={quota.remainingAmount < 1 || createTransaction.isPending}
|
||||||
|
className="flex-1 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Use 1
|
||||||
|
</button>
|
||||||
|
{quota.remainingAmount >= 2 && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleTransaction(quota.productId, 2)}
|
||||||
|
disabled={createTransaction.isPending}
|
||||||
|
className="flex-1 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Use 2
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user