mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42:15 +01:00
upgrade inertia v2; add prefetching; migrate queries to tanstack query
vue
This commit is contained in:
@@ -107,7 +107,7 @@ services:
|
|||||||
- sail
|
- sail
|
||||||
- reverse-proxy
|
- reverse-proxy
|
||||||
playwright:
|
playwright:
|
||||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
image: mcr.microsoft.com/playwright:v1.51.1-jammy
|
||||||
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
|
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
|
||||||
working_dir: /src
|
working_dir: /src
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
import { formatCentsWithOrganizationDefaults } from './utils/money';
|
import { formatCentsWithOrganizationDefaults } from './utils/money';
|
||||||
import type { CurrencyFormat } from '../resources/js/packages/ui/src/utils/money';
|
|
||||||
import { NumberFormat } from '@/packages/ui/src/utils/number';
|
|
||||||
|
|
||||||
async function goToProjectsOverview(page: Page) {
|
async function goToProjectsOverview(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
import { formatCentsWithOrganizationDefaults } from './utils/money';
|
import { formatCentsWithOrganizationDefaults } from './utils/money';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
import { expect, Locator, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Locator, Page } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
assertThatTimerHasStarted,
|
assertThatTimerHasStarted,
|
||||||
assertThatTimerIsStopped,
|
assertThatTimerIsStopped,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
startOrStopTimerWithButton,
|
startOrStopTimerWithButton,
|
||||||
stoppedTimeEntryResponse,
|
stoppedTimeEntryResponse,
|
||||||
} from './utils/currentTimeEntry';
|
} from './utils/currentTimeEntry';
|
||||||
import { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { newTagResponse } from './utils/tags';
|
import { newTagResponse } from './utils/tags';
|
||||||
|
|
||||||
async function goToDashboard(page: Page) {
|
async function goToDashboard(page: Page) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
export async function startOrStopTimerWithButton(page: Page) {
|
export async function startOrStopTimerWithButton(page: Page) {
|
||||||
await page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]').click();
|
await page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]').click();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { formatCents } from '../../resources/js/packages/ui/src/utils/money';
|
import { formatCents } from '../../resources/js/packages/ui/src/utils/money';
|
||||||
import type { CurrencyFormat } from '../../resources/js/packages/ui/src/utils/money';
|
import type { CurrencyFormat } from '../../resources/js/packages/ui/src/utils/money';
|
||||||
import { NumberFormat } from '../../resources/js/packages/ui/src/utils/number';
|
import type { NumberFormat } from '../../resources/js/packages/ui/src/utils/number';
|
||||||
|
|
||||||
export function formatCentsWithOrganizationDefaults(
|
export function formatCentsWithOrganizationDefaults(
|
||||||
cents: number,
|
cents: number,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
export function newTagResponse(page: Page, { name = '' } = {}) {
|
export function newTagResponse(page: Page, { name = '' } = {}) {
|
||||||
return page.waitForResponse(async (response) => {
|
return page.waitForResponse(async (response) => {
|
||||||
|
|||||||
118
package-lock.json
generated
118
package-lock.json
generated
@@ -41,7 +41,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@inertiajs/vue3": "^1.0.0",
|
"@inertiajs/vue3": "^2.0.0",
|
||||||
"@playwright/test": "^1.41.1",
|
"@playwright/test": "^1.41.1",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
@@ -1159,28 +1159,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inertiajs/core": {
|
"node_modules/@inertiajs/core": {
|
||||||
"version": "1.3.0",
|
"version": "2.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.7.tgz",
|
||||||
"integrity": "sha512-TJ8R1eUYY473m9DaKlCPRdHTdznFWTDuy5VvEzXg3t/hohbDQedLj46yn/uAqziJPEUZJrSftZzPI2NMzL9tQA==",
|
"integrity": "sha512-gBu2m71KoAu3kyaBDeszYiInkPqR0lIe+mRpI1JQyOlX1CT+Hwn+Tk5O7Dsr3dRMphEkRj6H9oVmJ19jHSNTIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"deepmerge": "^4.0.0",
|
"axios": "^1.13.2",
|
||||||
"nprogress": "^0.2.0",
|
"laravel-precognition": "^1.0.0",
|
||||||
"qs": "^6.9.0"
|
"lodash-es": "^4.17.21",
|
||||||
|
"qs": "^6.14.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inertiajs/vue3": {
|
"node_modules/@inertiajs/vue3": {
|
||||||
"version": "1.3.0",
|
"version": "2.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.3.7.tgz",
|
||||||
"integrity": "sha512-GizqdCM3u4JWunit3uUbW4fEmTLKQTi1W7VvPRdrNy8XDt4Qy2cCmfFjq+aH5tHBSS3fI/ngYuhN7XvwqNaKvw==",
|
"integrity": "sha512-jaJtOPjulmLy4uKHL8BlYRjXGKk5FMccjczCePJzKS1JJYaF0aixpY046HzlKDtbnU9xn2h2tZRwu4kQ1uoTTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/core": "1.3.0",
|
"@inertiajs/core": "2.3.7",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lodash.isequal": "^4.5.0"
|
"laravel-precognition": "^1.0.0",
|
||||||
|
"lodash-es": "^4.17.21"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
@@ -1927,6 +1929,23 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash-es": {
|
||||||
|
"version": "4.17.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.14.1",
|
"version": "22.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||||
@@ -2710,14 +2729,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.8.4",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3089,16 +3108,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge": {
|
|
||||||
"version": "4.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||||
@@ -3766,15 +3775,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4277,6 +4287,17 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/laravel-precognition": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvXPT7dayCQAidxnsY0hab9Q+Y2rsh7xRpH9uiFtXN8Dekc3tIZt+NrxrOZ9N5SwHBmRBze/Bv+ElfXac0kD6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"lodash-es": "^4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/laravel-vite-plugin": {
|
"node_modules/laravel-vite-plugin": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
|
||||||
@@ -4351,6 +4372,13 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||||
|
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.castarray": {
|
"node_modules/lodash.castarray": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
@@ -4358,21 +4386,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.clonedeep": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isequal": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
|
||||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
@@ -4604,13 +4617,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nprogress": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nth-check": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
@@ -5274,9 +5280,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@inertiajs/vue3": "^1.0.0",
|
"@inertiajs/vue3": "^2.0.0",
|
||||||
"@playwright/test": "^1.41.1",
|
"@playwright/test": "^1.41.1",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import type { Client } from '@/packages/api/src';
|
import type { Client } from '@/packages/api/src';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
|
|
||||||
const clientsStore = useClientsStore();
|
const { clients } = useClientsQuery();
|
||||||
const { clients } = storeToRefs(clientsStore);
|
|
||||||
|
|
||||||
function getKeyFromItem(item: Client) {
|
function getKeyFromItem(item: Client) {
|
||||||
return item.id;
|
return item.id;
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
|
||||||
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
|
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
import type { ProjectMember } from '@/packages/api/src';
|
import type { ProjectMember } from '@/packages/api/src';
|
||||||
import { Badge, SelectDropdown } from '@/packages/ui/src';
|
import { Badge, SelectDropdown } from '@/packages/ui/src';
|
||||||
import type { Member } from '@/packages/api/src';
|
import type { Member } from '@/packages/api/src';
|
||||||
|
|
||||||
const membersStore = useMembersStore();
|
const { members } = useMembersQuery();
|
||||||
const { members } = storeToRefs(membersStore);
|
|
||||||
|
|
||||||
const model = defineModel<string>({
|
const model = defineModel<string>({
|
||||||
default: '',
|
default: '',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { Member } from '@/packages/api/src';
|
import type { Member } from '@/packages/api/src';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { useForm } from '@tanstack/vue-form';
|
import { useForm } from '@tanstack/vue-form';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||||
import Modal from '@/packages/ui/src/Modal.vue';
|
import Modal from '@/packages/ui/src/Modal.vue';
|
||||||
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
|
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
@@ -11,7 +11,6 @@ import { useNotificationsStore } from '@/utils/notification';
|
|||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||||
import InputError from '@/packages/ui/src/Input/InputError.vue';
|
import InputError from '@/packages/ui/src/Input/InputError.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -23,6 +22,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -43,7 +43,7 @@ const deleteMutation = useMutation({
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
close();
|
close();
|
||||||
useMembersStore().fetchMembers();
|
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { api, type Member } from '@/packages/api/src';
|
import { api, type Member } from '@/packages/api/src';
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
|
||||||
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -41,7 +41,7 @@ async function submit() {
|
|||||||
'There was an error deactivating the user.',
|
'There was an error deactivating the user.',
|
||||||
() => {
|
() => {
|
||||||
show.value = false;
|
show.value = false;
|
||||||
useMembersStore().fetchMembers();
|
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import type { Member } from '@/packages/api/src';
|
import type { Member } from '@/packages/api/src';
|
||||||
|
|
||||||
const membersStore = useMembersStore();
|
const { members } = useMembersQuery();
|
||||||
const { members } = storeToRefs(membersStore);
|
|
||||||
|
|
||||||
function getKeyFromItem(item: Member) {
|
function getKeyFromItem(item: Member) {
|
||||||
return item.id;
|
return item.id;
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import MemberTableHeading from '@/Components/Common/Member/MemberTableHeading.vue';
|
import MemberTableHeading from '@/Components/Common/Member/MemberTableHeading.vue';
|
||||||
import MemberTableRow from '@/Components/Common/Member/MemberTableRow.vue';
|
import MemberTableRow from '@/Components/Common/Member/MemberTableRow.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
|
|
||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = useMembersQuery();
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await useMembersStore().fetchMembers();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ import MemberMakePlaceholderModal from '@/Components/Common/Member/MemberMakePla
|
|||||||
import MemberDeleteModal from '@/Components/Common/Member/MemberDeleteModal.vue';
|
import MemberDeleteModal from '@/Components/Common/Member/MemberDeleteModal.vue';
|
||||||
import { capitalizeFirstLetter } from '../../../utils/format';
|
import { capitalizeFirstLetter } from '../../../utils/format';
|
||||||
import { formatCents } from '../../../packages/ui/src/utils/money';
|
import { formatCents } from '../../../packages/ui/src/utils/money';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
member: Member;
|
member: Member;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const organization = inject<ComputedRef<Organization>>('organization');
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
const memberStore = useMembersStore();
|
|
||||||
|
|
||||||
const showEditMemberModal = ref(false);
|
const showEditMemberModal = ref(false);
|
||||||
const showMergeMemberModal = ref(false);
|
const showMergeMemberModal = ref(false);
|
||||||
@@ -31,7 +29,6 @@ const showDeleteMemberModal = ref(false);
|
|||||||
|
|
||||||
function removeMember() {
|
function removeMember() {
|
||||||
showDeleteMemberModal.value = true;
|
showDeleteMemberModal.value = true;
|
||||||
memberStore.fetchMembers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invitePlaceholder(id: string) {
|
async function invitePlaceholder(id: string) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<h3
|
<h3
|
||||||
class="text-text-primary font-semibold text-sm sm:text-base flex items-center space-x-2 sm:space-x-2.5">
|
class="text-text-primary font-semibold text-sm sm:text-base flex items-center space-x-2 sm:space-x-2.5">
|
||||||
<component :is="icon" class="w-5 sm:w-6 text-icon-default"></component>
|
<component :is="icon" class="w-5 text-icon-default"></component>
|
||||||
<span> {{ title }} </span>
|
<span> {{ title }} </span>
|
||||||
</h3>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { computed, ref } from 'vue';
|
|||||||
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
|
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
import ClientDropdown from '@/packages/ui/src/Client/ClientDropdown.vue';
|
import ClientDropdown from '@/packages/ui/src/Client/ClientDropdown.vue';
|
||||||
import Badge from '@/packages/ui/src/Badge.vue';
|
import Badge from '@/packages/ui/src/Badge.vue';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
|
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
|
||||||
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
|
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
|
||||||
@@ -21,7 +21,7 @@ import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBil
|
|||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
|
|
||||||
const { updateProject } = useProjectsStore();
|
const { updateProject } = useProjectsStore();
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
const { clients } = useClientsQuery();
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const showBillableRateModal = ref(false);
|
const showBillableRateModal = ref(false);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { canCreateProjects } from '@/utils/permissions';
|
|||||||
import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src';
|
import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import { storeToRefs } from 'pinia';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +34,7 @@ const emit = defineEmits<{
|
|||||||
sort: [column: SortColumn];
|
sort: [column: SortColumn];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
const { clients } = useClientsQuery();
|
||||||
|
|
||||||
// Create a map of client names for sorting
|
// Create a map of client names for sorting
|
||||||
const clientNameMap = computed(() => {
|
const clientNameMap = computed(() => {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import ProjectMoreOptionsDropdown from '@/Components/Common/Project/ProjectMoreO
|
|||||||
import type { Project } from '@/packages/api/src';
|
import type { Project } from '@/packages/api/src';
|
||||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
|
import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
|
||||||
@@ -17,8 +16,8 @@ import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time
|
|||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import type { Organization } from '@/packages/api/src';
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = storeToRefs(useTasksStore());
|
const { tasks } = useTasksQuery();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
project: Project;
|
project: Project;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
|
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
|
||||||
import type { ProjectMember } from '@/packages/api/src';
|
import type { ProjectMember } from '@/packages/api/src';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -19,7 +18,7 @@ const props = defineProps<{
|
|||||||
projectMember: ProjectMember;
|
projectMember: ProjectMember;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = useMembersQuery();
|
||||||
|
|
||||||
const currentMember = computed(() => {
|
const currentMember = computed(() => {
|
||||||
return members.value.find((member) => member.id === props.projectMember.user_id);
|
return members.value.find((member) => member.id === props.projectMember.user_id);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ProjectMember } from '@/packages/api/src';
|
import type { ProjectMember } from '@/packages/api/src';
|
||||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
||||||
import ProjectMemberMoreOptionsDropdown from '@/Components/Common/ProjectMember/ProjectMemberMoreOptionsDropdown.vue';
|
import ProjectMemberMoreOptionsDropdown from '@/Components/Common/ProjectMember/ProjectMemberMoreOptionsDropdown.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
@@ -29,7 +28,7 @@ function editProjectMember() {
|
|||||||
showEditModal.value = true;
|
showEditModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = useMembersQuery();
|
||||||
const member = computed(() => {
|
const member = computed(() => {
|
||||||
return members.value.find((member) => member.id === props.projectMember.member_id);
|
return members.value.find((member) => member.id === props.projectMember.member_id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,12 +39,13 @@ import {
|
|||||||
type Organization,
|
type Organization,
|
||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
|
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
|
||||||
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
import { useSessionStorage, useStorage } from '@vueuse/core';
|
import { useSessionStorage, useStorage } from '@vueuse/core';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import type { ExportFormat } from '@/types/reporting';
|
import type { ExportFormat } from '@/types/reporting';
|
||||||
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
|
|
||||||
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
|
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
|
||||||
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
|
||||||
@@ -154,7 +155,7 @@ onMounted(() => {
|
|||||||
updateTableReporting();
|
updateTableReporting();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
const { tags } = useTagsQuery();
|
||||||
|
|
||||||
async function createTag(tag: string) {
|
async function createTag(tag: string) {
|
||||||
return await useTagsStore().createTag(tag);
|
return await useTagsStore().createTag(tag);
|
||||||
@@ -199,8 +200,7 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
|
|
||||||
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
|
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
|
||||||
|
|
||||||
const projectsStore = useProjectsStore();
|
const { projects } = useProjectsQuery();
|
||||||
const { projects } = storeToRefs(projectsStore);
|
|
||||||
const showExportModal = ref(false);
|
const showExportModal = ref(false);
|
||||||
const exportUrl = ref<string | null>(null);
|
const exportUrl = ref<string | null>(null);
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ const tableData = computed(() => {
|
|||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<div class="sm:grid grid-cols-4 pt-6 items-start">
|
<div class="sm:grid grid-cols-4 pt-6 items-start">
|
||||||
<div class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
|
<div class="col-span-3 bg-secondary rounded-lg border border-card-border pt-3">
|
||||||
<div
|
<div
|
||||||
class="text-sm flex text-text-primary items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
|
class="text-sm flex text-text-primary items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
|
||||||
<span>Group by</span>
|
<span>Group by</span>
|
||||||
@@ -391,7 +391,7 @@ const tableData = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
|
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
|
||||||
<div
|
<div
|
||||||
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-tertiary [&>*]:pb-1.5 [&>*]:pt-1 text-text-secondary text-sm">
|
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-secondary [&>*]:pb-1.5 [&>*]:pt-1 text-text-tertiary text-sm">
|
||||||
<div class="pl-6">Name</div>
|
<div class="pl-6">Name</div>
|
||||||
<div class="text-right">Duration</div>
|
<div class="text-right">Duration</div>
|
||||||
<div class="text-right pr-6">Cost</div>
|
<div class="text-right pr-6">Cost</div>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
|||||||
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
|
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
|
||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
|
||||||
import TagTableRow from '@/Components/Common/Tag/TagTableRow.vue';
|
import TagTableRow from '@/Components/Common/Tag/TagTableRow.vue';
|
||||||
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
|
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
|
||||||
import TagTableHeading from '@/Components/Common/Tag/TagTableHeading.vue';
|
import TagTableHeading from '@/Components/Common/Tag/TagTableHeading.vue';
|
||||||
@@ -13,7 +12,7 @@ import type { Tag } from '@/packages/api/src';
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
createTag: (name: string) => Promise<Tag | undefined>;
|
createTag: (name: string) => Promise<Tag | undefined>;
|
||||||
}>();
|
}>();
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
const { tags } = useTagsQuery();
|
||||||
const showCreateTagModal = ref(false);
|
const showCreateTagModal = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import type { Task } from '@/packages/api/src';
|
import type { Task } from '@/packages/api/src';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
|
|
||||||
const tasksStore = useTasksStore();
|
const { tasks } = useTasksQuery();
|
||||||
const { tasks } = storeToRefs(tasksStore);
|
|
||||||
|
|
||||||
function getKeyFromItem(item: Task) {
|
function getKeyFromItem(item: Task) {
|
||||||
return item.id;
|
return item.id;
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProjectBadge from '@/packages/ui/src/Project/ProjectBadge.vue';
|
import ProjectBadge from '@/packages/ui/src/Project/ProjectBadge.vue';
|
||||||
import TimeTrackerStartStop from '@/packages/ui/src/TimeTrackerStartStop.vue';
|
import TimeTrackerStartStop from '@/packages/ui/src/TimeTrackerStartStop.vue';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { getDayJsInstance } from '@/packages/ui/src/utils/time';
|
import { getDayJsInstance } from '@/packages/ui/src/utils/time';
|
||||||
import type { TimeEntry } from '@/packages/api/src';
|
import type { TimeEntry } from '@/packages/api/src';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import { ChevronRightIcon } from '@heroicons/vue/16/solid';
|
import { ChevronRightIcon } from '@heroicons/vue/16/solid';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
timeEntry: TimeEntry;
|
timeEntry: TimeEntry;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { projects } = storeToRefs(useProjectsStore());
|
const { projects } = useProjectsQuery();
|
||||||
|
|
||||||
const project = computed(() => {
|
const project = computed(() => {
|
||||||
return projects.value.find((project) => project.id === props.timeEntry.project_id);
|
return projects.value.find((project) => project.id === props.timeEntry.project_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tasks } = storeToRefs(useTasksStore());
|
const { tasks } = useTasksQuery();
|
||||||
|
|
||||||
const task = computed(() => {
|
const task = computed(() => {
|
||||||
return tasks.value.find((task) => task.id === props.timeEntry.task_id);
|
return tasks.value.find((task) => task.id === props.timeEntry.task_id);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ defineProps<{
|
|||||||
<Link
|
<Link
|
||||||
v-else
|
v-else
|
||||||
:href="href ?? ''"
|
:href="href ?? ''"
|
||||||
|
prefetch
|
||||||
class="block px-4 py-2 text-sm leading-5 text-text-primary hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
class="block px-4 py-2 text-sm leading-5 text-text-primary hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||||
<slot />
|
<slot />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const classes = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Link :href="href ?? ''" :class="classes">
|
<Link :href="href ?? ''" :class="classes" prefetch>
|
||||||
<slot />
|
<slot />
|
||||||
</Link>
|
</Link>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Link :href="href" class="block group">
|
<Link :href="href" class="block group" prefetch>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
current
|
current
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const classes = computed(() => {
|
|||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link v-else :href="href ?? ''" :class="classes">
|
<Link v-else :href="href ?? ''" :class="classes" prefetch>
|
||||||
<slot />
|
<slot />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { switchOrganization } from '@/utils/useOrganization';
|
import { switchOrganization } from '@/utils/useOrganization';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
|
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
|
||||||
import type {
|
import type {
|
||||||
CreateClientBody,
|
CreateClientBody,
|
||||||
@@ -49,12 +52,9 @@ const currentTimeEntryStore = useCurrentTimeEntryStore();
|
|||||||
const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore);
|
const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore);
|
||||||
const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore;
|
const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore;
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const { projects } = useProjectsQuery();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { tasks } = useTasksQuery();
|
||||||
const taskStore = useTasksStore();
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = storeToRefs(taskStore);
|
|
||||||
const clientStore = useClientsStore();
|
|
||||||
const { clients } = storeToRefs(clientStore);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
change: [];
|
change: [];
|
||||||
@@ -157,7 +157,7 @@ async function discardCurrentTimeEntry() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
const { tags } = useTagsQuery();
|
||||||
const { timeEntries } = storeToRefs(useTimeEntriesStore());
|
const { timeEntries } = storeToRefs(useTimeEntriesStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import UserSettingsIcon from '@/Components/UserSettingsIcon.vue';
|
|||||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
import { computed, onMounted, provide, ref } from 'vue';
|
import { computed, onMounted, provide, ref } from 'vue';
|
||||||
import NotificationContainer from '@/Components/NotificationContainer.vue';
|
import NotificationContainer from '@/Components/NotificationContainer.vue';
|
||||||
import { initializeStores, refreshStores } from '@/utils/init';
|
import { initializeStores } from '@/utils/init';
|
||||||
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import {
|
import {
|
||||||
canManageBilling,
|
canManageBilling,
|
||||||
canUpdateOrganization,
|
canUpdateOrganization,
|
||||||
@@ -89,9 +91,11 @@ onMounted(async () => {
|
|||||||
await fetchToken();
|
await fetchToken();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// prevent store refreshing on navigation
|
// TanStack Query automatically refetches on window focus
|
||||||
|
// Only refresh non-migrated stores
|
||||||
if (isUnloading.value === false) {
|
if (isUnloading.value === false) {
|
||||||
refreshStores();
|
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
|
||||||
|
useTimeEntriesStore().patchTimeEntries();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import { TimeEntryCalendar } from '@/packages/ui/src';
|
|||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
|
||||||
import { getUserTimezone } from '@/packages/ui/src/utils/settings';
|
import { getUserTimezone } from '@/packages/ui/src/utils/settings';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
@@ -98,14 +100,10 @@ async function createClient(body: CreateClientBody): Promise<Client | undefined>
|
|||||||
return await useClientsStore().createClient(body);
|
return await useClientsStore().createClient(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const { projects } = useProjectsQuery();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { tasks } = useTasksQuery();
|
||||||
const taskStore = useTasksStore();
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = storeToRefs(taskStore);
|
const { tags } = useTagsQuery();
|
||||||
const clientStore = useClientsStore();
|
|
||||||
const { clients } = storeToRefs(clientStore);
|
|
||||||
const tagsStore = useTagsStore();
|
|
||||||
const { tags } = storeToRefs(tagsStore);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,21 @@ import AppLayout from '@/Layouts/AppLayout.vue';
|
|||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import ClientTable from '@/Components/Common/Client/ClientTable.vue';
|
import ClientTable from '@/Components/Common/Client/ClientTable.vue';
|
||||||
import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue';
|
import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import { canCreateClients } from '@/utils/permissions';
|
import { canCreateClients } from '@/utils/permissions';
|
||||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
|
|
||||||
onMounted(() => {
|
const { clients } = useClientsQuery();
|
||||||
useClientsStore().fetchClients();
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeTab = ref<'active' | 'archived'>('active');
|
const activeTab = ref<'active' | 'archived'>('active');
|
||||||
|
|
||||||
const createClient = ref(false);
|
const createClient = ref(false);
|
||||||
|
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
|
||||||
|
|
||||||
const shownClients = computed(() => {
|
const shownClients = computed(() => {
|
||||||
return clients.value.filter((client) => {
|
return clients.value.filter((client) => {
|
||||||
if (activeTab.value === 'active') {
|
if (activeTab.value === 'active') {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
|||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { computed, onMounted, ref, inject, type ComputedRef } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
@@ -20,18 +19,18 @@ import CardTitle from '@/packages/ui/src/CardTitle.vue';
|
|||||||
import Card from '@/Components/Common/Card.vue';
|
import Card from '@/Components/Common/Card.vue';
|
||||||
import ProjectMemberTable from '@/Components/Common/ProjectMember/ProjectMemberTable.vue';
|
import ProjectMemberTable from '@/Components/Common/ProjectMember/ProjectMemberTable.vue';
|
||||||
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
|
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
|
||||||
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
import { useProjectMembersQuery } from '@/utils/useProjectMembersQuery';
|
||||||
import { canCreateProjects, canCreateTasks, canViewProjectMembers } from '@/utils/permissions';
|
import { canCreateProjects, canCreateTasks, canViewProjectMembers } from '@/utils/permissions';
|
||||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
|
import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
|
||||||
import { Badge } from '@/packages/ui/src';
|
import { Badge } from '@/packages/ui/src';
|
||||||
import { formatCents } from '../packages/ui/src/utils/money';
|
import { formatCents } from '../packages/ui/src/utils/money';
|
||||||
import { getOrganizationCurrencyString } from '../utils/money';
|
import { getOrganizationCurrencyString } from '../utils/money';
|
||||||
import type { Organization } from '@/packages/api/src';
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const { projects } = storeToRefs(useProjectsStore());
|
const { projects } = useProjectsQuery();
|
||||||
|
|
||||||
const organization = inject<ComputedRef<Organization>>('organization');
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
@@ -42,20 +41,16 @@ const createTask = ref(false);
|
|||||||
const createProjectMember = ref(false);
|
const createProjectMember = ref(false);
|
||||||
const projectId = route()?.params?.project as string;
|
const projectId = route()?.params?.project as string;
|
||||||
|
|
||||||
const { projectMembers } = storeToRefs(useProjectMembersStore());
|
// TanStack Query automatically fetches project members when component mounts
|
||||||
|
const { projectMembers } = canViewProjectMembers()
|
||||||
onMounted(() => {
|
? useProjectMembersQuery(projectId)
|
||||||
if (canViewProjectMembers()) {
|
: { projectMembers: computed(() => []) };
|
||||||
useProjectMembersStore().fetchProjectMembers(projectId);
|
|
||||||
}
|
|
||||||
useTasksStore().fetchTasks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const showEditProjectModal = ref(false);
|
const showEditProjectModal = ref(false);
|
||||||
|
|
||||||
const activeTab = ref<'active' | 'done'>('active');
|
const activeTab = ref<'active' | 'done'>('active');
|
||||||
|
|
||||||
const { tasks } = storeToRefs(useTasksStore());
|
const { tasks } = useTasksQuery();
|
||||||
|
|
||||||
const shownTasks = computed(() => {
|
const shownTasks = computed(() => {
|
||||||
return tasks.value.filter((task) => {
|
return tasks.value.filter((task) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
import { FolderIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
||||||
import type {
|
import type {
|
||||||
@@ -15,6 +15,7 @@ import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue
|
|||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import type { CreateClientBody, Client, CreateProjectBody, Project } from '@/packages/api/src';
|
import type { CreateClientBody, Client, CreateProjectBody, Project } from '@/packages/api/src';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
@@ -29,8 +30,7 @@ import { NO_CLIENT_ID } from '@/Components/Common/Project/constants';
|
|||||||
|
|
||||||
// Fetch data using TanStack Query
|
// Fetch data using TanStack Query
|
||||||
const { projects } = useProjectsQuery();
|
const { projects } = useProjectsQuery();
|
||||||
|
const { clients } = useClientsQuery();
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
|
||||||
const { organization } = storeToRefs(useOrganizationStore());
|
const { organization } = storeToRefs(useOrganizationStore());
|
||||||
|
|
||||||
// Table state persisted in localStorage
|
// Table state persisted in localStorage
|
||||||
|
|||||||
@@ -36,15 +36,18 @@ import MemberMultiselectDropdown from '@/Components/Common/Member/MemberMultisel
|
|||||||
import TaskMultiselectDropdown from '@/Components/Common/Task/TaskMultiselectDropdown.vue';
|
import TaskMultiselectDropdown from '@/Components/Common/Task/TaskMultiselectDropdown.vue';
|
||||||
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
|
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
|
||||||
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
|
import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue';
|
||||||
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
import { useSessionStorage } from '@vueuse/core';
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
||||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import {
|
import {
|
||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
PaginationFirst,
|
PaginationFirst,
|
||||||
@@ -88,7 +91,7 @@ const roundingEnabled = ref<boolean>(false);
|
|||||||
const roundingType = ref<TimeEntryRoundingType>('nearest');
|
const roundingType = ref<TimeEntryRoundingType>('nearest');
|
||||||
const roundingMinutes = ref<number>(15);
|
const roundingMinutes = ref<number>(15);
|
||||||
|
|
||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = useMembersQuery();
|
||||||
const pageLimit = 15;
|
const pageLimit = 15;
|
||||||
|
|
||||||
// Watch rounding enabled state to trigger updates
|
// Watch rounding enabled state to trigger updates
|
||||||
@@ -126,7 +129,7 @@ const { setActiveState, startLiveTimer } = currentTimeEntryStore;
|
|||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
const { createTimeEntry, updateTimeEntry, updateTimeEntries } = useTimeEntriesStore();
|
const { createTimeEntry, updateTimeEntry, updateTimeEntries } = useTimeEntriesStore();
|
||||||
|
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
const { tags } = useTagsQuery();
|
||||||
|
|
||||||
const { data: timeEntryResponse } = useQuery<TimeEntryResponse>({
|
const { data: timeEntryResponse } = useQuery<TimeEntryResponse>({
|
||||||
queryKey: ['timeEntry', 'detailed-report'],
|
queryKey: ['timeEntry', 'detailed-report'],
|
||||||
@@ -160,12 +163,9 @@ onMounted(async () => {
|
|||||||
await updateFilteredTimeEntries();
|
await updateFilteredTimeEntries();
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const { projects } = useProjectsQuery();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { tasks } = useTasksQuery();
|
||||||
const taskStore = useTasksStore();
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = storeToRefs(taskStore);
|
|
||||||
const clientStore = useClientsStore();
|
|
||||||
const { clients } = storeToRefs(clientStore);
|
|
||||||
|
|
||||||
const selectedTimeEntries = ref<TimeEntry[]>([]);
|
const selectedTimeEntries = ref<TimeEntry[]>([]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,19 @@ import { useElementVisibility } from '@vueuse/core';
|
|||||||
import { ClockIcon } from '@heroicons/vue/20/solid';
|
import { ClockIcon } from '@heroicons/vue/20/solid';
|
||||||
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
|
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
|
||||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
|
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
|
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
|
import { useTagsStore } from '@/utils/useTags';
|
||||||
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
|
|
||||||
const timeEntriesStore = useTimeEntriesStore();
|
const timeEntriesStore = useTimeEntriesStore();
|
||||||
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
||||||
@@ -43,7 +46,6 @@ const isLoadMoreVisible = useElementVisibility(loadMoreContainer);
|
|||||||
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
||||||
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
||||||
const { setActiveState } = currentTimeEntryStore;
|
const { setActiveState } = currentTimeEntryStore;
|
||||||
const { tags } = storeToRefs(useTagsStore());
|
|
||||||
|
|
||||||
async function startTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
async function startTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
||||||
if (currentTimeEntry.value.id) {
|
if (currentTimeEntry.value.id) {
|
||||||
@@ -70,12 +72,11 @@ onMounted(async () => {
|
|||||||
await timeEntriesStore.fetchTimeEntries();
|
await timeEntriesStore.fetchTimeEntries();
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const { projects } = useProjectsQuery();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { tasks } = useTasksQuery();
|
||||||
const taskStore = useTasksStore();
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = storeToRefs(taskStore);
|
|
||||||
const clientStore = useClientsStore();
|
const { tags } = useTagsQuery();
|
||||||
const { clients } = storeToRefs(clientStore);
|
|
||||||
|
|
||||||
async function createTag(name: string) {
|
async function createTag(name: string) {
|
||||||
return await useTagsStore().createTag(name);
|
return await useTagsStore().createTag(name);
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
|||||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import type { User } from '@/types/models';
|
import type { User } from '@/types/models';
|
||||||
import { VueQueryPlugin } from '@tanstack/vue-query';
|
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
|
||||||
import { type DefineComponent } from 'vue';
|
import { type DefineComponent } from 'vue';
|
||||||
|
import { setupPrefetching } from '@/utils/prefetch';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => `${title} - ${appName}`,
|
title: (title) => `${title} - ${appName}`,
|
||||||
@@ -62,7 +64,10 @@ createInertiaApp({
|
|||||||
return page.props.auth.user.timezone;
|
return page.props.auth.user.timezone;
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(plugin).use(pinia).use(ZiggyVue).use(VueQueryPlugin).mount(el);
|
app.use(plugin).use(pinia).use(ZiggyVue).use(VueQueryPlugin, { queryClient }).mount(el);
|
||||||
|
|
||||||
|
// Setup Inertia prefetching to warm TanStack Query cache
|
||||||
|
setupPrefetching(queryClient);
|
||||||
},
|
},
|
||||||
|
|
||||||
progress: {
|
progress: {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const expandedStatusClasses = computed(() => {
|
const expandedStatusClasses = computed(() => {
|
||||||
if (props.expanded) {
|
if (props.expanded) {
|
||||||
return 'border-card-border border bg-card-background-active text-text-primary';
|
return 'border-card-border border bg-quaternary text-text-primary';
|
||||||
}
|
}
|
||||||
return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-text-secondary';
|
return 'border-card-border border hover:bg-tertiary hover:text-text-primary transition text-text-secondary';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
import { useProjectsStore } from '@/utils/useProjects';
|
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
|
||||||
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import { canViewClients, canViewMembers } from '@/utils/permissions';
|
|
||||||
|
|
||||||
export function initializeStores() {
|
export function initializeStores() {
|
||||||
refreshStores();
|
// TanStack Query now handles projects, tasks, tags, clients, and members fetching automatically
|
||||||
}
|
// Only initialize stores that aren't migrated to TanStack Query yet
|
||||||
|
|
||||||
export function refreshStores() {
|
|
||||||
useProjectsStore().fetchProjects();
|
|
||||||
useTasksStore().fetchTasks();
|
|
||||||
useTagsStore().fetchTags();
|
|
||||||
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
|
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
|
||||||
useTimeEntriesStore().patchTimeEntries();
|
useTimeEntriesStore().patchTimeEntries();
|
||||||
if (canViewMembers()) {
|
|
||||||
useMembersStore().fetchMembers();
|
|
||||||
}
|
|
||||||
if (canViewClients()) {
|
|
||||||
useClientsStore().fetchClients();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
283
resources/js/utils/prefetch.ts
Normal file
283
resources/js/utils/prefetch.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import { canViewClients, canViewMembers } from '@/utils/permissions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route patterns mapped to their prefetch functions.
|
||||||
|
* Each function receives the QueryClient and prefetches relevant data.
|
||||||
|
*/
|
||||||
|
const routePrefetchers: Record<string, (queryClient: QueryClient) => void> = {
|
||||||
|
'/': (queryClient) => {
|
||||||
|
prefetchDashboard(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/dashboard': (queryClient) => {
|
||||||
|
prefetchDashboard(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/time': (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchTasks(queryClient);
|
||||||
|
prefetchTags(queryClient);
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/calendar': (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchTasks(queryClient);
|
||||||
|
prefetchTags(queryClient);
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/projects': (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/clients': (queryClient) => {
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/tags': (queryClient) => {
|
||||||
|
prefetchTags(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/members': (queryClient) => {
|
||||||
|
prefetchMembers(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/reporting': (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchTags(queryClient);
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
prefetchMembers(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/reporting/detailed': (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchTasks(queryClient);
|
||||||
|
prefetchTags(queryClient);
|
||||||
|
prefetchClients(queryClient);
|
||||||
|
prefetchMembers(queryClient);
|
||||||
|
},
|
||||||
|
|
||||||
|
'/reporting/shared': (queryClient) => {
|
||||||
|
prefetchReports(queryClient);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function prefetchDashboard(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) return;
|
||||||
|
|
||||||
|
// Prefetch all dashboard card data
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['timeEntries', organizationId],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getTimeEntries({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { limit: 10, offset: 0, only_full_dates: 'true' },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['lastSevenDays', organizationId],
|
||||||
|
queryFn: () => api.lastSevenDays({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['dailyTrackedHours', organizationId],
|
||||||
|
queryFn: () => api.dailyTrackedHours({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['weeklyProjectOverview', organizationId],
|
||||||
|
queryFn: () => api.weeklyProjectOverview({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['totalWeeklyTime', organizationId],
|
||||||
|
queryFn: () => api.totalWeeklyTime({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['totalWeeklyBillableTime', organizationId],
|
||||||
|
queryFn: () => api.totalWeeklyBillableTime({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['totalWeeklyBillableAmount', organizationId],
|
||||||
|
queryFn: () => api.totalWeeklyBillableAmount({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['weeklyHistory', organizationId],
|
||||||
|
queryFn: () => api.weeklyHistory({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prefetch team activity only if user has permission
|
||||||
|
if (canViewMembers()) {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['latestTeamActivity', organizationId],
|
||||||
|
queryFn: () => api.latestTeamActivity({ params: { organization: organizationId } }),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchProjects(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['projects'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getProjects({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { archived: 'all' },
|
||||||
|
}),
|
||||||
|
staleTime: 30000, // Consider fresh for 30 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchTasks(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['tasks'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getTasks({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { done: 'all' },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchTags(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['tags'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getTags({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchClients(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId || !canViewClients()) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['clients'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getClients({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { archived: 'all' },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchMembers(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId || !canViewMembers()) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['members'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getMembers({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchReports(queryClient: QueryClient) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['reports', 1],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getReports({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchProjectMembers(queryClient: QueryClient, projectId: string) {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId || !canViewMembers()) return;
|
||||||
|
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['projectMembers', projectId],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getProjectMembers({
|
||||||
|
params: { organization: organizationId, project: projectId },
|
||||||
|
}),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a URL to find the appropriate prefetcher.
|
||||||
|
* Handles both exact matches and pattern matching for dynamic routes.
|
||||||
|
*/
|
||||||
|
function findPrefetcher(url: string): ((queryClient: QueryClient) => void) | undefined {
|
||||||
|
// Extract pathname from URL
|
||||||
|
const pathname = url.startsWith('http') ? new URL(url).pathname : url.split('?')[0];
|
||||||
|
|
||||||
|
// Try exact match first
|
||||||
|
if (routePrefetchers[pathname]) {
|
||||||
|
return routePrefetchers[pathname];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try pattern matching for dynamic routes like /projects/{id}
|
||||||
|
const projectMatch = pathname.match(/^\/projects\/([^/]+)$/);
|
||||||
|
if (projectMatch) {
|
||||||
|
const projectId = projectMatch[1];
|
||||||
|
return (queryClient) => {
|
||||||
|
prefetchProjects(queryClient);
|
||||||
|
prefetchTasks(queryClient);
|
||||||
|
prefetchProjectMembers(queryClient, projectId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up Inertia prefetch event listener to warm TanStack Query cache.
|
||||||
|
* Call this once during app initialization.
|
||||||
|
*/
|
||||||
|
export function setupPrefetching(queryClient: QueryClient) {
|
||||||
|
// Listen for the 'prefetching' event which fires when Inertia starts prefetching a page
|
||||||
|
// The event detail contains the visit object with the URL being prefetched
|
||||||
|
document.addEventListener('inertia:prefetching', ((event: CustomEvent) => {
|
||||||
|
const visit = event.detail?.visit;
|
||||||
|
if (!visit?.url) return;
|
||||||
|
|
||||||
|
const url = visit.url.href || visit.url.toString();
|
||||||
|
const prefetcher = findPrefetcher(url);
|
||||||
|
|
||||||
|
if (prefetcher) {
|
||||||
|
prefetcher(queryClient);
|
||||||
|
}
|
||||||
|
}) as EventListener);
|
||||||
|
}
|
||||||
@@ -1,37 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { computed, ref } from 'vue';
|
import type { CreateClientBody, Client, UpdateClientBody } from '@/packages/api/src';
|
||||||
import type {
|
|
||||||
CreateClientBody,
|
|
||||||
ClientIndexResponse,
|
|
||||||
Client,
|
|
||||||
UpdateClientBody,
|
|
||||||
} from '@/packages/api/src';
|
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
|
|
||||||
export const useClientsStore = defineStore('clients', () => {
|
export const useClientsStore = defineStore('clients', () => {
|
||||||
const clientResponse = ref<ClientIndexResponse | null>(null);
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
async function fetchClients() {
|
|
||||||
const organization = getCurrentOrganizationId();
|
|
||||||
if (organization) {
|
|
||||||
clientResponse.value = await handleApiRequestNotifications(
|
|
||||||
() =>
|
|
||||||
api.getClients({
|
|
||||||
queries: {
|
|
||||||
archived: 'all',
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
organization: organization,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
'Failed to fetch clients'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createClient(clientBody: CreateClientBody): Promise<Client | undefined> {
|
async function createClient(clientBody: CreateClientBody): Promise<Client | undefined> {
|
||||||
const organization = getCurrentOrganizationId();
|
const organization = getCurrentOrganizationId();
|
||||||
@@ -46,7 +22,7 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
'Client created successfully',
|
'Client created successfully',
|
||||||
'Failed to create client'
|
'Failed to create client'
|
||||||
);
|
);
|
||||||
await fetchClients();
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||||
return response?.data;
|
return response?.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +41,7 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
'Client updated successfully',
|
'Client updated successfully',
|
||||||
'Failed to update client'
|
'Failed to update client'
|
||||||
);
|
);
|
||||||
await fetchClients();
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +59,9 @@ export const useClientsStore = defineStore('clients', () => {
|
|||||||
'Client deleted successfully',
|
'Client deleted successfully',
|
||||||
'Failed to delete client'
|
'Failed to delete client'
|
||||||
);
|
);
|
||||||
await fetchClients();
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clients = computed<Client[]>(() => {
|
return { createClient, deleteClient, updateClient };
|
||||||
return clientResponse.value?.data || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
return { clients, fetchClients, createClient, deleteClient, updateClient };
|
|
||||||
});
|
});
|
||||||
|
|||||||
35
resources/js/utils/useClientsQuery.ts
Normal file
35
resources/js/utils/useClientsQuery.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import type { Client } from '@/packages/api/src';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export function useClientsQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['clients'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) throw new Error('No organization');
|
||||||
|
return api.getClients({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { archived: 'all' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled: () => !!getCurrentOrganizationId(),
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const clients = computed<Client[]>(() => query.data.value?.data ?? []);
|
||||||
|
|
||||||
|
const invalidateClients = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
clients,
|
||||||
|
invalidateClients,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,31 +1,15 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { computed, ref } from 'vue';
|
import type { UpdateMemberBody } from '@/packages/api/src';
|
||||||
import type { Member, MemberIndexResponse, UpdateMemberBody } from '@/packages/api/src';
|
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
|
|
||||||
export type MemberBillableKey = 'default-rate' | 'custom-rate';
|
export type MemberBillableKey = 'default-rate' | 'custom-rate';
|
||||||
|
|
||||||
export const useMembersStore = defineStore('members', () => {
|
export const useMembersStore = defineStore('members', () => {
|
||||||
const membersResponse = ref<MemberIndexResponse | null>(null);
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
async function fetchMembers() {
|
|
||||||
const organization = getCurrentOrganizationId();
|
|
||||||
if (organization) {
|
|
||||||
membersResponse.value = await handleApiRequestNotifications(
|
|
||||||
() =>
|
|
||||||
api.getMembers({
|
|
||||||
params: {
|
|
||||||
organization: organization,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
'Failed to fetch members'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeMember(membershipId: string) {
|
async function removeMember(membershipId: string) {
|
||||||
const organization = getCurrentOrganizationId();
|
const organization = getCurrentOrganizationId();
|
||||||
@@ -41,7 +25,7 @@ export const useMembersStore = defineStore('members', () => {
|
|||||||
'Member deleted successfully',
|
'Member deleted successfully',
|
||||||
'Failed to delete member'
|
'Failed to delete member'
|
||||||
);
|
);
|
||||||
await fetchMembers();
|
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,13 +43,9 @@ export const useMembersStore = defineStore('members', () => {
|
|||||||
'Member updated successfully',
|
'Member updated successfully',
|
||||||
'Failed to update member'
|
'Failed to update member'
|
||||||
);
|
);
|
||||||
await fetchMembers();
|
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const members = computed<Member[]>(() => {
|
return { removeMember, updateMember };
|
||||||
return membersResponse.value?.data || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
return { members, fetchMembers, removeMember, updateMember };
|
|
||||||
});
|
});
|
||||||
|
|||||||
34
resources/js/utils/useMembersQuery.ts
Normal file
34
resources/js/utils/useMembersQuery.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import type { Member } from '@/packages/api/src';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export function useMembersQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['members'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) throw new Error('No organization');
|
||||||
|
return api.getMembers({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled: () => !!getCurrentOrganizationId(),
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const members = computed<Member[]>(() => query.data.value?.data ?? []);
|
||||||
|
|
||||||
|
const invalidateMembers = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['members'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
members,
|
||||||
|
invalidateMembers,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,35 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { computed, ref } from 'vue';
|
import type { CreateProjectMemberBody, UpdateProjectMemberBody } from '@/packages/api/src';
|
||||||
import type {
|
|
||||||
CreateProjectMemberBody,
|
|
||||||
ProjectMember,
|
|
||||||
ProjectMemberResponse,
|
|
||||||
UpdateProjectMemberBody,
|
|
||||||
} from '@/packages/api/src';
|
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
|
|
||||||
export const useProjectMembersStore = defineStore('project-members', () => {
|
export const useProjectMembersStore = defineStore('project-members', () => {
|
||||||
const projectMemberResponse = ref<ProjectMemberResponse | null>(null);
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
async function fetchProjectMembers(projectId: string) {
|
|
||||||
const organization = getCurrentOrganizationId();
|
|
||||||
if (organization) {
|
|
||||||
projectMemberResponse.value = await handleApiRequestNotifications(
|
|
||||||
() =>
|
|
||||||
api.getProjectMembers({
|
|
||||||
params: {
|
|
||||||
organization: organization,
|
|
||||||
project: projectId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
'Failed to fetch project members'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createProjectMember(
|
async function createProjectMember(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -48,7 +26,7 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
|||||||
'Project member added successfully',
|
'Project member added successfully',
|
||||||
'Failed to add project member'
|
'Failed to add project member'
|
||||||
);
|
);
|
||||||
await fetchProjectMembers(projectId);
|
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +47,11 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
|||||||
'Project member updated successfully',
|
'Project member updated successfully',
|
||||||
'Failed to update project member'
|
'Failed to update project member'
|
||||||
);
|
);
|
||||||
await fetchProjectMembers(response.data.project_id);
|
if (response?.data?.project_id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['projectMembers', response.data.project_id],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +69,11 @@ export const useProjectMembersStore = defineStore('project-members', () => {
|
|||||||
'Project member removed successfully',
|
'Project member removed successfully',
|
||||||
'Failed to remove project member'
|
'Failed to remove project member'
|
||||||
);
|
);
|
||||||
await fetchProjectMembers(projectId);
|
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectId] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectMembers = computed<ProjectMember[]>(() => projectMemberResponse.value?.data || []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectMembers,
|
|
||||||
fetchProjectMembers,
|
|
||||||
createProjectMember,
|
createProjectMember,
|
||||||
deleteProjectMember,
|
deleteProjectMember,
|
||||||
updateProjectMember,
|
updateProjectMember,
|
||||||
|
|||||||
39
resources/js/utils/useProjectMembersQuery.ts
Normal file
39
resources/js/utils/useProjectMembersQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import type { ProjectMember } from '@/packages/api/src';
|
||||||
|
import { computed, type Ref } from 'vue';
|
||||||
|
|
||||||
|
export function useProjectMembersQuery(projectId: Ref<string | null> | string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const projectIdValue = computed(() => {
|
||||||
|
return typeof projectId === 'string' ? projectId : projectId.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['projectMembers', projectIdValue],
|
||||||
|
queryFn: async () => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
const pid = projectIdValue.value;
|
||||||
|
if (!organizationId || !pid) throw new Error('No organization or project');
|
||||||
|
return api.getProjectMembers({
|
||||||
|
params: { organization: organizationId, project: pid },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled: () => !!getCurrentOrganizationId() && !!projectIdValue.value,
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectMembers = computed<ProjectMember[]>(() => query.data.value?.data ?? []);
|
||||||
|
|
||||||
|
const invalidateProjectMembers = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['projectMembers', projectIdValue.value] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
projectMembers,
|
||||||
|
invalidateProjectMembers,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export function useProjectsQuery() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled: () => !!getCurrentOrganizationId(),
|
enabled: () => !!getCurrentOrganizationId(),
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects = computed<Project[]>(() => query.data.value?.data ?? []);
|
const projects = computed<Project[]>(() => query.data.value?.data ?? []);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineStore, storeToRefs } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { type Component, computed, ref } from 'vue';
|
import { type Component, computed, ref } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
@@ -8,11 +8,11 @@ import type {
|
|||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import { getCurrentOrganizationId, getCurrentRole, getCurrentUser } from '@/utils/useUser';
|
import { getCurrentOrganizationId, getCurrentRole, getCurrentUser } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersQuery } from '@/utils/useMembersQuery';
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { useTagsStore } from '@/utils/useTags';
|
import { useTagsQuery } from '@/utils/useTagsQuery';
|
||||||
import { CheckCircleIcon, UserCircleIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
import { CheckCircleIcon, UserCircleIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||||
import { DocumentTextIcon, FolderIcon } from '@heroicons/vue/16/solid';
|
import { DocumentTextIcon, FolderIcon } from '@heroicons/vue/16/solid';
|
||||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||||
@@ -32,6 +32,13 @@ export const useReportingStore = defineStore('reporting', () => {
|
|||||||
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
|
// Cache query composables to avoid creating new subscriptions on every call
|
||||||
|
const { projects } = useProjectsQuery();
|
||||||
|
const { members } = useMembersQuery();
|
||||||
|
const { tasks } = useTasksQuery();
|
||||||
|
const { clients } = useClientsQuery();
|
||||||
|
const { tags } = useTagsQuery();
|
||||||
|
|
||||||
async function fetchGraphReporting(params: AggregatedTimeEntriesQueryParams) {
|
async function fetchGraphReporting(params: AggregatedTimeEntriesQueryParams) {
|
||||||
const organization = getCurrentOrganizationId();
|
const organization = getCurrentOrganizationId();
|
||||||
if (organization) {
|
if (organization) {
|
||||||
@@ -93,31 +100,21 @@ export const useReportingStore = defineStore('reporting', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'project') {
|
if (type === 'project') {
|
||||||
const projectsStore = useProjectsStore();
|
|
||||||
const { projects } = storeToRefs(projectsStore);
|
|
||||||
return projects.value.find((project) => project.id === key)?.name;
|
return projects.value.find((project) => project.id === key)?.name;
|
||||||
}
|
}
|
||||||
if (type === 'user') {
|
if (type === 'user') {
|
||||||
if (getCurrentRole() === 'employee') {
|
if (getCurrentRole() === 'employee') {
|
||||||
return getCurrentUser().name;
|
return getCurrentUser().name;
|
||||||
}
|
}
|
||||||
const memberStore = useMembersStore();
|
|
||||||
const { members } = storeToRefs(memberStore);
|
|
||||||
return members.value.find((member) => member.user_id === key)?.name;
|
return members.value.find((member) => member.user_id === key)?.name;
|
||||||
}
|
}
|
||||||
if (type === 'task') {
|
if (type === 'task') {
|
||||||
const taskStore = useTasksStore();
|
|
||||||
const { tasks } = storeToRefs(taskStore);
|
|
||||||
return tasks.value.find((task) => task.id === key)?.name;
|
return tasks.value.find((task) => task.id === key)?.name;
|
||||||
}
|
}
|
||||||
if (type === 'client') {
|
if (type === 'client') {
|
||||||
const clientsStore = useClientsStore();
|
|
||||||
const { clients } = storeToRefs(clientsStore);
|
|
||||||
return clients.value.find((client) => client.id === key)?.name;
|
return clients.value.find((client) => client.id === key)?.name;
|
||||||
}
|
}
|
||||||
if (type === 'tag') {
|
if (type === 'tag') {
|
||||||
const tagsStore = useTagsStore();
|
|
||||||
const { tags } = storeToRefs(tagsStore);
|
|
||||||
return tags.value.find((tag) => tag.id === key)?.name;
|
return tags.value.find((tag) => tag.id === key)?.name;
|
||||||
}
|
}
|
||||||
if (type === 'billable') {
|
if (type === 'billable') {
|
||||||
|
|||||||
@@ -1,33 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
|
||||||
import type { Tag } from '@/packages/api/src';
|
import type { Tag } from '@/packages/api/src';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
|
|
||||||
export const useTagsStore = defineStore('tags', () => {
|
export const useTagsStore = defineStore('tags', () => {
|
||||||
const tags = ref<Tag[]>([]);
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
async function fetchTags() {
|
const queryClient = useQueryClient();
|
||||||
const organizationId = getCurrentOrganizationId();
|
|
||||||
if (organizationId) {
|
|
||||||
const response = await handleApiRequestNotifications(
|
|
||||||
() =>
|
|
||||||
api.getTags({
|
|
||||||
params: {
|
|
||||||
organization: organizationId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
'Failed to fetch tags'
|
|
||||||
);
|
|
||||||
if (response?.data) {
|
|
||||||
tags.value = response.data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to fetch current tags because organization ID is missing.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteTag(tagId: string) {
|
async function deleteTag(tagId: string) {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
@@ -43,11 +23,11 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
'Tag deleted successfully',
|
'Tag deleted successfully',
|
||||||
'Failed to delete tag'
|
'Failed to delete tag'
|
||||||
);
|
);
|
||||||
await fetchTags();
|
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTag(name: string) {
|
async function createTag(name: string): Promise<Tag | undefined> {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const response = await handleApiRequestNotifications(
|
const response = await handleApiRequestNotifications(
|
||||||
@@ -66,7 +46,7 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
'Failed to create tag'
|
'Failed to create tag'
|
||||||
);
|
);
|
||||||
if (response?.data) {
|
if (response?.data) {
|
||||||
tags.value.unshift(response.data);
|
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -74,5 +54,5 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tags, fetchTags, createTag, deleteTag };
|
return { createTag, deleteTag };
|
||||||
});
|
});
|
||||||
|
|||||||
34
resources/js/utils/useTagsQuery.ts
Normal file
34
resources/js/utils/useTagsQuery.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import type { Tag } from '@/packages/api/src';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export function useTagsQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['tags'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) throw new Error('No organization');
|
||||||
|
return api.getTags({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled: () => !!getCurrentOrganizationId(),
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags = computed<Tag[]>(() => query.data.value?.data ?? []);
|
||||||
|
|
||||||
|
const invalidateTags = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
tags,
|
||||||
|
invalidateTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,32 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { reactive, ref } from 'vue';
|
import type { CreateTaskBody, UpdateTaskBody } from '@/packages/api/src';
|
||||||
import type { CreateTaskBody, Task, UpdateTaskBody } from '@/packages/api/src';
|
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
|
|
||||||
export const useTasksStore = defineStore('tasks', () => {
|
export const useTasksStore = defineStore('tasks', () => {
|
||||||
const tasks = ref<Task[]>(reactive([]));
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
async function fetchTasks() {
|
|
||||||
const organizationId = getCurrentOrganizationId();
|
|
||||||
if (organizationId) {
|
|
||||||
const tasksResponse = await handleApiRequestNotifications(() =>
|
|
||||||
api.getTasks({
|
|
||||||
params: {
|
|
||||||
organization: organizationId,
|
|
||||||
},
|
|
||||||
queries: {
|
|
||||||
done: 'all',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (tasksResponse?.data) {
|
|
||||||
tasks.value = tasksResponse.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateTask(taskId: string, taskBody: UpdateTaskBody) {
|
async function updateTask(taskId: string, taskBody: UpdateTaskBody) {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
@@ -42,7 +23,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
'Task updated successfully',
|
'Task updated successfully',
|
||||||
'Failed to update task'
|
'Failed to update task'
|
||||||
);
|
);
|
||||||
await fetchTasks();
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +40,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
'Task created successfully',
|
'Task created successfully',
|
||||||
'Failed to create task'
|
'Failed to create task'
|
||||||
);
|
);
|
||||||
await fetchTasks();
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,13 +58,11 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
'Task deleted successfully',
|
'Task deleted successfully',
|
||||||
'Failed to delete task'
|
'Failed to delete task'
|
||||||
);
|
);
|
||||||
await fetchTasks();
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tasks,
|
|
||||||
fetchTasks,
|
|
||||||
updateTask,
|
updateTask,
|
||||||
createTask,
|
createTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
|
|||||||
35
resources/js/utils/useTasksQuery.ts
Normal file
35
resources/js/utils/useTasksQuery.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import type { Task } from '@/packages/api/src';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export function useTasksQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['tasks'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (!organizationId) throw new Error('No organization');
|
||||||
|
return api.getTasks({
|
||||||
|
params: { organization: organizationId },
|
||||||
|
queries: { done: 'all' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled: () => !!getCurrentOrganizationId(),
|
||||||
|
staleTime: 1000 * 30, // 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = computed<Task[]>(() => query.data.value?.data ?? []);
|
||||||
|
|
||||||
|
const invalidateTasks = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
tasks,
|
||||||
|
invalidateTasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user