From 30e5d0964352b2cf00d33a66b967d9ade49c3fde Mon Sep 17 00:00:00 2001 From: Darijo Milicevic Date: Wed, 22 Oct 2025 08:56:35 -0300 Subject: [PATCH] feat(goals): add goal definitions, records, and overview with completion sync --- .gitignore | 1 + package-lock.json | 351 +++++++++++++++++++++++++++++++- package.json | 5 +- requiredClasses.json | 110 +++++++++- src/components/Nav.vue | 20 +- src/components/SideMenu.vue | 8 +- src/main.js | 7 +- src/router/index.js | 28 ++- src/stores/globals.js | 19 +- src/utils/date.js | 46 +++++ src/utils/goalRecords.js | 132 ++++++++++++ src/utils/goals.js | 152 ++++++++++++++ src/utils/utils.js | 3 + src/views/AddGoalDefinition.vue | 103 ++++++++++ src/views/Dashboard.vue | 6 + src/views/GoalDefinitions.vue | 152 ++++++++++++++ src/views/GoalRecords.vue | 88 ++++++++ src/views/GoalsOverview.vue | 59 ++++++ 18 files changed, 1276 insertions(+), 14 deletions(-) create mode 100644 src/utils/date.js create mode 100644 src/utils/goalRecords.js create mode 100644 src/utils/goals.js create mode 100644 src/views/AddGoalDefinition.vue create mode 100644 src/views/GoalDefinitions.vue create mode 100644 src/views/GoalRecords.vue create mode 100644 src/views/GoalsOverview.vue diff --git a/.gitignore b/.gitignore index 9f69cbb..88f8acf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist .vscode logs .idea/ +docker-compose.override.yml diff --git a/package-lock.json b/package-lock.json index 6e842fc..7ea73be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.2", + "concurrently": "^9.2.1", + "nodemon": "^3.1.10", "vite": "^5.4.2" } }, @@ -3635,6 +3637,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -3805,6 +3821,19 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "license": "MIT", @@ -4048,6 +4077,44 @@ "version": "0.7.0", "license": "MIT" }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -4098,7 +4165,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "optional": true, + "devOptional": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -4225,8 +4292,48 @@ }, "node_modules/concat-map": { "version": "0.0.1", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } }, "node_modules/config-chain": { "version": "1.1.13", @@ -6517,6 +6624,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/immutable": { "version": "4.1.0", "license": "MIT" @@ -6621,6 +6735,19 @@ "version": "0.3.2", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "license": "MIT", @@ -7599,6 +7726,120 @@ "license": "MIT", "peer": true }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "6.0.0", "license": "ISC", @@ -7612,6 +7853,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "license": "MIT", @@ -8890,6 +9141,13 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "license": "MIT", @@ -9451,6 +9709,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/redis": { "version": "4.6.13", "license": "MIT", @@ -9754,7 +10025,9 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -9951,6 +10224,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shepherd.js": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-13.0.3.tgz", @@ -10045,6 +10331,32 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", @@ -10399,6 +10711,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -10430,6 +10752,16 @@ "node": ">=6" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "license": "MIT", @@ -10569,6 +10901,13 @@ "node": ">= 0.8" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "license": "MIT" @@ -11141,7 +11480,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -11154,7 +11493,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "optional": true, + "devOptional": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -11172,7 +11511,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "optional": true, + "devOptional": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index c2f7974..98ef5c9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "start": "node index.mjs", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "dev": "concurrently \"node index.mjs\" \"vite --host\"" }, "dependencies": { "@stripe/stripe-js": "^5.2.0", @@ -38,6 +39,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.2", + "concurrently": "^9.2.1", + "nodemon": "^3.1.10", "vite": "^5.4.2" }, "main": "index.mjs" diff --git a/requiredClasses.json b/requiredClasses.json index c436f40..72ec483 100644 --- a/requiredClasses.json +++ b/requiredClasses.json @@ -469,6 +469,114 @@ } } }, +{ + "className": "goalRecords", + "fields": { + "objectId": { "type": "String" }, + "createdAt": { "type": "Date" }, + "updatedAt": { "type": "Date" }, + "ACL": { "type": "ACL" }, + "user": { "type": "Pointer", "targetClass": "_User", "required": true }, + "goal": { "type": "Pointer", "targetClass": "goals", "required": true }, + "dateUnix": { "type": "Number", "required": true }, + "done": { "type": "Boolean", "required": false }, + "notes": { "type": "String", "required": false }, + "periodStartUnix": { "type": "Number", "required": true}, + "period": {"type": "String","required": true, "enum": ["Daily", "Weekly", "Monthly"]} + }, + "classLevelPermissions": { + "find": { "*": true }, + "count": { "*": true }, + "get": { "*": true }, + "create": { "*": true }, + "update": { "*": true }, + "delete": { "*": true }, + "addField": { "*": true }, + "protectedFields": { "*": [] } + }, + "indexes": { + "_id_": { "_id": 1 }, + "user_index": { "user": 1 }, + "dateUnix_index": { "dateUnix": 1 }, + "goal_index": { "goal": 1 }, + "user_period_goal": { "user": 1, "periodStartUnix": 1, "goal": 1 } + } +}, + { + "className": "goals", + "fields": { + "objectId": { + "type": "String" + }, + "createdAt": { + "type": "Date" + }, + "updatedAt": { + "type": "Date" + }, + "ACL": { + "type": "ACL" + }, + "title": { + "type": "String", + "required": true + }, + "criteria": { + "type": "String", + "required": false + }, + "period": { + "type": "String", + "required": true, + "default": "Daily", + "enum": ["Daily", "Weekly", "Monthly"] + }, + "successRate": { + "type": "Number", + "required": false + }, + "completed": { + "type": "Boolean", + "required": false + }, + "user": { + "type": "Pointer", + "targetClass": "_User", + "required": true + } + }, + "classLevelPermissions": { + "find": { + "*": true + }, + "count": { + "*": true + }, + "get": { + "*": true + }, + "create": { + "*": true + }, + "update": { + "*": true + }, + "delete": { + "*": true + }, + "addField": { + "*": true + }, + "protectedFields": { + "*": [] + } + }, + "indexes": { + "_id_": { + "_id": 1 + } + } + }, { "className": "scenarios", "fields": { @@ -889,4 +997,4 @@ } } } -] \ No newline at end of file +] diff --git a/src/components/Nav.vue b/src/components/Nav.vue index 1b3ba06..06919b3 100644 --- a/src/components/Nav.vue +++ b/src/components/Nav.vue @@ -44,6 +44,16 @@ const pages = [{ name: "Diary", icon: "uil uil-diary" }, +{ + id: "goalRecords", + name: "Goals", + icon: "uil uil-bullseye" +}, +{ + id: "goalDefinitions", + name: "Goal Definitions", + icon: "uil uil-bullseye" +}, { id: "notes", name: "Notes", @@ -74,6 +84,11 @@ const pages = [{ name: "Add Diary", icon: "uil uil-plus-circle" }, +{ + id: "addGoalDefinition", + name: "Add Goal Definition", + icon: "uil uil-bullseye" +}, { id: "settings", name: "Settings", @@ -209,6 +224,9 @@ const navAdd = async (param) => {
  • Diary Entry
  • +
  • + Goal Definition +
  • Screenshot
  • @@ -268,4 +286,4 @@ const navAdd = async (param) => { - \ No newline at end of file + diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index ff8e99c..3b77eb1 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -21,6 +21,9 @@ import { useToggleMobileMenu } from "../utils/utils"; Daily + + Goals + Calendar @@ -33,6 +36,9 @@ import { useToggleMobileMenu } from "../utils/utils"; Diary + + Goal Definitions + Screenshots @@ -44,4 +50,4 @@ import { useToggleMobileMenu } from "../utils/utils"; - \ No newline at end of file + diff --git a/src/main.js b/src/main.js index 503903d..9da420d 100644 --- a/src/main.js +++ b/src/main.js @@ -9,8 +9,13 @@ import './assets/style-dark.css' const app = createApp(App) +// --- SET APP ID FOR FRONTEND --- +if (!localStorage.getItem('parse_app_id')) { + localStorage.setItem('parse_app_id', import.meta.env.VITE_APP_ID) + console.log('Parse App ID set in localStorage:', import.meta.env.VITE_APP_ID) +} app.use(createPinia()) app.use(router) -app.mount('#app') \ No newline at end of file +app.mount('#app') diff --git a/src/router/index.js b/src/router/index.js index 8914ac3..bbe5eb2 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -82,6 +82,22 @@ const router = createRouter({ component: () => import('../views/Daily.vue') }, + { + path: '/goalDefinitions', + name: 'goalDefinitions', + meta: { + title: "Goal Definitions", + layout: DashboardLayout + }, + component: () => + import('../views/GoalDefinitions.vue') + }, + { + path: '/goalRecords', + name: 'goalRecords', + meta: { title: 'Goals', layout: DashboardLayout }, + component: () => import('../views/GoalRecords.vue') + }, { path: '/diary', name: 'diary', @@ -133,6 +149,16 @@ const router = createRouter({ component: () => import('../views/AddDiary.vue') }, + { + path: '/addGoalDefinition', + name: 'addGoalDefinition', + meta: { + title: "Add Goal Definition", + layout: DashboardLayout + }, + component: () => + import('../views/AddGoalDefinition.vue') + }, { path: '/addPlaybook', name: 'addPlaybook', @@ -222,4 +248,4 @@ router.beforeEach((to, from, next) => { } }) -export default router \ No newline at end of file +export default router diff --git a/src/stores/globals.js b/src/stores/globals.js index b2c3f3f..4913be3 100644 --- a/src/stores/globals.js +++ b/src/stores/globals.js @@ -70,10 +70,25 @@ export const legacy = reactive([]) **************************************/ export const modalDailyTradeOpen = ref(false) +/************************************** +* GOALS +**************************************/ +export const goals = ref([]) +export const goalQueryLimit = ref(10) +export const goalPagination = ref(0) +export const goalIdToEdit = ref(null) +export const selectedGoal = ref(null) +// Global reactive state for Goals +export const goalUpdate = reactive({}) +export const goalButton = ref(false) +export const dailyGoals = ref([]) +export const dailyRecord = ref(null) +export const goalRecords = ref([]) + /************************************** * TRADES **************************************/ -export const selectedRange = ref() +export const selectedRange = ref({ start: 0, end: Date.now() }) export const filteredTrades = reactive([]) export const filteredTradesDaily = reactive([]) export const filteredTradesTrades = reactive([]) @@ -3380,4 +3395,4 @@ export const selectedPeriodRange = typeof localStorage !== 'undefined' ? ref(JSO export const selectedDashTab = typeof localStorage !== 'undefined' ? ref(localStorage.getItem('selectedDashTab')) : "" export const amountCase = typeof localStorage !== 'undefined' ? ref(localStorage.getItem('selectedGrossNet')) : "" -export const amountCapital = ref() \ No newline at end of file +export const amountCapital = ref() diff --git a/src/utils/date.js b/src/utils/date.js new file mode 100644 index 0000000..f8b21c1 --- /dev/null +++ b/src/utils/date.js @@ -0,0 +1,46 @@ +// src/utils/date.js + +// Get today's date in Unix timestamp (start of the day) +export function getTodayUnix() { + const today = new Date() + today.setHours(0, 0, 0, 0) + return Math.floor(today.getTime() / 1000) +} + +// Return start of the current week (Monday) in Unix seconds +export function getWeekStartUnix() { + const today = new Date() + const day = today.getDay() || 7 // Sunday=0, we treat as 7 + today.setDate(today.getDate() - day + 1) // Go back to Monday + today.setHours(0, 0, 0, 0) + return Math.floor(today.getTime() / 1000) +} + +// Return start of the current month in Unix seconds +export function getMonthStartUnix() { + const today = new Date() + today.setDate(1) // first day of month + today.setHours(0, 0, 0, 0) + return Math.floor(today.getTime() / 1000) +} + + +// Optionally, a few other helpers for future use +export function formatDate(unixTimestamp) { + const date = new Date(unixTimestamp * 1000) + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) +} + +export function getWeekRangeUnix() { + const today = new Date() + const dayOfWeek = today.getDay() // 0 (Sun) → 6 (Sat) + const start = new Date(today) + start.setDate(today.getDate() - dayOfWeek) + start.setHours(0, 0, 0, 0) + const end = new Date(start) + end.setDate(start.getDate() + 7) + return { + startUnix: Math.floor(start.getTime() / 1000), + endUnix: Math.floor(end.getTime() / 1000) + } +} diff --git a/src/utils/goalRecords.js b/src/utils/goalRecords.js new file mode 100644 index 0000000..4075155 --- /dev/null +++ b/src/utils/goalRecords.js @@ -0,0 +1,132 @@ +import Parse from 'parse/dist/parse.min.js' +import { ref } from 'vue' +import { goalRecords } from '../stores/globals' +import { getTodayUnix, getWeekStartUnix, getMonthStartUnix } from './date.js' + +export const spinnerLoadingPage = ref(false) +export const completionPct = ref(0) + +// 🧮 Recalculate local completion % +export function recalculateCompletion() { + const total = goalRecords.value.length + const completed = goalRecords.value.filter(g => g.completed).length + completionPct.value = total ? Math.round((completed / total) * 100) : 0 +} + +// 📥 Fetch goal records by period +export async function useGetGoalsByPeriod(period = 'daily') { + spinnerLoadingPage.value = true + goalRecords.value = [] + + const periodLower = period.toLowerCase() + const user = Parse.User.current() + const GoalRecord = Parse.Object.extend('goalRecords') + const query = new Parse.Query(GoalRecord) + query.equalTo('user', user) + query.equalTo('period', periodLower) + + let periodStart + if (periodLower === 'daily') periodStart = getTodayUnix() + else if (periodLower === 'weekly') periodStart = getWeekStartUnix() + else periodStart = getMonthStartUnix() + query.equalTo('periodStart', periodStart) + + const results = await query.find() + + if (results.length === 0) { + // 🔍 Create new ones based on goal definitions + const GoalDef = Parse.Object.extend('goals') + const goalQuery = new Parse.Query(GoalDef) + goalQuery.equalTo('user', user) + goalQuery.equalTo('period', periodLower) + const defs = await goalQuery.find() + + if (defs.length === 0) { + console.warn(`⚠️ No goal definitions found for period "${periodLower}"`) + } + + for (const def of defs) { + const title = def.get('title') || '(Untitled Goal)' + const criteria = def.get('criteria') || def.get('description') || '(No criteria set)' + + const rec = new GoalRecord() + rec.set('user', user) + rec.set('goal', def) + rec.set('title', title) + rec.set('criteria', criteria) + rec.set('period', periodLower) + rec.set('periodStart', periodStart) + rec.set('completed', false) + await rec.save() + } + + // Reload to populate the store with the newly created records + return await useGetGoalsByPeriod(period) + } + + // ✅ Map records with all needed info + goalRecords.value = results.map(r => ({ + id: r.id, + goalId: r.get('goal')?.id, + title: r.get('title') || '(Untitled)', + criteria: r.get('criteria') || '(No criteria)', + notes: r.get('notes') || '', + completed: r.get('completed') || false, + period: r.get('period') + })) + + recalculateCompletion() + spinnerLoadingPage.value = false +} + +// 💾 Save all records & update success rates +export async function saveGoalRecords() { + spinnerLoadingPage.value = true + const user = Parse.User.current() + const GoalRecord = Parse.Object.extend('goalRecords') + + for (const g of goalRecords.value) { + const rec = new GoalRecord() + if (g.id) rec.id = g.id + rec.set('user', user) + rec.set('completed', g.completed) + rec.set('notes', g.notes || '') + await rec.save() + } + + // ✅ Recalculate goal success rates after save + const goalIds = goalRecords.value.map(g => g.goalId) + await updateGoalSuccessRates(goalIds) + + spinnerLoadingPage.value = false +} + +// 📊 Update successRate in goal definitions +export async function updateGoalSuccessRates(goalIds = []) { + if (!goalIds || goalIds.length === 0) return + + const Goal = Parse.Object.extend('goals') + const GoalRecord = Parse.Object.extend('goalRecords') + + for (const goalId of goalIds) { + const goalPointer = { + __type: 'Pointer', + className: 'goals', + objectId: goalId + } + + const query = new Parse.Query(GoalRecord) + query.equalTo('goal', goalPointer) + const allRecords = await query.find() + + if (allRecords.length === 0) continue + + const completed = allRecords.filter(r => r.get('completed')).length + const successRate = Math.round((completed / allRecords.length) * 100) + + const goalQuery = new Parse.Query(Goal) + const goalObj = await goalQuery.get(goalId) + goalObj.set('successRate', successRate) + await goalObj.save() + } +} diff --git a/src/utils/goals.js b/src/utils/goals.js new file mode 100644 index 0000000..e59d876 --- /dev/null +++ b/src/utils/goals.js @@ -0,0 +1,152 @@ +import { + goals, + selectedGoal, + selectedRange, + endOfList, + spinnerLoadingPage, + spinnerLoadMore, + goalIdToEdit, + goalUpdate, + goalQueryLimit, + goalPagination, + pageId, + queryLimit, + renderData +} from "../stores/globals.js" + +import { usePageRedirect } from "./utils.js" +import Parse from "parse/dist/parse.min.js" + +/** + * Fetch goals from Parse server for the current user + * @param {Boolean} loadMore - whether to append to existing goals or start fresh + */ +export async function useGetGoals(loadMore = false) { + return new Promise(async (resolve, reject) => { + console.log(" -> Getting Goals"); + + if (!loadMore) { + goals.value.length = 0; // reset array if not loading more + endOfList.value = false; + } + + const Goal = Parse.Object.extend("goals"); + const query = new Parse.Query(Goal); + + // Fetch only current user's goals + query.equalTo("user", Parse.User.current()); + query.limit(queryLimit.value); + query.descending("createdAt"); + + // If loading more, skip already loaded + if (loadMore) { + query.skip(goals.value.length); + } + + try { + spinnerLoadingPage.value = !loadMore; + spinnerLoadMore.value = loadMore; + + const results = await query.find(); + + if (results.length === 0) { + endOfList.value = true; + } else { + for (let i = 0; i < results.length; i++) { + const object = results[i]; + const temp = { + objectId: object.id, + title: object.get("title"), + criteria: object.get("criteria"), + period: object.get("period").toLowerCase(), + completed: object.get("completed") ?? false, + successRate: object.get("successRate") ?? 0, + }; + goals.value.push(temp); + } + } + + spinnerLoadMore.value = false; + spinnerLoadingPage.value = false; + resolve(goals.value); + } catch (error) { + console.error("Error fetching goals:", error); + spinnerLoadMore.value = false; + spinnerLoadingPage.value = false; + reject(error); + } + }); +} + + +export async function useUploadGoal(param) { + return new Promise(async (resolve, reject) => { + const parseObject = Parse.Object.extend("goals"); + + if (goalIdToEdit.value) { + console.log(" -> Updating goal") + const query = new Parse.Query(parseObject); + query.equalTo("objectId", goalIdToEdit.value); + const results = await query.first(); + if (results) { + results.set("title", goalUpdate.title) + results.set("criteria", goalUpdate.criteria) + results.set("period", goalUpdate.period.toLowerCase()) + results.set("successRate", goalUpdate.successRate) + results.set("completed", goalUpdate.completed) + await results.save() + if (param != "autoSave") usePageRedirect() + } else { + alert("Update query did not return any results") + } + } else { + console.log(" -> Saving new goal") + const object = new parseObject() + object.set("user", Parse.User.current()) + object.set("title", goalUpdate.title) + object.set("criteria", goalUpdate.criteria) + object.set("period", goalUpdate.period.toLowerCase()) + object.set("successRate", goalUpdate.successRate) + object.set("completed", goalUpdate.completed) + object.setACL(new Parse.ACL(Parse.User.current())) + object.save() + .then((object) => { + console.log(" --> Added new goal with id " + object.id) + if (param != "autoSave") usePageRedirect() + else goalIdToEdit.value = object.id + }, (error) => { + console.log("Failed to create new goal, error: " + error.message) + }) + } + + resolve() + }) +} + +export async function useDeleteGoal() { + console.log(" -> Deleting goal entry") + const Goal = Parse.Object.extend("goals") + const query = new Parse.Query(Goal) + query.equalTo("objectId", selectedGoal.value) + const res = await query.first() + if (res) { + await res.destroy() + await refreshGoals() + } else { + alert("There was a problem with the query") + } +} + +async function refreshGoals() { + console.log(" -> Refreshing goal entries") + return new Promise(async resolve => { + goalQueryLimit.value = 10 + goalPagination.value = 0 + goals.length = 0 + await useGetGoals(true) + await (renderData.value += 1) + selectedGoal.value = null + resolve() + }) +} + diff --git a/src/utils/utils.js b/src/utils/utils.js index 4f833a8..d597bb8 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1044,6 +1044,9 @@ export function usePageRedirect(param) { if (pageId.value == "daily") { window.location.href = "/daily" } + if (pageId.value == "goals") { + window.location.href = "/goals" + } if (pageId.value == "addDiary") { window.location.href = "/diary" } diff --git a/src/views/AddGoalDefinition.vue b/src/views/AddGoalDefinition.vue new file mode 100644 index 0000000..fa1c7e6 --- /dev/null +++ b/src/views/AddGoalDefinition.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index d47ce6f..354ee57 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -5,6 +5,7 @@ import Filters from '../components/Filters.vue' import { selectedDashTab, spinnerLoadingPage, dashboardIdMounted, totals, amountCase, amountCapital, profitAnalysis, renderData, selectedRatio, dashboardChartsMounted, hasData, satisfactionArray, availableTags, groups, barChartNegativeTagGroups } from '../stores/globals'; import { useThousandCurrencyFormat, useTwoDecCurrencyFormat, useXDecCurrencyFormat, useMountDashboard, useThousandFormat, useXDecFormat } from '../utils/utils'; import NoData from '../components/NoData.vue'; +import GoalsOverview from './GoalsOverview.vue'; const dashTabs = [{ id: "overviewTab", @@ -323,6 +324,11 @@ onBeforeMount(async () => { + +
    + +
    +
    diff --git a/src/views/GoalDefinitions.vue b/src/views/GoalDefinitions.vue new file mode 100644 index 0000000..84d29c7 --- /dev/null +++ b/src/views/GoalDefinitions.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/views/GoalRecords.vue b/src/views/GoalRecords.vue new file mode 100644 index 0000000..fee2171 --- /dev/null +++ b/src/views/GoalRecords.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/views/GoalsOverview.vue b/src/views/GoalsOverview.vue new file mode 100644 index 0000000..145f3c4 --- /dev/null +++ b/src/views/GoalsOverview.vue @@ -0,0 +1,59 @@ + + +