From 93d6a86f7472f6555bdd71391cc502c809bca48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gro=C3=9F?= Date: Fri, 17 Oct 2025 00:55:41 +0200 Subject: [PATCH 01/38] Show clients that are assigned to the employee, closes #893 --- .../Controllers/Api/V1/ClientController.php | 13 +++++++ app/Providers/JetstreamServiceProvider.php | 6 +++- .../Endpoint/Api/V1/ClientEndpointTest.php | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index b992ca6e..2fbe0363 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -12,6 +12,7 @@ use App\Http\Resources\V1\Client\ClientResource; use App\Models\Client; use App\Models\Organization; +use App\Models\Project; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Support\Carbon; @@ -38,11 +39,23 @@ protected function checkPermission(Organization $organization, string $permissio public function index(Organization $organization, ClientIndexRequest $request): ClientCollection { $this->checkPermission($organization, 'clients:view'); + $canViewAllClients = $this->hasPermission($organization, 'clients:view:all'); + $user = $this->user(); $clientsQuery = Client::query() ->whereBelongsTo($organization, 'organization') ->orderBy('created_at', 'desc'); + if (! $canViewAllClients) { + $projectsQuery = Project::query() + ->whereBelongsTo($organization, 'organization') + ->visibleByEmployee($user) + ->distinct() + ->select('client_id'); + + $clientsQuery->whereIn('id', $projectsQuery); + } + $filterArchived = $request->getFilterArchived(); if ($filterArchived === 'true') { $clientsQuery->whereNotNull('archived_at'); diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index bca7bbbd..610c2fd3 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -109,6 +109,7 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -172,6 +173,7 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -232,6 +234,7 @@ protected function configurePermissions(): void 'tags:update', 'tags:delete', 'clients:view', + 'clients:view:all', 'clients:create', 'clients:update', 'clients:delete', @@ -256,12 +259,13 @@ protected function configurePermissions(): void 'projects:view', 'tags:view', 'tasks:view', + 'clients:view', 'time-entries:view:own', 'time-entries:create:own', 'time-entries:update:own', 'time-entries:delete:own', 'organizations:view', - ])->description('Employees have the ability to read, create, and update their own time entries and they can see the projects that they are members of.'); + ])->description('Employees have the ability to read, create, and update their own time entries, they can see the projects that they are members of and the clients they are assigned to.'); Jetstream::role(Role::Placeholder->value, 'Placeholder', [ ])->description('Placeholders are used for importing data. They cannot log in and have no permissions.'); diff --git a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php index faf2af53..38100969 100644 --- a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php @@ -34,6 +34,7 @@ public function test_index_endpoint_returns_list_of_all_clients_of_organization_ // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $clients = Client::factory()->forOrganization($data->organization)->randomCreatedAt()->createMany(4); Passport::actingAs($data->user); @@ -57,11 +58,43 @@ public function test_index_endpoint_returns_list_of_all_clients_of_organization_ ); } + public function test_index_endpoint_returns_list_of_clients_assigned_to_employee_user(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view' + ]); + + $clients = Client::factory()->forOrganization($data->organization)->createMany(2); + $projectWithMembership1 = Project::factory()->forOrganization($data->organization)->forClient($clients->get(0))->addMember($data->member)->isPrivate()->create(); + $projectWithMembership2 = Project::factory()->forOrganization($data->organization)->forClient($clients->get(1))->addMember($data->member)->isPrivate()->create(); + + $otherClients = Client::factory()->forOrganization($data->organization)->createMany(2); + $projectWithoutMembership = Project::factory()->forOrganization($data->organization)->forClient($otherClients->get(0))->isPrivate()->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->has('links') + ->has('meta') + ->count('data', 2) + ->where('data.0.id', $clients->get(0)->getKey()) + ->where('data.1.id', $clients->get(1)->getKey()) + ); + } + public function test_index_endpoint_without_filter_archived_returns_only_non_archived_clients(): void { // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -81,6 +114,7 @@ public function test_index_endpoint_with_filter_archived_true_returns_only_archi // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -103,6 +137,7 @@ public function test_index_endpoint_with_filter_archived_false_returns_only_non_ // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); @@ -125,6 +160,7 @@ public function test_index_endpoint_with_filter_archived_all_returns_all_clients // Arrange $data = $this->createUserWithPermission([ 'clients:view', + 'clients:view:all', ]); $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); From 1e985b71ec571dbfd2a9849b24d3873a9a493560 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 21 Oct 2025 12:22:17 +0200 Subject: [PATCH 02/38] move Client visibleByEmployee logic from controller to model --- app/Http/Controllers/Api/V1/ClientController.php | 9 +-------- app/Models/Client.php | 13 +++++++++++++ tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index 2fbe0363..8f657bac 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -12,7 +12,6 @@ use App\Http\Resources\V1\Client\ClientResource; use App\Models\Client; use App\Models\Organization; -use App\Models\Project; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Support\Carbon; @@ -47,13 +46,7 @@ public function index(Organization $organization, ClientIndexRequest $request): ->orderBy('created_at', 'desc'); if (! $canViewAllClients) { - $projectsQuery = Project::query() - ->whereBelongsTo($organization, 'organization') - ->visibleByEmployee($user) - ->distinct() - ->select('client_id'); - - $clientsQuery->whereIn('id', $projectsQuery); + $clientsQuery->visibleByEmployee($user); } $filterArchived = $request->getFilterArchived(); diff --git a/app/Models/Client.php b/app/Models/Client.php index 6189fbf3..82ced395 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -7,6 +7,7 @@ use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; use Database\Factories\ClientFactory; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -62,6 +63,18 @@ public function projects(): HasMany return $this->hasMany(Project::class, 'client_id'); } + /** + * @param Builder $builder + * @return Builder + */ + public function scopeVisibleByEmployee(Builder $builder, User $user): Builder + { + return $builder->whereHas('projects', function (Builder $builder) use ($user): Builder { + /** @var Builder $builder */ + return $builder->visibleByEmployee($user); + }); + } + /** * @return Attribute */ diff --git a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php index 38100969..f2cf0fce 100644 --- a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php @@ -62,7 +62,7 @@ public function test_index_endpoint_returns_list_of_clients_assigned_to_employee { // Arrange $data = $this->createUserWithPermission([ - 'clients:view' + 'clients:view', ]); $clients = Client::factory()->forOrganization($data->organization)->createMany(2); From 9a1dd4861cb683ca31c6b622c2ddacb4ae14e780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gro=C3=9F?= Date: Thu, 16 Oct 2025 21:53:52 +0200 Subject: [PATCH 03/38] Extend description to 5000 chars, closes #914 --- .../V1/TimeEntry/TimeEntryStoreRequest.php | 2 +- .../TimeEntryUpdateMultipleRequest.php | 2 +- .../V1/TimeEntry/TimeEntryUpdateRequest.php | 2 +- .../Importers/ClockifyTimeEntriesImporter.php | 2 +- .../Importers/HarvestTimeEntriesImporter.php | 2 +- .../Import/Importers/SolidtimeImporter.php | 2 +- ...6_000001_extend_time_entry_description.php | 30 +++++++++++++++++++ database/schema/pgsql_test-schema.sql | 2 +- 8 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2025_10_16_000001_extend_time_entry_description.php diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php index 4e0cd0d2..b4d3840c 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php @@ -79,7 +79,7 @@ public function rules(): array 'description' => [ 'nullable', 'string', - 'max:500', + 'max:5000', ], // List of tag IDs 'tags' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php index 353577aa..9f198654 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php @@ -79,7 +79,7 @@ public function rules(): array 'changes.description' => [ 'nullable', 'string', - 'max:500', + 'max:5000', ], // List of tag IDs 'changes.tags' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php index ee9347f0..e9e6795a 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php @@ -77,7 +77,7 @@ public function rules(): array 'description' => [ 'nullable', 'string', - 'max:500', + 'max:5000', ], // List of tag IDs 'tags' => [ diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index 19d30af3..f0fb94ab 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -112,7 +112,7 @@ public function importData(string $data, string $timezone): void $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; $timeEntry->organization_id = $this->organization->id; - if (strlen($record['Description']) > 500) { + if (strlen($record['Description']) > 5000) { throw new ImportException('Time entry description is too long'); } $timeEntry->description = $record['Description']; diff --git a/app/Service/Import/Importers/HarvestTimeEntriesImporter.php b/app/Service/Import/Importers/HarvestTimeEntriesImporter.php index 55398b89..1a1aade7 100644 --- a/app/Service/Import/Importers/HarvestTimeEntriesImporter.php +++ b/app/Service/Import/Importers/HarvestTimeEntriesImporter.php @@ -107,7 +107,7 @@ public function importData(string $data, string $timezone): void $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; $timeEntry->organization_id = $this->organization->id; - if (strlen($record['Notes']) > 500) { + if (strlen($record['Notes']) > 5000) { throw new ImportException('Time entry note is too long'); } $timeEntry->description = $record['Notes']; diff --git a/app/Service/Import/Importers/SolidtimeImporter.php b/app/Service/Import/Importers/SolidtimeImporter.php index bd0382be..b416c688 100644 --- a/app/Service/Import/Importers/SolidtimeImporter.php +++ b/app/Service/Import/Importers/SolidtimeImporter.php @@ -247,7 +247,7 @@ public function importData(string $data, string $timezone): void $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; $timeEntry->organization_id = $this->organization->id; - if (strlen($timeEntryRow['description']) > 500) { + if (strlen($timeEntryRow['description']) > 5000) { throw new ImportException('Time entry description is too long'); } $timeEntry->description = $timeEntryRow['description']; diff --git a/database/migrations/2025_10_16_000001_extend_time_entry_description.php b/database/migrations/2025_10_16_000001_extend_time_entry_description.php new file mode 100644 index 00000000..5fbaa75b --- /dev/null +++ b/database/migrations/2025_10_16_000001_extend_time_entry_description.php @@ -0,0 +1,30 @@ +string('description', 5000)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('time_entries', function (Blueprint $table): void { + $table->string('description', 500)->change(); + }); + } +}; diff --git a/database/schema/pgsql_test-schema.sql b/database/schema/pgsql_test-schema.sql index 24596c40..9add0f15 100644 --- a/database/schema/pgsql_test-schema.sql +++ b/database/schema/pgsql_test-schema.sql @@ -435,7 +435,7 @@ CREATE TABLE public.tasks ( CREATE TABLE public.time_entries ( id uuid NOT NULL, - description character varying(500) NOT NULL, + description character varying(5000) NOT NULL, start timestamp(0) without time zone NOT NULL, "end" timestamp(0) without time zone, billable_rate integer, From 55d12aaae109ab064f2cd55d243e067e75c99538 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 16 Oct 2025 12:52:21 +0200 Subject: [PATCH 04/38] add discard option for running timer --- e2e/organization.spec.ts | 5 +- e2e/reporting.spec.ts | 15 ++- resources/js/Components/TimeTracker.vue | 117 ++++++++++++++---- resources/js/Pages/Time.vue | 30 +---- .../TimeTrackerMoreOptionsDropdown.vue | 58 +++++++++ 5 files changed, 171 insertions(+), 54 deletions(-) create mode 100644 resources/js/packages/ui/src/TimeTracker/TimeTrackerMoreOptionsDropdown.vue diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index d2386577..78541c25 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -9,7 +9,10 @@ async function goToOrganizationSettings(page) { async function createTimeEntry(page, duration: string) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page.getByTestId('time_entry_description').fill('Test time entry'); diff --git a/e2e/reporting.spec.ts b/e2e/reporting.spec.ts index 0cfa872c..4c3a777d 100644 --- a/e2e/reporting.spec.ts +++ b/e2e/reporting.spec.ts @@ -26,7 +26,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat // Then create the time entry await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page @@ -52,7 +55,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) { await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page @@ -81,7 +87,10 @@ async function createTimeEntryWithBillableStatus( duration: string ) { await goToTimeOverview(page); - await page.getByRole('button', { name: 'Manual time entry' }).click(); + + // Open the dropdown menu and click "Manual time entry" + await page.getByRole('button', { name: 'Time entry actions' }).click(); + await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page diff --git a/resources/js/Components/TimeTracker.vue b/resources/js/Components/TimeTracker.vue index 6498af65..0aec12df 100644 --- a/resources/js/Components/TimeTracker.vue +++ b/resources/js/Components/TimeTracker.vue @@ -16,12 +16,25 @@ import { useProjectsStore } from '@/utils/useProjects'; import { useTasksStore } from '@/utils/useTasks'; import { useTagsStore } from '@/utils/useTags'; import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue'; -import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src'; +import type { + CreateClientBody, + CreateProjectBody, + CreateTimeEntryBody, + Project, + Tag, +} from '@/packages/api/src'; import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue'; +import TimeTrackerMoreOptionsDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerMoreOptionsDropdown.vue'; +import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue'; import { useClientsStore } from '@/utils/useClients'; import { getOrganizationCurrencyString } from '@/utils/money'; import { isAllowedToPerformPremiumAction } from '@/utils/billing'; import { canCreateProjects } from '@/utils/permissions'; +import { ref } from 'vue'; +import { useTimeEntriesStore } from '@/utils/useTimeEntries'; +import { useMutation, useQueryClient } from '@tanstack/vue-query'; +import { api } from '@/packages/api/src'; +import { useNotificationsStore } from '@/utils/notification'; const page = usePage<{ auth: { @@ -47,6 +60,8 @@ const emit = defineEmits<{ change: []; }>(); +const showManualTimeEntryModal = ref(false); + watch(isActive, () => { if (isActive.value) { startLiveTimer(); @@ -93,14 +108,64 @@ function switchToTimeEntryOrganization() { switchOrganization(currentTimeEntry.value.organization_id); } } -async function createTag(tag: string) { +async function createTag(tag: string): Promise { return await useTagsStore().createTag(tag); } +async function createTimeEntry(timeEntry: Omit) { + await useTimeEntriesStore().createTimeEntry(timeEntry); + showManualTimeEntryModal.value = false; +} + +const { handleApiRequestNotifications } = useNotificationsStore(); +const queryClient = useQueryClient(); + +const deleteTimeEntryMutation = useMutation({ + mutationFn: async (timeEntryId: string) => { + const organizationId = getCurrentOrganizationId(); + if (!organizationId) { + throw new Error('No organization selected'); + } + return await api.deleteTimeEntry(undefined, { + params: { + organization: organizationId, + timeEntry: timeEntryId, + }, + }); + }, + onSuccess: async () => { + await currentTimeEntryStore.fetchCurrentTimeEntry(); + await useTimeEntriesStore().fetchTimeEntries(); + queryClient.invalidateQueries({ queryKey: ['timeEntry'] }); + queryClient.invalidateQueries({ queryKey: ['timeEntries'] }); + }, +}); + +async function discardCurrentTimeEntry() { + if (currentTimeEntry.value.id) { + await handleApiRequestNotifications( + () => deleteTimeEntryMutation.mutateAsync(currentTimeEntry.value.id), + 'Time entry discarded successfully', + 'Failed to discard time entry' + ); + } +} + const { tags } = storeToRefs(useTagsStore()); diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index eb89fad9..f555e10d 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -15,8 +15,6 @@ import type { } from '@/packages/api/src'; import { useElementVisibility } from '@vueuse/core'; import { ClockIcon } from '@heroicons/vue/20/solid'; -import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; -import { PlusIcon } from '@heroicons/vue/16/solid'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; import { useTasksStore } from '@/utils/useTasks'; @@ -24,7 +22,6 @@ import { useProjectsStore } from '@/utils/useProjects'; import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue'; import { useTagsStore } from '@/utils/useTags'; import { useClientsStore } from '@/utils/useClients'; -import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue'; import { getOrganizationCurrencyString } from '@/utils/money'; import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue'; import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src'; @@ -73,7 +70,6 @@ onMounted(async () => { await timeEntriesStore.fetchTimeEntries(); }); -const showManualTimeEntryModal = ref(false); const projectStore = useProjectsStore(); const { projects } = storeToRefs(projectStore); const taskStore = useTasksStore(); @@ -105,33 +101,9 @@ function deleteSelected() {