diff --git a/package-lock.json b/package-lock.json index 141503b8..1f9ef0c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,18 @@ "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.7.0", "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/vue-query": "^5.56.2", + "@tanstack/vue-query-devtools": "^5.58.0", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vueuse/core": "^10.11.0", + "@vueuse/integrations": "^11.1.0", "dayjs": "^1.11.11", "echarts": "^5.5.0", + "focus-trap": "^7.6.0", "parse-duration": "^1.1.0", "pinia": "^2.1.7", - "radix-vue": "^1.5.2", + "radix-vue": "^1.9.6", "tailwind-merge": "^2.2.1", "vue-echarts": "^6.7.2" }, @@ -1663,6 +1667,42 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz", + "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.58.0.tgz", + "integrity": "sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-core": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.9.0.tgz", @@ -1673,6 +1713,74 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/vue-query": { + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.56.2.tgz", + "integrity": "sha512-VW7qS8JXwC3SZpawJHxQ+mWwWa5WVIQUUOh/OD6WI85eLcbJPg83ezjGupPXGKF9h31gl7CIRrnJDi4g5pK3Jg==", + "license": "MIT", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.15.1", + "@tanstack/query-core": "5.56.2", + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.2", + "vue": "^2.6.0 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@tanstack/vue-query-devtools": { + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query-devtools/-/vue-query-devtools-5.58.0.tgz", + "integrity": "sha512-OdCXA7cTt0qkgw87JhLeLdGLmGIXsldXHERZ+Wm2up52H6TqZbWGCs4jvBF3AafiUHl5ibCRoynQ5r114+YPXw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.58.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/vue-query": "^5.56.2", + "vue": "^3.3.0" + } + }, + "node_modules/@tanstack/vue-query/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@tanstack/vue-virtual": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.9.0.tgz", @@ -2185,6 +2293,134 @@ } } }, + "node_modules/@vueuse/integrations": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.1.0.tgz", + "integrity": "sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "11.1.0", + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", + "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.1.0", + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", + "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vueuse/metadata": { "version": "10.11.1", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", @@ -2391,7 +2627,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -2436,7 +2672,7 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2674,7 +2910,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -2819,7 +3055,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3428,11 +3664,20 @@ "license": "ISC", "peer": true }, + "node_modules/focus-trap": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -3469,7 +3714,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4182,7 +4427,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4192,7 +4437,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -4343,7 +4588,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/nth-check": { @@ -5065,7 +5310,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/punycode": { @@ -5115,9 +5360,9 @@ "license": "MIT" }, "node_modules/radix-vue": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.4.tgz", - "integrity": "sha512-d950wxB+MVVU6L9h39OsNzAdk2BiGDDfhXJiHsksPAIK5pCR8W4U0RB0WLQEdjmmL9p1aXOYm4FBDq0oIo2G/w==", + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.6.tgz", + "integrity": "sha512-legrn9jHbEpbJS4QYrA0VmIafj1bmc4MSVzN55WZatGiXMJg3oFrQL5QxpiURJciS+OlATbKA2KAGkMuuLA0LA==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.7", @@ -5175,6 +5420,12 @@ "node": ">=8.10.0" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5612,6 +5863,12 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "license": "0BSD" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", diff --git a/package.json b/package.json index 2f693426..7b370110 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,18 @@ "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.7.0", "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/vue-query": "^5.56.2", + "@tanstack/vue-query-devtools": "^5.58.0", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vueuse/core": "^10.11.0", + "@vueuse/integrations": "^11.1.0", "dayjs": "^1.11.11", "echarts": "^5.5.0", + "focus-trap": "^7.6.0", "parse-duration": "^1.1.0", "pinia": "^2.1.7", - "radix-vue": "^1.5.2", + "radix-vue": "^1.9.6", "tailwind-merge": "^2.2.1", "vue-echarts": "^6.7.2" } diff --git a/resources/js/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue b/resources/js/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue new file mode 100644 index 00000000..bfa18e3f --- /dev/null +++ b/resources/js/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue b/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue index ff3d2182..1e6c7202 100644 --- a/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue +++ b/resources/js/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue @@ -24,7 +24,7 @@ const { setActiveState } = useCurrentTimeEntryStore(); async function startTaskTimer() { if (currentTimeEntry.value.id) { - await setActiveState(true); + await setActiveState(false); } currentTimeEntry.value.project_id = props.project_id; currentTimeEntry.value.task_id = props.task_id; diff --git a/resources/js/Pages/ReportingDetailed.vue b/resources/js/Pages/ReportingDetailed.vue index 92980eb5..8efceaa3 100644 --- a/resources/js/Pages/ReportingDetailed.vue +++ b/resources/js/Pages/ReportingDetailed.vue @@ -9,7 +9,12 @@ import { CheckCircleIcon, TagIcon, ChevronLeftIcon, + ChevronDoubleLeftIcon, ChevronRightIcon, + ChevronDoubleRightIcon, + PencilSquareIcon, + TrashIcon, + ClockIcon, } from '@heroicons/vue/20/solid'; import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue'; import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue'; @@ -20,13 +25,15 @@ import { } from '@/packages/ui/src/utils/time'; import { storeToRefs } from 'pinia'; import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue'; -import type { - Client, - CreateClientBody, - CreateProjectBody, - Project, - TimeEntriesQueryParams, - TimeEntry, +import { + api, + type Client, + type CreateClientBody, + type CreateProjectBody, + type Project, + type TimeEntriesQueryParams, + type TimeEntry, + type TimeEntryResponse, } from '@/packages/api/src'; import ReportingFilterBadge from '@/Components/Common/Reporting/ReportingFilterBadge.vue'; import ProjectMultiselectDropdown from '@/Components/Common/Project/ProjectMultiselectDropdown.vue'; @@ -35,20 +42,31 @@ import TaskMultiselectDropdown from '@/Components/Common/Task/TaskMultiselectDro import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue'; import ClientMultiselectDropdown from '@/Components/Common/Client/ClientMultiselectDropdown.vue'; import { useTagsStore } from '@/utils/useTags'; -import { useElementVisibility, useSessionStorage } from '@vueuse/core'; +import { useSessionStorage } from '@vueuse/core'; import { router } from '@inertiajs/vue3'; import TabBar from '@/Components/Common/TabBar/TabBar.vue'; import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue'; import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue'; -import { useTimeEntriesStore } from '@/utils/useTimeEntries'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; import { useProjectsStore } from '@/utils/useProjects'; import { useTasksStore } from '@/utils/useTasks'; import { useClientsStore } from '@/utils/useClients'; -import dayjs from 'dayjs'; import { getOrganizationCurrencyString } from '@/utils/money'; import { useMembersStore } from '@/utils/useMembers'; -import { SecondaryButton } from '@/packages/ui/src'; +import { + PaginationEllipsis, + PaginationFirst, + PaginationLast, + PaginationList, + PaginationListItem, + PaginationNext, + PaginationPrev, + PaginationRoot, +} from 'radix-vue'; +import { useQuery, useQueryClient } from '@tanstack/vue-query'; +import { getCurrentOrganizationId } from '@/utils/useUser'; +import { useTimeEntriesStore } from '@/utils/useTimeEntries'; +import TimeEntryMassUpdateModal from '@/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue'; const startDate = useSessionStorage( 'reporting-start-date', @@ -66,13 +84,16 @@ const selectedClients = ref([]); const billable = ref<'true' | 'false' | null>(null); const { members } = storeToRefs(useMembersStore()); -const pageLimit = 10; +const pageLimit = 15; +const currentPage = ref(1); + function getFilterAttributes() { let params: TimeEntriesQueryParams = { start: getLocalizedDayJs(startDate.value).startOf('day').utc().format(), end: getLocalizedDayJs(endDate.value).endOf('day').utc().format(), active: 'false', limit: pageLimit, + offset: currentPage.value * pageLimit - pageLimit, }; params = { ...params, @@ -96,37 +117,41 @@ function getFilterAttributes() { return params; } -const timeEntriesStore = useTimeEntriesStore(); -const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore); -const { updateTimeEntry, fetchTimeEntries, createTimeEntry } = - useTimeEntriesStore(); - -const loading = ref(false); -const loadMoreContainer = ref(null); -const isLoadMoreVisible = useElementVisibility(loadMoreContainer); const currentTimeEntryStore = useCurrentTimeEntryStore(); const { currentTimeEntry } = storeToRefs(currentTimeEntryStore); -const { stopTimer } = currentTimeEntryStore; +const { setActiveState, startLiveTimer } = currentTimeEntryStore; +const { createTimeEntry, updateTimeEntry } = useTimeEntriesStore(); + const { tags } = storeToRefs(useTagsStore()); -const currentPage = ref(1); +const { data: timeEntryResponse } = useQuery({ + queryKey: ['timeEntry', 'detailed-report'], + enabled: !!getCurrentOrganizationId(), + queryFn: () => + api.getTimeEntries({ + params: { + organization: getCurrentOrganizationId() || '', + }, + queries: getFilterAttributes(), + }), +}); -function deleteTimeEntries(timeEntries: TimeEntry[]) { - timeEntries.forEach((entry) => { - timeEntriesStore.deleteTimeEntry(entry.id); - }); - updateFilteredTimeEntries(); +const totalPages = computed(() => { + return timeEntryResponse?.value?.meta?.total ?? 1; +}); + +const timeEntriesStore = useTimeEntriesStore(); + +async function deleteTimeEntries(timeEntries: TimeEntry[]) { + for (const timeEntry of timeEntries) { + await timeEntriesStore.deleteTimeEntry(timeEntry.id); + } + selectedTimeEntries.value = []; + await updateFilteredTimeEntries(); } -watch(isLoadMoreVisible, async (isVisible) => { - if ( - isVisible && - timeEntries.value.length > 0 && - !allTimeEntriesLoaded.value - ) { - loading.value = true; - await timeEntriesStore.fetchMoreTimeEntries(); - } +const timeEntries = computed(() => { + return timeEntryResponse?.value?.data || []; }); onMounted(async () => { @@ -140,14 +165,18 @@ const { tasks } = storeToRefs(taskStore); const clientStore = useClientsStore(); const { clients } = storeToRefs(clientStore); +const selectedTimeEntries = ref([]); + async function createTag(name: string) { return await useTagsStore().createTag(name); } + async function createProject( project: CreateProjectBody ): Promise { return await useProjectsStore().createProject(project); } + async function createClient( body: CreateClientBody ): Promise { @@ -156,35 +185,34 @@ async function createClient( async function startTimeEntryFromExisting(entry: TimeEntry) { if (currentTimeEntry.value.id) { - await stopTimer(); + await setActiveState(false); } await createTimeEntry({ project_id: entry.project_id, task_id: entry.task_id, - start: dayjs().utc().format(), + start: getDayJsInstance().utc().format(), end: null, billable: entry.billable, description: entry.description, }); + startLiveTimer(); updateFilteredTimeEntries(); useCurrentTimeEntryStore().fetchCurrentTimeEntry(); } - +const queryClient = useQueryClient(); async function updateFilteredTimeEntries() { - await fetchTimeEntries(getFilterAttributes()); -} - -const isNextPageAvailable = computed(() => { - return timeEntries && timeEntries.value.length === pageLimit; -}); - -function nextPage() { - currentPage.value++; - fetchTimeEntries({ - ...getFilterAttributes(), - end: timeEntries.value[timeEntries.value.length - 1].start, + await queryClient.invalidateQueries({ + queryKey: ['timeEntry', 'detailed-report'], }); } +watch(currentPage, () => { + updateFilteredTimeEntries(); +}); +function deleteSelected() { + deleteTimeEntries(selectedTimeEntries.value); +} + +const showMassUpdateModal = ref(false); + diff --git a/resources/js/app.ts b/resources/js/app.ts index a9fd11b6..5adf58e6 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -7,6 +7,7 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { ZiggyVue } from '../../vendor/tightenco/ziggy'; import { createPinia } from 'pinia'; import type { User } from '@/types/models'; +import { VueQueryPlugin } from '@tanstack/vue-query'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; const pinia = createPinia(); @@ -42,7 +43,7 @@ createInertiaApp({ return page.props.auth.user.timezone; }; - app.use(plugin).use(pinia).use(ZiggyVue).mount(el); + app.use(plugin).use(pinia).use(ZiggyVue).use(VueQueryPlugin).mount(el); }, progress: { diff --git a/resources/js/packages/api/src/index.ts b/resources/js/packages/api/src/index.ts index 120812f9..13917ea0 100644 --- a/resources/js/packages/api/src/index.ts +++ b/resources/js/packages/api/src/index.ts @@ -28,6 +28,14 @@ export type CreateTimeEntryBody = ZodiosBodyByAlias< 'createTimeEntry' >; +export type UpdateMultipleTimeEntriesBody = ZodiosBodyByAlias< + SolidTimeApi, + 'updateMultipleTimeEntries' +>; + +export type UpdateMultipleTimeEntriesChangeset = + UpdateMultipleTimeEntriesBody['changes']; + export type ProjectResponse = ZodiosResponseByAlias< SolidTimeApi, 'getProjects' diff --git a/resources/js/packages/api/src/openapi.json.client.ts b/resources/js/packages/api/src/openapi.json.client.ts index 8d4aa9e4..a06131c7 100644 --- a/resources/js/packages/api/src/openapi.json.client.ts +++ b/resources/js/packages/api/src/openapi.json.client.ts @@ -170,7 +170,6 @@ const TimeEntryResource = z billable: z.boolean(), }) .passthrough(); -const TimeEntryCollection = z.array(TimeEntryResource); const TimeEntryStoreRequest = z .object({ member_id: z.string().uuid(), @@ -270,7 +269,6 @@ export const schemas = { TaskUpdateRequest, start, TimeEntryResource, - TimeEntryCollection, TimeEntryStoreRequest, TimeEntryUpdateMultipleRequest, TimeEntryUpdateRequest, @@ -2147,6 +2145,11 @@ Users with the permission `time-entries:view:own` can only use this en type: 'Query', schema: z.number().int().gte(1).lte(500).optional(), }, + { + name: 'offset', + type: 'Query', + schema: z.number().int().gte(0).optional(), + }, { name: 'only_full_dates', type: 'Query', @@ -2183,7 +2186,12 @@ Users with the permission `time-entries:view:own` can only use this en schema: z.string().optional(), }, ], - response: z.object({ data: TimeEntryCollection }).passthrough(), + response: z + .object({ + data: z.array(TimeEntryResource), + meta: z.object({ total: z.number().int() }).passthrough(), + }) + .passthrough(), errors: [ { status: 401, diff --git a/resources/js/packages/ui/src/Badge.vue b/resources/js/packages/ui/src/Badge.vue index 1bb6d429..8a4fa7f4 100644 --- a/resources/js/packages/ui/src/Badge.vue +++ b/resources/js/packages/ui/src/Badge.vue @@ -39,7 +39,7 @@ const borderClasses = computed(() => { twMerge( badgeClasses[size], borderClasses, - 'rounded inline-flex items-center font-semibold text-white', + 'rounded inline-flex items-center font-semibold text-white outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80', props.class ) "> diff --git a/resources/js/packages/ui/src/Input/DateRangePicker.vue b/resources/js/packages/ui/src/Input/DateRangePicker.vue index eb74a85f..bded7b1e 100644 --- a/resources/js/packages/ui/src/Input/DateRangePicker.vue +++ b/resources/js/packages/ui/src/Input/DateRangePicker.vue @@ -94,7 +94,7 @@ function setLastYear() { @submit="emit('submit')">