Fix Tailwind CSS v4 build error
Build And Push Image / docker (push) Successful in 1m54s
Details
Build And Push Image / docker (push) Successful in 1m54s
Details
- Install @tailwindcss/postcss package for v4 compatibility
- Update PostCSS configuration to use new package structure
- Fix nuxt.config.ts PostCSS plugin configuration
- Resolves Docker build failures
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a0d703e7cb
commit
a0d2d2b00f
|
|
@ -93,7 +93,11 @@
|
||||||
"Read(/Z:\\Repos\\monacousa-portal\\components/**)",
|
"Read(/Z:\\Repos\\monacousa-portal\\components/**)",
|
||||||
"Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)",
|
"Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)",
|
||||||
"Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
|
"Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
|
||||||
"Bash(git pull:*)"
|
"Bash(git pull:*)",
|
||||||
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(git branch:*)",
|
||||||
|
"mcp__zen__consensus",
|
||||||
|
"mcp___21st-dev_magic__21st_magic_component_refiner"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
|
@ -18,7 +18,7 @@ export default defineNuxtConfig({
|
||||||
css: ["~/assets/css/tailwind.css", "~/assets/scss/main.scss"],
|
css: ["~/assets/css/tailwind.css", "~/assets/scss/main.scss"],
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
"vuetify-nuxt-module": "^0.18.3"
|
"vuetify-nuxt-module": "^0.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
|
|
@ -52,6 +53,19 @@
|
||||||
"sass": "^1.91.0"
|
"sass": "^1.91.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
|
|
@ -2664,6 +2678,17 @@
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/remapping": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
|
@ -2684,9 +2709,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
|
@ -5417,6 +5442,282 @@
|
||||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
|
"enhanced-resolve": "^5.18.3",
|
||||||
|
"jiti": "^2.5.1",
|
||||||
|
"lightningcss": "1.30.1",
|
||||||
|
"magic-string": "^0.30.18",
|
||||||
|
"source-map-js": "^1.2.1",
|
||||||
|
"tailwindcss": "4.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.4",
|
||||||
|
"tar": "^7.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@napi-rs/wasm-runtime",
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@tybys/wasm-util",
|
||||||
|
"@emnapi/wasi-threads",
|
||||||
|
"tslib"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.4.5",
|
||||||
|
"@emnapi/runtime": "^1.4.5",
|
||||||
|
"@emnapi/wasi-threads": "^1.0.4",
|
||||||
|
"@napi-rs/wasm-runtime": "^0.2.12",
|
||||||
|
"@tybys/wasm-util": "^0.10.0",
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/postcss": {
|
||||||
|
"version": "4.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz",
|
||||||
|
"integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
|
"@tailwindcss/node": "4.1.13",
|
||||||
|
"@tailwindcss/oxide": "4.1.13",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "4.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/virtual-core": {
|
"node_modules/@tanstack/virtual-core": {
|
||||||
"version": "3.13.12",
|
"version": "3.13.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||||
|
|
@ -11210,9 +11511,8 @@
|
||||||
"version": "1.30.1",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -11243,12 +11543,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11264,12 +11564,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11285,12 +11585,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11306,12 +11606,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11327,12 +11627,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11348,12 +11648,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11369,12 +11669,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11390,12 +11690,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11411,12 +11711,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11432,12 +11732,12 @@
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -11691,12 +11991,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.18",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string-ast": {
|
"node_modules/magic-string-ast": {
|
||||||
|
|
@ -15279,9 +15579,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||||
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
|
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
"vuetify-nuxt-module": "^0.18.3"
|
"vuetify-nuxt-module": "^0.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,721 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="admin-dashboard-v2">
|
|
||||||
<!-- Neumorphic Header -->
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<h1 class="dashboard-title">System Administration</h1>
|
|
||||||
<p class="dashboard-subtitle">Complete platform control and management</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Grid with Neumorphic Cards -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card neumorphic-card" v-for="stat in stats" :key="stat.id">
|
|
||||||
<div class="stat-content">
|
|
||||||
<div class="stat-info">
|
|
||||||
<div class="stat-label">{{ stat.label }}</div>
|
|
||||||
<div class="stat-value">{{ stat.value }}</div>
|
|
||||||
<div class="stat-change" :class="stat.changeType">
|
|
||||||
<Icon :name="stat.changeIcon" class="change-icon" />
|
|
||||||
<span>{{ stat.changeText }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon-wrapper neumorphic-inset">
|
|
||||||
<Icon :name="stat.icon" class="stat-icon" :style="{ color: stat.color }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Management Sections -->
|
|
||||||
<div class="management-grid">
|
|
||||||
<!-- User Management -->
|
|
||||||
<div class="management-card neumorphic-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<Icon name="mdi:account-group" class="header-icon" />
|
|
||||||
<h2>User Management</h2>
|
|
||||||
</div>
|
|
||||||
<p class="card-description">Manage user accounts, roles, and permissions</p>
|
|
||||||
|
|
||||||
<!-- Morphing Dropdown for User Filters -->
|
|
||||||
<div class="morphing-select-wrapper">
|
|
||||||
<div class="select-trigger neumorphic-button" @click="toggleUserFilter">
|
|
||||||
<span>{{ selectedUserFilter }}</span>
|
|
||||||
<Icon name="mdi:chevron-down" class="dropdown-icon" :class="{ 'rotate': showUserFilter }" />
|
|
||||||
</div>
|
|
||||||
<Transition name="morph">
|
|
||||||
<div v-if="showUserFilter" class="morphing-dropdown">
|
|
||||||
<div
|
|
||||||
v-for="option in userFilterOptions"
|
|
||||||
:key="option"
|
|
||||||
class="dropdown-option"
|
|
||||||
@click="selectUserFilter(option)"
|
|
||||||
>
|
|
||||||
{{ option }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="neumorphic-button primary" @click="showCreateUserDialog = true">
|
|
||||||
<Icon name="mdi:account-plus" />
|
|
||||||
Create User
|
|
||||||
</button>
|
|
||||||
<button class="neumorphic-button" @click="manageRoles">
|
|
||||||
<Icon name="mdi:shield-account" />
|
|
||||||
Manage Roles
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- System Maintenance -->
|
|
||||||
<div class="management-card neumorphic-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<Icon name="mdi:cog" class="header-icon" />
|
|
||||||
<h2>System Maintenance</h2>
|
|
||||||
</div>
|
|
||||||
<p class="card-description">Backend operations and system health</p>
|
|
||||||
|
|
||||||
<!-- System Status Indicator -->
|
|
||||||
<div class="system-status neumorphic-inset">
|
|
||||||
<div class="status-indicator" :class="systemStatus.type"></div>
|
|
||||||
<span>{{ systemStatus.text }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="neumorphic-button" @click="assignMemberIds">
|
|
||||||
Assign Member IDs
|
|
||||||
</button>
|
|
||||||
<button class="neumorphic-button" @click="backfillEventIds">
|
|
||||||
Backfill Event IDs
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reports & Analytics -->
|
|
||||||
<div class="management-card neumorphic-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<Icon name="mdi:chart-line" class="header-icon" />
|
|
||||||
<h2>Reports & Analytics</h2>
|
|
||||||
</div>
|
|
||||||
<p class="card-description">Generate insights and track metrics</p>
|
|
||||||
|
|
||||||
<!-- Report Type Dropdown -->
|
|
||||||
<div class="morphing-select-wrapper">
|
|
||||||
<div class="select-trigger neumorphic-button" @click="toggleReportType">
|
|
||||||
<span>{{ selectedReportType }}</span>
|
|
||||||
<Icon name="mdi:chevron-down" class="dropdown-icon" :class="{ 'rotate': showReportType }" />
|
|
||||||
</div>
|
|
||||||
<Transition name="morph">
|
|
||||||
<div v-if="showReportType" class="morphing-dropdown">
|
|
||||||
<div
|
|
||||||
v-for="type in reportTypes"
|
|
||||||
:key="type"
|
|
||||||
class="dropdown-option"
|
|
||||||
@click="selectReportType(type)"
|
|
||||||
>
|
|
||||||
{{ type }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="neumorphic-button primary full-width" @click="generateReport">
|
|
||||||
<Icon name="mdi:file-chart" />
|
|
||||||
Generate Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Configuration -->
|
|
||||||
<div class="management-card neumorphic-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<Icon name="mdi:tune" class="header-icon" />
|
|
||||||
<h2>Configuration</h2>
|
|
||||||
</div>
|
|
||||||
<p class="card-description">Portal settings and integrations</p>
|
|
||||||
|
|
||||||
<div class="config-grid">
|
|
||||||
<button class="config-button neumorphic-button" @click="showMembershipConfig = true">
|
|
||||||
<Icon name="mdi:card-account-details" />
|
|
||||||
<span>Membership</span>
|
|
||||||
</button>
|
|
||||||
<button class="config-button neumorphic-button" @click="showRecaptchaConfig = true">
|
|
||||||
<Icon name="mdi:robot" />
|
|
||||||
<span>reCAPTCHA</span>
|
|
||||||
</button>
|
|
||||||
<button class="config-button neumorphic-button" @click="openEmailConfig">
|
|
||||||
<Icon name="mdi:email" />
|
|
||||||
<span>Email</span>
|
|
||||||
</button>
|
|
||||||
<button class="config-button neumorphic-button" @click="showNocoDBSettings = true">
|
|
||||||
<Icon name="mdi:database" />
|
|
||||||
<span>Database</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Activity Feed -->
|
|
||||||
<div class="activity-section neumorphic-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<Icon name="mdi:timeline" class="header-icon" />
|
|
||||||
<h2>Recent Activity</h2>
|
|
||||||
<button class="neumorphic-button small" @click="refreshActivity">
|
|
||||||
<Icon name="mdi:refresh" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="activity-list">
|
|
||||||
<div v-for="activity in recentActivity" :key="activity.id" class="activity-item neumorphic-inset">
|
|
||||||
<Icon :name="activity.icon" class="activity-icon" :style="{ color: activity.color }" />
|
|
||||||
<div class="activity-content">
|
|
||||||
<p class="activity-text">{{ activity.text }}</p>
|
|
||||||
<span class="activity-time">{{ activity.time }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
|
|
||||||
// Define page meta
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'admin',
|
|
||||||
middleware: 'auth'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stats data
|
|
||||||
const stats = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
label: 'Total Members',
|
|
||||||
value: '1,247',
|
|
||||||
changeType: 'positive',
|
|
||||||
changeIcon: 'mdi:trending-up',
|
|
||||||
changeText: '+12% this month',
|
|
||||||
icon: 'mdi:account-group',
|
|
||||||
color: '#CC0000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
label: 'Active Sessions',
|
|
||||||
value: '342',
|
|
||||||
changeType: 'neutral',
|
|
||||||
changeIcon: 'mdi:circle',
|
|
||||||
changeText: 'Live now',
|
|
||||||
icon: 'mdi:monitor-dashboard',
|
|
||||||
color: '#10B981'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
label: 'Revenue MTD',
|
|
||||||
value: '$48,392',
|
|
||||||
changeType: 'positive',
|
|
||||||
changeIcon: 'mdi:trending-up',
|
|
||||||
changeText: '+8% vs last month',
|
|
||||||
icon: 'mdi:currency-usd',
|
|
||||||
color: '#3B82F6'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
label: 'System Health',
|
|
||||||
value: '98.5%',
|
|
||||||
changeType: 'positive',
|
|
||||||
changeIcon: 'mdi:check-circle',
|
|
||||||
changeText: 'All systems operational',
|
|
||||||
icon: 'mdi:shield-check',
|
|
||||||
color: '#10B981'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// Dropdown states
|
|
||||||
const showUserFilter = ref(false)
|
|
||||||
const selectedUserFilter = ref('All Users')
|
|
||||||
const userFilterOptions = ref(['All Users', 'Active Users', 'Inactive Users', 'Admins', 'Members'])
|
|
||||||
|
|
||||||
const showReportType = ref(false)
|
|
||||||
const selectedReportType = ref('Financial Report')
|
|
||||||
const reportTypes = ref(['Financial Report', 'Member Report', 'Activity Report', 'Usage Report'])
|
|
||||||
|
|
||||||
// System status
|
|
||||||
const systemStatus = ref({
|
|
||||||
type: 'healthy',
|
|
||||||
text: 'All systems operational'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Recent activity
|
|
||||||
const recentActivity = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
icon: 'mdi:account-plus',
|
|
||||||
text: 'New member registration: John Doe',
|
|
||||||
time: '2 minutes ago',
|
|
||||||
color: '#10B981'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
icon: 'mdi:credit-card',
|
|
||||||
text: 'Payment received from Jane Smith',
|
|
||||||
time: '15 minutes ago',
|
|
||||||
color: '#3B82F6'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
icon: 'mdi:calendar-check',
|
|
||||||
text: 'Event created: Annual Gala 2024',
|
|
||||||
time: '1 hour ago',
|
|
||||||
color: '#F59E0B'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
icon: 'mdi:account-edit',
|
|
||||||
text: 'Profile updated: Mike Johnson',
|
|
||||||
time: '3 hours ago',
|
|
||||||
color: '#6B7280'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// Dialog states
|
|
||||||
const showCreateUserDialog = ref(false)
|
|
||||||
const showMembershipConfig = ref(false)
|
|
||||||
const showRecaptchaConfig = ref(false)
|
|
||||||
const showNocoDBSettings = ref(false)
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const toggleUserFilter = () => {
|
|
||||||
showUserFilter.value = !showUserFilter.value
|
|
||||||
showReportType.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectUserFilter = (option) => {
|
|
||||||
selectedUserFilter.value = option
|
|
||||||
showUserFilter.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleReportType = () => {
|
|
||||||
showReportType.value = !showReportType.value
|
|
||||||
showUserFilter.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectReportType = (type) => {
|
|
||||||
selectedReportType.value = type
|
|
||||||
showReportType.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const manageRoles = () => {
|
|
||||||
console.log('Managing roles...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignMemberIds = () => {
|
|
||||||
console.log('Assigning member IDs...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const backfillEventIds = () => {
|
|
||||||
console.log('Backfilling event IDs...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateReport = () => {
|
|
||||||
console.log('Generating report:', selectedReportType.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openEmailConfig = () => {
|
|
||||||
console.log('Opening email configuration...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshActivity = () => {
|
|
||||||
console.log('Refreshing activity...')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('.morphing-select-wrapper')) {
|
|
||||||
showUserFilter.value = false
|
|
||||||
showReportType.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '@/assets/scss/design-system-v2.scss';
|
|
||||||
|
|
||||||
.admin-dashboard-v2 {
|
|
||||||
padding: 2rem;
|
|
||||||
background: linear-gradient(135deg, $neutral-50 0%, $neutral-100 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header
|
|
||||||
.dashboard-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
|
|
||||||
.dashboard-title {
|
|
||||||
font-size: $text-4xl;
|
|
||||||
font-weight: $font-bold;
|
|
||||||
color: $neutral-800;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background: linear-gradient(135deg, $primary-600, $primary-800);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-subtitle {
|
|
||||||
color: $neutral-600;
|
|
||||||
font-size: $text-lg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats Grid
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
@include neumorphic-card('md');
|
|
||||||
padding: 1.5rem;
|
|
||||||
transition: all $transition-base;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@include neumorphic-card('lg');
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: $neutral-600;
|
|
||||||
font-size: $text-sm;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: $text-3xl;
|
|
||||||
font-weight: $font-bold;
|
|
||||||
color: $neutral-800;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-change {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
font-size: $text-sm;
|
|
||||||
|
|
||||||
&.positive {
|
|
||||||
color: $success-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.neutral {
|
|
||||||
color: $neutral-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-icon {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon-wrapper {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: $radius-xl;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: $shadow-inset-sm;
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Management Grid
|
|
||||||
.management-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.management-card {
|
|
||||||
@include neumorphic-card('md');
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.header-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
color: $primary-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: $text-xl;
|
|
||||||
font-weight: $font-semibold;
|
|
||||||
color: $neutral-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-description {
|
|
||||||
color: $neutral-600;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
font-size: $text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: $text-sm;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Morphing Dropdown
|
|
||||||
.morphing-select-wrapper {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
.select-trigger {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.dropdown-icon {
|
|
||||||
transition: transform 0.3s $spring-smooth;
|
|
||||||
|
|
||||||
&.rotate {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.morphing-dropdown {
|
|
||||||
@include morphing-dropdown();
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: $z-dropdown;
|
|
||||||
|
|
||||||
.dropdown-option {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all $transition-fast;
|
|
||||||
color: $neutral-700;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba($blue-500, 0.1);
|
|
||||||
color: $blue-600;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neumorphic Elements
|
|
||||||
.neumorphic-card {
|
|
||||||
background: linear-gradient(145deg, #ffffff, #f0f0f0);
|
|
||||||
border-radius: $radius-xl;
|
|
||||||
box-shadow: $shadow-soft-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neumorphic-button {
|
|
||||||
@include neumorphic-button();
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
font-weight: $font-medium;
|
|
||||||
color: $neutral-700;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: linear-gradient(145deg, $primary-600, $primary-700);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: linear-gradient(145deg, $primary-700, $primary-800);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
font-size: $text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.full-width {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.neumorphic-inset {
|
|
||||||
box-shadow: $shadow-inset-sm;
|
|
||||||
background: linear-gradient(145deg, #e6e6e6, #ffffff);
|
|
||||||
}
|
|
||||||
|
|
||||||
// System Status
|
|
||||||
.system-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
|
|
||||||
&.healthy {
|
|
||||||
background-color: $success-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
background-color: $warning-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
background-color: $error-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity Section
|
|
||||||
.activity-section {
|
|
||||||
@include neumorphic-card('lg');
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
|
|
||||||
.activity-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.activity-text {
|
|
||||||
color: $neutral-800;
|
|
||||||
font-size: $text-sm;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-time {
|
|
||||||
color: $neutral-500;
|
|
||||||
font-size: $text-xs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transitions
|
|
||||||
.morph-enter-active,
|
|
||||||
.morph-leave-active {
|
|
||||||
transition: all 0.3s $spring-smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
.morph-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95) translateY(-10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.morph-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95) translateY(-10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responsive
|
|
||||||
@include responsive($breakpoint-md) {
|
|
||||||
.admin-dashboard-v2 {
|
|
||||||
padding: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include responsive($breakpoint-lg) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.management-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,996 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid class="pa-6">
|
|
||||||
<!-- Animated Header with Gradient -->
|
|
||||||
<div class="header-section mb-8">
|
|
||||||
<v-row align="center" justify="space-between">
|
|
||||||
<v-col cols="auto">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar size="56" class="gradient-avatar mr-4 elevation-3">
|
|
||||||
<v-icon size="32" color="white">mdi-account-group</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-h3 font-weight-bold gradient-text">Member Directory</h1>
|
|
||||||
<p class="text-body-1 text-medium-emphasis mt-1">
|
|
||||||
<v-icon size="18" class="mr-1">mdi-account-multiple</v-icon>
|
|
||||||
{{ stats.total }} total members • {{ stats.active }} active
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="auto">
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
elevation="3"
|
|
||||||
rounded="lg"
|
|
||||||
prepend-icon="mdi-account-plus"
|
|
||||||
@click="showCreateDialog = true"
|
|
||||||
class="pulse-animation"
|
|
||||||
>
|
|
||||||
Add New Member
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Enhanced Stats Cards with Glassmorphism -->
|
|
||||||
<v-row class="mb-8">
|
|
||||||
<v-col v-for="stat in statsCards" :key="stat.title" cols="12" sm="6" md="3">
|
|
||||||
<v-card
|
|
||||||
class="stat-card glass-card"
|
|
||||||
elevation="0"
|
|
||||||
:style="`border-left: 4px solid ${stat.color}`"
|
|
||||||
>
|
|
||||||
<v-card-text class="pa-5">
|
|
||||||
<div class="d-flex align-center justify-space-between mb-3">
|
|
||||||
<v-avatar :color="stat.color" size="48" class="elevation-2">
|
|
||||||
<v-icon color="white">{{ stat.icon }}</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
<v-chip
|
|
||||||
v-if="stat.change"
|
|
||||||
:color="stat.changeType === 'increase' ? 'success' : 'error'"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
<v-icon size="14">
|
|
||||||
{{ stat.changeType === 'increase' ? 'mdi-trending-up' : 'mdi-trending-down' }}
|
|
||||||
</v-icon>
|
|
||||||
{{ stat.change }}
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
|
||||||
<div class="text-h3 font-weight-bold mb-1">{{ stat.value }}</div>
|
|
||||||
<div class="text-body-2 text-medium-emphasis">{{ stat.title }}</div>
|
|
||||||
<v-progress-linear
|
|
||||||
v-if="stat.progress"
|
|
||||||
:model-value="stat.progress"
|
|
||||||
:color="stat.color"
|
|
||||||
height="4"
|
|
||||||
rounded
|
|
||||||
class="mt-3"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Enhanced Search & Filters Bar -->
|
|
||||||
<v-card class="filter-card glass-card mb-6" elevation="0">
|
|
||||||
<v-card-text class="pa-5">
|
|
||||||
<v-row align="center">
|
|
||||||
<v-col cols="12" md="5">
|
|
||||||
<v-text-field
|
|
||||||
v-model="searchQuery"
|
|
||||||
label="Search members"
|
|
||||||
placeholder="Name, email, or ID..."
|
|
||||||
prepend-inner-icon="mdi-magnify"
|
|
||||||
variant="solo"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
class="search-field"
|
|
||||||
>
|
|
||||||
<template v-slot:append-inner>
|
|
||||||
<v-badge
|
|
||||||
v-if="searchQuery"
|
|
||||||
:content="filteredMembers.length"
|
|
||||||
color="primary"
|
|
||||||
inline
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" md="7">
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<v-chip-group
|
|
||||||
v-model="quickFilter"
|
|
||||||
selected-class="chip-active"
|
|
||||||
>
|
|
||||||
<v-chip filter variant="outlined" value="all">
|
|
||||||
<v-icon start size="18">mdi-all-inclusive</v-icon>
|
|
||||||
All Members
|
|
||||||
</v-chip>
|
|
||||||
<v-chip filter variant="outlined" value="active">
|
|
||||||
<v-icon start size="18" color="success">mdi-check-circle</v-icon>
|
|
||||||
Active
|
|
||||||
</v-chip>
|
|
||||||
<v-chip filter variant="outlined" value="dues-pending">
|
|
||||||
<v-icon start size="18" color="warning">mdi-clock-alert</v-icon>
|
|
||||||
Dues Pending
|
|
||||||
</v-chip>
|
|
||||||
<v-chip filter variant="outlined" value="new">
|
|
||||||
<v-icon start size="18" color="info">mdi-new-box</v-icon>
|
|
||||||
New This Month
|
|
||||||
</v-chip>
|
|
||||||
</v-chip-group>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
variant="outlined"
|
|
||||||
@click="showAdvancedFilters = !showAdvancedFilters"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-filter-variant</v-icon>
|
|
||||||
<v-tooltip activator="parent">Advanced Filters</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
variant="outlined"
|
|
||||||
@click="exportMembers"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-download</v-icon>
|
|
||||||
<v-tooltip activator="parent">Export</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Advanced Filters (Collapsible) -->
|
|
||||||
<v-expand-transition>
|
|
||||||
<v-row v-if="showAdvancedFilters" class="mt-4">
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="statusFilter"
|
|
||||||
label="Status"
|
|
||||||
:items="statusOptions"
|
|
||||||
variant="solo"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="membershipFilter"
|
|
||||||
label="Membership Type"
|
|
||||||
:items="membershipOptions"
|
|
||||||
variant="solo"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="nationalityFilter"
|
|
||||||
label="Nationality"
|
|
||||||
:items="countryOptions"
|
|
||||||
item-title="name"
|
|
||||||
item-value="code"
|
|
||||||
variant="solo"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="duesFilter"
|
|
||||||
label="Dues Status"
|
|
||||||
:items="['Paid', 'Unpaid', 'Overdue']"
|
|
||||||
variant="solo"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-expand-transition>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- View Mode Toggle -->
|
|
||||||
<div class="d-flex justify-space-between align-center mb-4">
|
|
||||||
<div class="text-body-1">
|
|
||||||
Showing <strong>{{ filteredMembers.length }}</strong> of {{ members.length }} members
|
|
||||||
</div>
|
|
||||||
<v-btn-toggle
|
|
||||||
v-model="viewMode"
|
|
||||||
mandatory
|
|
||||||
density="comfortable"
|
|
||||||
rounded="lg"
|
|
||||||
color="primary"
|
|
||||||
class="elevation-2"
|
|
||||||
>
|
|
||||||
<v-btn value="cards" prepend-icon="mdi-view-grid">
|
|
||||||
Cards
|
|
||||||
</v-btn>
|
|
||||||
<v-btn value="table" prepend-icon="mdi-table">
|
|
||||||
Table
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Enhanced Card View -->
|
|
||||||
<transition-group
|
|
||||||
v-if="viewMode === 'cards'"
|
|
||||||
name="card-list"
|
|
||||||
tag="div"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<v-col
|
|
||||||
v-for="member in paginatedMembers"
|
|
||||||
:key="member.member_id"
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
lg="3"
|
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
class="member-card glass-card h-100"
|
|
||||||
elevation="0"
|
|
||||||
@click="viewMember(member)"
|
|
||||||
>
|
|
||||||
<!-- Card Header with Gradient Background -->
|
|
||||||
<div class="card-header gradient-bg pa-4 text-center">
|
|
||||||
<ProfileAvatar
|
|
||||||
:member-id="member.member_id"
|
|
||||||
:first-name="member.first_name"
|
|
||||||
:last-name="member.last_name"
|
|
||||||
size="80"
|
|
||||||
class="mb-3 mx-auto elevation-4 white-border"
|
|
||||||
/>
|
|
||||||
<h3 class="text-h6 font-weight-bold white--text">
|
|
||||||
{{ member.first_name }} {{ member.last_name }}
|
|
||||||
</h3>
|
|
||||||
<div class="text-caption white--text opacity-90">
|
|
||||||
{{ member.member_id || 'Pending ID' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<!-- Contact Info -->
|
|
||||||
<div class="info-row mb-3">
|
|
||||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-email</v-icon>
|
|
||||||
<span class="text-body-2 text-truncate">{{ member.email }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nationality with Flag -->
|
|
||||||
<div class="info-row mb-3">
|
|
||||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-flag</v-icon>
|
|
||||||
<MultipleCountryFlags
|
|
||||||
:nationality="member.nationality"
|
|
||||||
:show-name="true"
|
|
||||||
size="small"
|
|
||||||
fallback-text="Not specified"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Member Since -->
|
|
||||||
<div class="info-row mb-3">
|
|
||||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-calendar</v-icon>
|
|
||||||
<span class="text-body-2">Since {{ formatDate(member.join_date) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status Badges -->
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
||||||
<v-chip
|
|
||||||
:color="member.status === 'active' ? 'success' : 'error'"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
>
|
|
||||||
<v-icon start size="14">
|
|
||||||
{{ member.status === 'active' ? 'mdi-check' : 'mdi-close' }}
|
|
||||||
</v-icon>
|
|
||||||
{{ member.status }}
|
|
||||||
</v-chip>
|
|
||||||
|
|
||||||
<v-chip
|
|
||||||
:color="getDuesChipColor(member)"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
>
|
|
||||||
<v-icon start size="14">mdi-cash</v-icon>
|
|
||||||
{{ member.dues_paid_this_year ? 'Dues Paid' : 'Dues Pending' }}
|
|
||||||
</v-chip>
|
|
||||||
|
|
||||||
<v-chip
|
|
||||||
v-if="member.membership_type !== 'Standard'"
|
|
||||||
color="purple"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
>
|
|
||||||
{{ member.membership_type }}
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
|
||||||
<v-card-actions class="pa-3 pt-0">
|
|
||||||
<v-btn
|
|
||||||
v-if="!member.dues_paid_this_year"
|
|
||||||
color="success"
|
|
||||||
variant="flat"
|
|
||||||
size="small"
|
|
||||||
block
|
|
||||||
rounded
|
|
||||||
@click.stop="markDuesPaid(member)"
|
|
||||||
>
|
|
||||||
<v-icon start>mdi-check</v-icon>
|
|
||||||
Mark Dues Paid
|
|
||||||
</v-btn>
|
|
||||||
<v-row v-else dense>
|
|
||||||
<v-col cols="4">
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-eye"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
block
|
|
||||||
@click.stop="viewMember(member)"
|
|
||||||
>
|
|
||||||
<v-tooltip activator="parent">View</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="4">
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-pencil"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
block
|
|
||||||
@click.stop="editMember(member)"
|
|
||||||
>
|
|
||||||
<v-tooltip activator="parent">Edit</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="4">
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-email"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
block
|
|
||||||
@click.stop="sendEmail(member)"
|
|
||||||
>
|
|
||||||
<v-tooltip activator="parent">Email</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</transition-group>
|
|
||||||
|
|
||||||
<!-- Enhanced Table View -->
|
|
||||||
<v-card v-else-if="viewMode === 'table'" class="glass-card" elevation="0">
|
|
||||||
<v-data-table
|
|
||||||
:headers="tableHeaders"
|
|
||||||
:items="filteredMembers"
|
|
||||||
:search="searchQuery"
|
|
||||||
:loading="loading"
|
|
||||||
class="modern-table"
|
|
||||||
hover
|
|
||||||
:items-per-page="15"
|
|
||||||
@click:row="(e, { item }) => viewMember(item)"
|
|
||||||
>
|
|
||||||
<template v-slot:item.member="{ item }">
|
|
||||||
<div class="d-flex align-center py-3">
|
|
||||||
<ProfileAvatar
|
|
||||||
:member-id="item.member_id"
|
|
||||||
:first-name="item.first_name"
|
|
||||||
:last-name="item.last_name"
|
|
||||||
size="40"
|
|
||||||
class="mr-3"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div class="font-weight-medium">
|
|
||||||
{{ item.first_name }} {{ item.last_name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-medium-emphasis">
|
|
||||||
{{ item.member_id || 'Pending ID' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:item.contact="{ item }">
|
|
||||||
<div class="py-2">
|
|
||||||
<div class="d-flex align-center mb-1">
|
|
||||||
<v-icon size="14" class="mr-1">mdi-email</v-icon>
|
|
||||||
<a :href="`mailto:${item.email}`" class="text-primary text-decoration-none" @click.stop>
|
|
||||||
{{ item.email }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.phone" class="d-flex align-center text-caption">
|
|
||||||
<v-icon size="14" class="mr-1">mdi-phone</v-icon>
|
|
||||||
{{ item.phone }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:item.nationality="{ item }">
|
|
||||||
<MultipleCountryFlags
|
|
||||||
:nationality="item.nationality"
|
|
||||||
:show-name="true"
|
|
||||||
size="small"
|
|
||||||
fallback-text="—"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:item.membership="{ item }">
|
|
||||||
<div class="py-2">
|
|
||||||
<v-chip
|
|
||||||
:color="getMembershipColor(item.membership_type)"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
class="mb-1"
|
|
||||||
>
|
|
||||||
{{ item.membership_type }}
|
|
||||||
</v-chip>
|
|
||||||
<div class="text-caption text-medium-emphasis">
|
|
||||||
Since {{ formatDate(item.join_date) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:item.status="{ item }">
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<v-chip
|
|
||||||
:color="item.status === 'active' ? 'success' : 'error'"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
>
|
|
||||||
{{ item.status }}
|
|
||||||
</v-chip>
|
|
||||||
<v-chip
|
|
||||||
:color="getDuesChipColor(item)"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
>
|
|
||||||
{{ item.dues_paid_this_year ? 'Paid' : 'Due' }}
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:item.actions="{ item }">
|
|
||||||
<div class="d-flex gap-1">
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-eye"
|
|
||||||
size="x-small"
|
|
||||||
variant="text"
|
|
||||||
@click.stop="viewMember(item)"
|
|
||||||
/>
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-pencil"
|
|
||||||
size="x-small"
|
|
||||||
variant="text"
|
|
||||||
@click.stop="editMember(item)"
|
|
||||||
/>
|
|
||||||
<v-menu>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-dots-vertical"
|
|
||||||
size="x-small"
|
|
||||||
variant="text"
|
|
||||||
v-bind="props"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<v-list density="compact">
|
|
||||||
<v-list-item @click="sendEmail(item)">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon size="small">mdi-email</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>Send Email</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item
|
|
||||||
v-if="!item.dues_paid_this_year"
|
|
||||||
@click="markDuesPaid(item)"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon size="small" color="success">mdi-check</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>Mark Dues Paid</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="viewPaymentHistory(item)">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon size="small">mdi-history</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>Payment History</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-divider />
|
|
||||||
<v-list-item
|
|
||||||
@click="toggleStatus(item)"
|
|
||||||
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon size="small">
|
|
||||||
{{ item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check' }}
|
|
||||||
</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>
|
|
||||||
{{ item.status === 'active' ? 'Deactivate' : 'Activate' }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-data-table>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<v-card
|
|
||||||
v-if="viewMode === 'cards' && filteredMembers.length > itemsPerPage"
|
|
||||||
class="mt-6 glass-card"
|
|
||||||
elevation="0"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<v-pagination
|
|
||||||
v-model="currentPage"
|
|
||||||
:length="Math.ceil(filteredMembers.length / itemsPerPage)"
|
|
||||||
:total-visible="7"
|
|
||||||
rounded="circle"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<!-- Dialogs -->
|
|
||||||
<ViewMemberDialog
|
|
||||||
v-model="showViewDialog"
|
|
||||||
:member="selectedMember"
|
|
||||||
@edit="handleEditMember"
|
|
||||||
@mark-dues-paid="handleMarkDuesPaid"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditMemberDialog
|
|
||||||
v-model="showEditDialog"
|
|
||||||
:member="selectedMember"
|
|
||||||
@member-updated="handleMemberUpdated"
|
|
||||||
/>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Member } from '~/utils/types';
|
|
||||||
import { countries } from '~/utils/countries';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'admin',
|
|
||||||
middleware: 'admin'
|
|
||||||
});
|
|
||||||
|
|
||||||
// State
|
|
||||||
const loading = ref(false);
|
|
||||||
const members = ref<Member[]>([]);
|
|
||||||
const searchQuery = ref('');
|
|
||||||
const quickFilter = ref('all');
|
|
||||||
const statusFilter = ref(null);
|
|
||||||
const membershipFilter = ref(null);
|
|
||||||
const nationalityFilter = ref(null);
|
|
||||||
const duesFilter = ref(null);
|
|
||||||
const viewMode = ref('cards');
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const itemsPerPage = 12;
|
|
||||||
const showAdvancedFilters = ref(false);
|
|
||||||
const showViewDialog = ref(false);
|
|
||||||
const showEditDialog = ref(false);
|
|
||||||
const showCreateDialog = ref(false);
|
|
||||||
const selectedMember = ref<Member | null>(null);
|
|
||||||
|
|
||||||
// Stats
|
|
||||||
const stats = ref({
|
|
||||||
total: 0,
|
|
||||||
active: 0,
|
|
||||||
paidThisYear: 0,
|
|
||||||
duesOutstanding: 0,
|
|
||||||
newThisMonth: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Computed stats cards
|
|
||||||
const statsCards = computed(() => [
|
|
||||||
{
|
|
||||||
title: 'Total Members',
|
|
||||||
value: stats.value.total,
|
|
||||||
icon: 'mdi-account-group',
|
|
||||||
color: '#3b82f6',
|
|
||||||
change: '+12',
|
|
||||||
changeType: 'increase'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Active Members',
|
|
||||||
value: stats.value.active,
|
|
||||||
icon: 'mdi-account-check',
|
|
||||||
color: '#10b981',
|
|
||||||
progress: Math.round((stats.value.active / stats.value.total) * 100)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Dues Paid',
|
|
||||||
value: stats.value.paidThisYear,
|
|
||||||
icon: 'mdi-cash-check',
|
|
||||||
color: '#8b5cf6',
|
|
||||||
progress: Math.round((stats.value.paidThisYear / stats.value.total) * 100)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'New This Month',
|
|
||||||
value: stats.value.newThisMonth,
|
|
||||||
icon: 'mdi-account-plus',
|
|
||||||
color: '#f59e0b',
|
|
||||||
change: '+8',
|
|
||||||
changeType: 'increase'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Options
|
|
||||||
const statusOptions = ['active', 'inactive'];
|
|
||||||
const membershipOptions = ['Standard', 'Premium', 'VIP', 'Lifetime'];
|
|
||||||
const countryOptions = countries;
|
|
||||||
|
|
||||||
// Table headers
|
|
||||||
const tableHeaders = [
|
|
||||||
{ title: 'Member', key: 'member', sortable: true },
|
|
||||||
{ title: 'Contact', key: 'contact', sortable: true },
|
|
||||||
{ title: 'Nationality', key: 'nationality', sortable: true },
|
|
||||||
{ title: 'Membership', key: 'membership', sortable: true },
|
|
||||||
{ title: 'Status', key: 'status', sortable: true },
|
|
||||||
{ title: '', key: 'actions', sortable: false, align: 'end' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
const filteredMembers = computed(() => {
|
|
||||||
let filtered = [...members.value];
|
|
||||||
|
|
||||||
// Apply quick filter
|
|
||||||
if (quickFilter.value === 'active') {
|
|
||||||
filtered = filtered.filter(m => m.status === 'active');
|
|
||||||
} else if (quickFilter.value === 'dues-pending') {
|
|
||||||
filtered = filtered.filter(m => !m.dues_paid_this_year);
|
|
||||||
} else if (quickFilter.value === 'new') {
|
|
||||||
const thisMonth = new Date().getMonth();
|
|
||||||
filtered = filtered.filter(m => {
|
|
||||||
const joinDate = new Date(m.join_date);
|
|
||||||
return joinDate.getMonth() === thisMonth;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply advanced filters
|
|
||||||
if (statusFilter.value) {
|
|
||||||
filtered = filtered.filter(m => m.status === statusFilter.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (membershipFilter.value) {
|
|
||||||
filtered = filtered.filter(m => m.membership_type === membershipFilter.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nationalityFilter.value) {
|
|
||||||
filtered = filtered.filter(m => m.nationality === nationalityFilter.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duesFilter.value) {
|
|
||||||
if (duesFilter.value === 'Paid') {
|
|
||||||
filtered = filtered.filter(m => m.dues_paid_this_year);
|
|
||||||
} else if (duesFilter.value === 'Unpaid' || duesFilter.value === 'Overdue') {
|
|
||||||
filtered = filtered.filter(m => !m.dues_paid_this_year);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginatedMembers = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * itemsPerPage;
|
|
||||||
const end = start + itemsPerPage;
|
|
||||||
return filteredMembers.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const formatDate = (date: string) => {
|
|
||||||
if (!date) return 'N/A';
|
|
||||||
const parsedDate = new Date(date);
|
|
||||||
if (isNaN(parsedDate.getTime())) return 'N/A';
|
|
||||||
return parsedDate.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMembershipColor = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'VIP': return 'error';
|
|
||||||
case 'Premium': return 'warning';
|
|
||||||
case 'Lifetime': return 'purple';
|
|
||||||
default: return 'info';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDuesChipColor = (member: Member) => {
|
|
||||||
return member.dues_paid_this_year ? 'success' : 'warning';
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewMember = (member: Member) => {
|
|
||||||
selectedMember.value = member;
|
|
||||||
showViewDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const editMember = (member: Member) => {
|
|
||||||
selectedMember.value = member;
|
|
||||||
showEditDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditMember = (member: Member) => {
|
|
||||||
showViewDialog.value = false;
|
|
||||||
selectedMember.value = member;
|
|
||||||
showEditDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMemberUpdated = (member: Member) => {
|
|
||||||
const index = members.value.findIndex(m => m.member_id === member.member_id);
|
|
||||||
if (index > -1) {
|
|
||||||
members.value[index] = member;
|
|
||||||
}
|
|
||||||
showEditDialog.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const markDuesPaid = async (member: Member) => {
|
|
||||||
try {
|
|
||||||
member.dues_paid_this_year = true;
|
|
||||||
member.dues_status = 'Paid';
|
|
||||||
member.last_dues_paid = new Date().toISOString();
|
|
||||||
|
|
||||||
stats.value.paidThisYear++;
|
|
||||||
stats.value.duesOutstanding--;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error marking dues as paid:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMarkDuesPaid = (member: Member) => {
|
|
||||||
markDuesPaid(member);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEmail = (member: Member) => {
|
|
||||||
window.location.href = `mailto:${member.email}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewPaymentHistory = (member: Member) => {
|
|
||||||
// TODO: Navigate to payment history
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleStatus = (member: Member) => {
|
|
||||||
member.status = member.status === 'active' ? 'inactive' : 'active';
|
|
||||||
// TODO: Make API call
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportMembers = () => {
|
|
||||||
// TODO: Export to CSV/Excel
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load members data
|
|
||||||
const loadMembers = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const response = await $fetch('/api/members');
|
|
||||||
const membersList = response?.data?.list || response?.data?.members || response?.list || [];
|
|
||||||
|
|
||||||
if (membersList && membersList.length > 0) {
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const currentMonth = new Date().getMonth();
|
|
||||||
|
|
||||||
members.value = membersList.map((member: any) => {
|
|
||||||
const lastPaid = member.last_dues_paid ? new Date(member.last_dues_paid) : null;
|
|
||||||
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
|
|
||||||
const joinDate = member.member_since || member.created_at;
|
|
||||||
const joinMonth = joinDate ? new Date(joinDate).getMonth() : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
member_id: member.member_id || '',
|
|
||||||
first_name: member.first_name,
|
|
||||||
last_name: member.last_name,
|
|
||||||
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
|
||||||
email: member.email,
|
|
||||||
nationality: member.nationality || member.country_code || '',
|
|
||||||
membership_type: member.membership_type || 'Standard',
|
|
||||||
status: member.membership_status === 'Active' ? 'active' : 'inactive',
|
|
||||||
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
|
|
||||||
dues_paid_this_year: duesPaidThisYear,
|
|
||||||
last_dues_paid: member.last_dues_paid,
|
|
||||||
join_date: joinDate,
|
|
||||||
phone: member.phone_number || member.phone || ''
|
|
||||||
};
|
|
||||||
}).sort((a, b) => {
|
|
||||||
const aLastName = (a.last_name || '').toLowerCase();
|
|
||||||
const bLastName = (b.last_name || '').toLowerCase();
|
|
||||||
const aFirstName = (a.first_name || '').toLowerCase();
|
|
||||||
const bFirstName = (b.first_name || '').toLowerCase();
|
|
||||||
|
|
||||||
const lastNameCompare = aLastName.localeCompare(bLastName);
|
|
||||||
if (lastNameCompare !== 0) return lastNameCompare;
|
|
||||||
|
|
||||||
return aFirstName.localeCompare(bFirstName);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate stats
|
|
||||||
const currentYearMembers = members.value.filter(m => m.dues_paid_this_year);
|
|
||||||
const newThisMonth = members.value.filter(m => {
|
|
||||||
const joinDate = new Date(m.join_date);
|
|
||||||
return joinDate.getMonth() === currentMonth && joinDate.getFullYear() === currentYear;
|
|
||||||
});
|
|
||||||
|
|
||||||
stats.value = {
|
|
||||||
total: members.value.length,
|
|
||||||
active: members.value.filter(m => m.status === 'active').length,
|
|
||||||
paidThisYear: currentYearMembers.length,
|
|
||||||
duesOutstanding: members.value.length - currentYearMembers.length,
|
|
||||||
newThisMonth: newThisMonth.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading members:', error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load on mount
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadMembers();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Glassmorphism effect */
|
|
||||||
.glass-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
||||||
border-radius: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gradient text */
|
|
||||||
.gradient-text {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gradient avatar */
|
|
||||||
.gradient-avatar {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gradient background for card headers */
|
|
||||||
.gradient-bg {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stat card hover effect */
|
|
||||||
.stat-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Member card effects */
|
|
||||||
.member-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card .card-header {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card .card-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-card:hover .card-header::before {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* White border for avatar */
|
|
||||||
.white-border {
|
|
||||||
border: 3px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info row styling */
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search field styling */
|
|
||||||
.search-field :deep(.v-field) {
|
|
||||||
border-radius: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chip active state */
|
|
||||||
.chip-active {
|
|
||||||
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
|
||||||
border-color: rgb(var(--v-theme-primary)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table styling */
|
|
||||||
.modern-table :deep(tbody tr) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modern-table :deep(tbody tr:hover) {
|
|
||||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation for cards */
|
|
||||||
.card-list-move,
|
|
||||||
.card-list-enter-active,
|
|
||||||
.card-list-leave-active {
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-list-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-list-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pulse animation for add button */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0.4);
|
|
||||||
}
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-primary), 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-animation {
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter card styling */
|
|
||||||
.filter-card {
|
|
||||||
border-left: 4px solid rgb(var(--v-theme-primary));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue