From 8b12dec54651a300d3baf2fdf86619b8c1a1117b Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 21 May 2024 01:53:50 +0200 Subject: [PATCH] change color palette, change user_id to member_id --- .../Api/V1/TimeEntryController.php | 4 +- app/Http/Middleware/ShareInertiaData.php | 1 + config/auth.php | 4 +- e2e/projects.spec.ts | 2 +- e2e/tasks.spec.ts | 2 +- e2e/time.spec.ts | 10 +- e2e/utils/currentTimeEntry.ts | 2 +- openapi.json.client.ts | 391 +++++++++++++++--- package-lock.json | 10 +- package.json | 2 +- resources/css/app.css | 48 ++- resources/js/Components/Common/Badge.vue | 4 +- .../Components/Common/BillableRateInput.vue | 9 +- .../Common/BillableToggleButton.vue | 16 +- .../Common/GroupedItemsCountButton.vue | 37 ++ .../Components/Common/Icons/BillableIcon.vue | 14 + .../Common/Member/MemberCombobox.vue | 33 +- .../Member/MemberMultiselectDropdown.vue | 29 ++ .../Common/Member/MemberTableRow.vue | 2 +- .../Common/Project/ProjectColorSelector.vue | 16 +- .../Common/Project/ProjectCreateModal.vue | 63 +-- .../Common/Project/ProjectEditModal.vue | 4 +- .../Project/ProjectMultiselectDropdown.vue | 29 ++ .../ProjectMemberCreateModal.vue | 7 +- .../ProjectMember/ProjectMemberTableRow.vue | 2 +- .../TimeEntry/TimeEntryAggregateRow.vue | 28 +- .../TimeEntry/TimeEntryDescriptionInput.vue | 2 +- .../TimeEntry/TimeEntryRowTagDropdown.vue | 4 +- .../TimeTrackerProjectTaskDropdown.vue | 231 ++++++----- .../TimeTracker/TimeTrackerTagDropdown.vue | 2 +- .../Common/TimeTrackerStartStop.vue | 2 +- .../Dashboard/ActivityGraphCard.vue | 13 +- .../js/Components/Dashboard/DashboardCard.vue | 2 +- .../Dashboard/DayOverviewCardChart.vue | 7 +- .../Dashboard/ProjectsChartCard.vue | 22 +- .../Components/Dashboard/ThisWeekOverview.vue | 33 +- resources/js/Components/DialogModal.vue | 2 +- resources/js/Components/Dropdown.vue | 13 +- resources/js/Components/Modal.vue | 2 +- resources/js/Components/SecondaryButton.vue | 2 +- resources/js/Components/TextInput.vue | 2 +- resources/js/Components/TimeTracker.vue | 6 +- resources/js/Pages/Tags.vue | 5 +- resources/js/Pages/Time.vue | 2 - resources/js/types/models.ts | 3 + resources/js/utils/notification.ts | 2 +- resources/js/utils/useCurrentTimeEntry.ts | 12 +- resources/js/utils/useMembers.ts | 2 +- resources/js/utils/useTimeEntries.ts | 24 +- resources/js/utils/useUser.ts | 7 + tailwind.config.js | 53 +-- 51 files changed, 836 insertions(+), 388 deletions(-) create mode 100644 resources/js/Components/Common/GroupedItemsCountButton.vue create mode 100644 resources/js/Components/Common/Icons/BillableIcon.vue create mode 100644 resources/js/Components/Common/Member/MemberMultiselectDropdown.vue create mode 100644 resources/js/Components/Common/Project/ProjectMultiselectDropdown.vue diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index 3d4d0f5e..22cc7c8c 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -224,7 +224,7 @@ class TimeEntryController extends Controller /** @var string $group2Type */ $group2Response[] = [ 'type' => $group2Type, - 'key' => $group2 === '' ? null : $group2, + 'key' => $group2 === '' ? null : (string) $group2, 'seconds' => (int) $aggregate->get(0)->aggregate, 'cost' => (int) $aggregate->get(0)->cost, ]; @@ -241,7 +241,7 @@ class TimeEntryController extends Controller /** @var string $group1Type */ $group1Response[] = [ 'type' => $group1Type, - 'key' => $group1 === '' ? null : $group1, + 'key' => $group1 === '' ? null : (string) $group1, 'seconds' => $group2ResponseSum, 'cost' => $group2ResponseCost, 'grouped_data' => $group2Response, diff --git a/app/Http/Middleware/ShareInertiaData.php b/app/Http/Middleware/ShareInertiaData.php index cbe154f3..e5c3cfba 100644 --- a/app/Http/Middleware/ShareInertiaData.php +++ b/app/Http/Middleware/ShareInertiaData.php @@ -85,6 +85,7 @@ class ShareInertiaData 'currency' => $organization->currency, 'membership' => [ 'role' => $organization->membership->role, + 'id' => $organization->membership->id, ], ]; })->all(), diff --git a/config/auth.php b/config/auth.php index 6ac82835..8143c2ee 100644 --- a/config/auth.php +++ b/config/auth.php @@ -117,9 +117,9 @@ return [ 'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')), - 'terms_url' => env('TERMS_URL'), + 'terms_url' => env('TERMS_URL', ''), - 'privacy_policy_url' => env('PRIVACY_POLICY_URL'), + 'privacy_policy_url' => env('PRIVACY_POLICY_URL', ''), 'newsletter_consent' => env('NEWSLETTER_CONSENT', false), diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index 937ce670..797963a1 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -14,7 +14,7 @@ test('test that creating and deleting a new project via the modal works', async 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); - await page.getByPlaceholder('Project Name').fill(newProjectName); + await page.getByLabel('Project Name').fill(newProjectName); await Promise.all([ page.getByRole('button', { name: 'Create Project' }).nth(1).click(), page.waitForResponse( diff --git a/e2e/tasks.spec.ts b/e2e/tasks.spec.ts index 4a8196b4..54b7be78 100644 --- a/e2e/tasks.spec.ts +++ b/e2e/tasks.spec.ts @@ -14,7 +14,7 @@ test('test that creating and deleting a new tag in a new project works', async ( 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); - await page.getByPlaceholder('Project Name').fill(newProjectName); + await page.getByLabel('Project Name').fill(newProjectName); await Promise.all([ page.getByRole('button', { name: 'Create Project' }).nth(1).click(), page.waitForResponse( diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index ee63ea75..1de2fb11 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -57,7 +57,7 @@ test('test that starting and stopping an empty time entry shows a new time entry async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) { await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-accent-300\/50/ + /bg-accent-300\/70/ ); } @@ -297,7 +297,7 @@ test('test that stopping a time entry from the overview works', async ({ ]); await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-accent-300\/50/ + /bg-accent-300\/70/ ); }); @@ -311,7 +311,7 @@ test('test that starting a time entry from the overview works', async ({ const newTimeEntry = timeEntryRows.first(); const startButton = newTimeEntry.getByTestId('timer_button'); - await expect(startButton).toHaveClass(/bg-accent-300\/50/); + await expect(startButton).toHaveClass(/bg-accent-300\/70/); await Promise.all([ page.waitForResponse(async (response) => { @@ -341,7 +341,7 @@ test('test that starting a time entry from the overview works', async ({ ); }), startOrStopTimerWithButton(page), - expect(startButton).toHaveClass(/bg-accent-300\/50/), + expect(startButton).toHaveClass(/bg-accent-300\/70/), ]); }); @@ -401,7 +401,7 @@ test('test that updating a the duration in the overview for a running timer work ); }), startOrStopTimerWithButton(page), - expect(startButton).toHaveClass(/bg-accent-300\/50/), + expect(startButton).toHaveClass(/bg-accent-300\/70/), ]); }); diff --git a/e2e/utils/currentTimeEntry.ts b/e2e/utils/currentTimeEntry.ts index a616552f..f73faa68 100644 --- a/e2e/utils/currentTimeEntry.ts +++ b/e2e/utils/currentTimeEntry.ts @@ -40,7 +40,7 @@ export async function assertThatTimerIsStopped(page: Page) { page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"]' ) - ).toHaveClass(/bg-accent-300\/50/); + ).toHaveClass(/bg-accent-300\/70/); } export async function stoppedTimeEntryResponse( diff --git a/openapi.json.client.ts b/openapi.json.client.ts index f876523b..e6fafb4b 100644 --- a/openapi.json.client.ts +++ b/openapi.json.client.ts @@ -83,13 +83,13 @@ const ProjectMemberResource = z .object({ id: z.string(), billable_rate: z.union([z.number(), z.null()]), - user_id: z.string(), + member_id: z.string(), project_id: z.string(), }) .passthrough(); const createProjectMember_Body = z .object({ - user_id: z.string().uuid(), + member_id: z.string().uuid(), billable_rate: z.union([z.number(), z.null()]).optional(), }) .passthrough(); @@ -137,7 +137,7 @@ const TimeEntryResource = z const TimeEntryCollection = z.array(TimeEntryResource); const createTimeEntry_Body = z .object({ - user_id: z.string().uuid(), + member_id: z.string().uuid(), project_id: z.union([z.string(), z.null()]).optional(), task_id: z.union([z.string(), z.null()]).optional(), start: z.string(), @@ -147,8 +147,41 @@ const createTimeEntry_Body = z tags: z.union([z.array(z.string()), z.null()]).optional(), }) .passthrough(); +const v1_time_entries_update_multiple_Body = z + .object({ + ids: z.array(z.string()), + changes: z + .object({ + member_id: z.string().uuid(), + project_id: z.union([z.string(), z.null()]), + task_id: z.union([z.string(), z.null()]), + billable: z.boolean(), + description: z.union([z.string(), z.null()]), + tags: z.union([z.array(z.string()), z.null()]), + }) + .partial() + .passthrough(), + }) + .passthrough(); +const group = z + .union([ + z.enum([ + 'day', + 'week', + 'month', + 'year', + 'user', + 'project', + 'task', + 'client', + 'billable', + ]), + z.null(), + ]) + .optional(); const updateTimeEntry_Body = z .object({ + member_id: z.string().uuid().optional(), project_id: z.union([z.string(), z.null()]).optional(), task_id: z.union([z.string(), z.null()]).optional(), start: z.string(), @@ -184,6 +217,8 @@ export const schemas = { TimeEntryResource, TimeEntryCollection, createTimeEntry_Body, + v1_time_entries_update_multiple_Body, + group, updateTimeEntry_Body, }; @@ -197,7 +232,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: OrganizationResource }).passthrough(), @@ -228,7 +263,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: OrganizationResource }).passthrough(), @@ -264,7 +299,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ClientCollection }).passthrough(), @@ -295,7 +330,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ClientResource }).passthrough(), @@ -336,12 +371,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'client', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ClientResource }).passthrough(), @@ -382,12 +417,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'client', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -429,7 +464,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z @@ -497,7 +532,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z @@ -535,7 +570,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z @@ -608,7 +643,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -649,12 +684,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'invitation', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -685,12 +720,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'invitation', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -716,7 +751,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z @@ -777,7 +812,7 @@ const endpoints = makeApi([ }, { method: 'put', - path: '/v1/organizations/:organization/members/:membership', + path: '/v1/organizations/:organization/members/:member', alias: 'updateMember', requestFormat: 'json', parameters: [ @@ -789,12 +824,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { - name: 'membership', + name: 'member', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: MemberResource }).passthrough(), @@ -823,7 +858,7 @@ const endpoints = makeApi([ }, { method: 'delete', - path: '/v1/organizations/:organization/members/:membership', + path: '/v1/organizations/:organization/members/:member', alias: 'removeMember', requestFormat: 'json', parameters: [ @@ -835,12 +870,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { - name: 'membership', + name: 'member', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -870,7 +905,7 @@ const endpoints = makeApi([ }, { method: 'post', - path: '/v1/organizations/:organization/members/:membership/invite-placeholder', + path: '/v1/organizations/:organization/members/:member/invite-placeholder', alias: 'invitePlaceholder', requestFormat: 'json', parameters: [ @@ -882,12 +917,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { - name: 'membership', + name: 'member', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -929,12 +964,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'projectMember', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ProjectMemberResource }).passthrough(), @@ -975,12 +1010,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'projectMember', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -1006,7 +1041,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), + }, + { + name: 'page', + type: 'Query', + schema: z.number().int().gte(1).optional(), }, ], response: z @@ -1053,6 +1093,16 @@ const endpoints = makeApi([ description: `Not found`, schema: z.object({ message: z.string() }).passthrough(), }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, ], }, { @@ -1069,7 +1119,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ProjectResource }).passthrough(), @@ -1105,12 +1155,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ProjectResource }).passthrough(), @@ -1141,12 +1191,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ProjectResource }).passthrough(), @@ -1187,12 +1237,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -1229,12 +1279,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z @@ -1297,12 +1347,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: ProjectMemberResource }).passthrough(), @@ -1349,7 +1399,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TagCollection }).passthrough(), @@ -1380,7 +1430,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TagResource }).passthrough(), @@ -1421,12 +1471,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'tag', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TagResource }).passthrough(), @@ -1467,12 +1517,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'tag', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -1509,7 +1559,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'project_id', @@ -1587,7 +1637,7 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TaskResource }).passthrough(), @@ -1628,12 +1678,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'task', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TaskResource }).passthrough(), @@ -1674,12 +1724,12 @@ const endpoints = makeApi([ { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'task', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -1718,10 +1768,10 @@ Users with the permission `time-entries:view:own` can only use this en { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { - name: 'user_id', + name: 'member_id', type: 'Query', schema: z.string().uuid().optional(), }, @@ -1740,6 +1790,11 @@ Users with the permission `time-entries:view:own` can only use this en type: 'Query', schema: z.enum(['true', 'false']).optional(), }, + { + name: 'billable', + type: 'Query', + schema: z.enum(['true', 'false']).optional(), + }, { name: 'limit', type: 'Query', @@ -1750,6 +1805,26 @@ Users with the permission `time-entries:view:own` can only use this en type: 'Query', schema: z.enum(['true', 'false']).optional(), }, + { + name: 'member_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'project_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'tag_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'task_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, ], response: z.object({ data: TimeEntryCollection }).passthrough(), errors: [ @@ -1789,7 +1864,7 @@ Users with the permission `time-entries:view:own` can only use this en { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TimeEntryResource }).passthrough(), @@ -1827,6 +1902,49 @@ Users with the permission `time-entries:view:own` can only use this en }, ], }, + { + method: 'patch', + path: '/v1/organizations/:organization/time-entries', + alias: 'v1.time-entries.update-multiple', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: v1_time_entries_update_multiple_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string(), + }, + ], + response: z + .object({ success: z.string(), error: z.string() }) + .passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, { method: 'put', path: '/v1/organizations/:organization/time-entries/:timeEntry', @@ -1841,12 +1959,12 @@ Users with the permission `time-entries:view:own` can only use this en { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'timeEntry', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.object({ data: TimeEntryResource }).passthrough(), @@ -1898,12 +2016,12 @@ Users with the permission `time-entries:view:own` can only use this en { name: 'organization', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, { name: 'timeEntry', type: 'Path', - schema: z.string().uuid(), + schema: z.string(), }, ], response: z.null(), @@ -1920,6 +2038,145 @@ Users with the permission `time-entries:view:own` can only use this en }, ], }, + { + method: 'get', + path: '/v1/organizations/:organization/time-entries/aggregate', + alias: 'getAggregatedTimeEntries', + description: `This endpoint allows you to filter time entries and aggregate them by different criteria. +The parameters `group` and `sub_group` allow you to group the time entries by different criteria. +If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.`, + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string(), + }, + { + name: 'group', + type: 'Query', + schema: group, + }, + { + name: 'sub_group', + type: 'Query', + schema: group, + }, + { + name: 'member_id', + type: 'Query', + schema: z.string().uuid().optional(), + }, + { + name: 'user_id', + type: 'Query', + schema: z.string().uuid().optional(), + }, + { + name: 'before', + type: 'Query', + schema: before, + }, + { + name: 'after', + type: 'Query', + schema: before, + }, + { + name: 'active', + type: 'Query', + schema: z.enum(['true', 'false']).optional(), + }, + { + name: 'billable', + type: 'Query', + schema: z.enum(['true', 'false']).optional(), + }, + { + name: 'member_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'project_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'tag_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + { + name: 'task_ids', + type: 'Query', + schema: z.array(z.string()).min(1).optional(), + }, + ], + response: z + .object({ + data: z + .object({ + grouped_data: z.union([ + z.array( + z + .object({ + type: z.string(), + key: z.union([z.string(), z.null()]), + seconds: z.number().int(), + cost: z.number().int(), + grouped_data: z.union([ + z.array( + z + .object({ + type: z.string(), + key: z.union([ + z.string(), + z.null(), + ]), + seconds: z + .number() + .int(), + cost: z.number().int(), + }) + .passthrough() + ), + z.null(), + ]), + }) + .passthrough() + ), + z.null(), + ]), + seconds: z.number().int(), + cost: z.number().int(), + }) + .passthrough(), + }) + .passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, { method: 'get', path: '/v1/users/me/time-entries/active', diff --git a/package-lock.json b/package-lock.json index 508cfc1a..3175d14d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "html", + "name": "solidtime", "lockfileVersion": 3, "requires": true, "packages": { @@ -19,7 +19,7 @@ "pinia": "^2.1.7", "radix-vue": "^1.5.2", "tailwind-merge": "^2.2.1", - "vue-echarts": "^6.6.9" + "vue-echarts": "^6.7.2" }, "devDependencies": { "@inertiajs/vue3": "^1.0.0", @@ -5735,9 +5735,9 @@ } }, "node_modules/vue-echarts": { - "version": "6.6.9", - "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.6.9.tgz", - "integrity": "sha512-mojIq3ZvsjabeVmDthhAUDV8Kgf2Rr/X4lV4da7gEFd1fP05gcSJ0j7wa7HQkW5LlFmF2gdCJ8p4Chas6NNIQQ==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.2.tgz", + "integrity": "sha512-SG8Vmszhx24KjtySsk361DogZLRkPCyLhgoyh7iN1eH3WGJ0kyl3k0g4QiSJqK0+F1Ej0HDopq4A5OGcBlAwzw==", "hasInstallScript": true, "dependencies": { "resize-detector": "^0.3.0", diff --git a/package.json b/package.json index 835beac3..ab99128c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,6 @@ "pinia": "^2.1.7", "radix-vue": "^1.5.2", "tailwind-merge": "^2.2.1", - "vue-echarts": "^6.6.9" + "vue-echarts": "^6.7.2" } } diff --git a/resources/css/app.css b/resources/css/app.css index 8236cc91..c055ca05 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2,16 +2,40 @@ @tailwind components; @tailwind utilities; +:root { + --color-bg-primary: #0f1011; + --color-bg-secondary: #1b1c20; + --color-bg-tertiary: #2A2C32; + --color-bg-quaternary: #141518; + --color-text-primary: #ffffff; + --color-text-secondary: #e3e4e6; + --color-text-tertiary: #969799; + --color-text-quaternary: #595a5c; + --color-border-primary: #191b1f; + --color-border-secondary: #23252a; + --color-border-tertiary: #2c2e33; + --color-border-quaternary: #393B42; + --color-input-border-active: rgba(255,255,255,0.3); -:root{ - --theme-color-default-background: #0b0d1c; - --theme-color-icon-default: #42466C; - --theme-color-card-background: #13152B; - --theme-color-card-background-active: #1C1E34; - --theme-color-card-background-separator: #1c2033; - --theme-color-card-border: #1c2033; - --theme-color-card-border-active: #2A3461; - --theme-color-default-background-separator: #141a2f; + --color-accent-primary: 14, 165, 233; /* sky-500 */ + --color-accent-secondary: 56, 189, 248; + --color-accent-tertiary: 125, 211, 252; + --color-accent-quaternary: 186, 230, 253; + + --theme-color-default-background: var(--color-bg-primary); + --theme-color-icon-default: var(--color-text-tertiary); + --theme-color-icon-active: rgb(var(--color-text-tertiary)); + --theme-color-card-background: var(--color-bg-secondary); + --theme-color-card-background-active: var(--color-bg-tertiary); + --theme-color-card-background-separator: var(--color-border-quaternary); + --theme-color-card-border: var(--color-border-secondary); + --theme-color-card-border-active: var(--color-border-tertiary); + --theme-color-default-background-separator: var(--color-border-primary); + --theme-color-primary-text: var(--color-text-primary); + --theme-color-muted-text: var(--color-text-secondary); + --theme-color-menu-active: var(--color-bg-secondary); + --theme-color-input-border: var(--color-border-quaternary); + --theme-color-input-background: var(--color-bg-secondary); --theme-color-tab-background: var(--theme-color-card-background); --theme-color-tab-background-active: var(--theme-color-card-background-active); --theme-color-tab-border: var(--theme-color-card-border); @@ -21,17 +45,15 @@ --theme-color-row-heading-border: var(--theme-color-card-border); } -*{ +* { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - [x-cloak] { display: none; } - -body{ +body { background-color: var(--theme-color-default-background); } diff --git a/resources/js/Components/Common/Badge.vue b/resources/js/Components/Common/Badge.vue index 52c32983..1bb6d429 100644 --- a/resources/js/Components/Common/Badge.vue +++ b/resources/js/Components/Common/Badge.vue @@ -37,10 +37,10 @@ const borderClasses = computed(() => { :is="tag" :class=" twMerge( - props.class, badgeClasses[size], borderClasses, - 'rounded inline-flex items-center font-semibold text-white' + 'rounded inline-flex items-center font-semibold text-white', + props.class ) "> diff --git a/resources/js/Components/Common/BillableRateInput.vue b/resources/js/Components/Common/BillableRateInput.vue index 7bba4cce..f43277da 100644 --- a/resources/js/Components/Common/BillableRateInput.vue +++ b/resources/js/Components/Common/BillableRateInput.vue @@ -6,6 +6,10 @@ import { getOrganizationCurrencySymbol, } from '../../utils/money'; +defineProps<{ + name: string; +}>(); + const model = defineModel({ default: null, type: Number, @@ -51,13 +55,14 @@ function formatCents(modelValue: number) { diff --git a/resources/js/Components/Common/GroupedItemsCountButton.vue b/resources/js/Components/Common/GroupedItemsCountButton.vue new file mode 100644 index 00000000..1a8d66e9 --- /dev/null +++ b/resources/js/Components/Common/GroupedItemsCountButton.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/resources/js/Components/Common/Icons/BillableIcon.vue b/resources/js/Components/Common/Icons/BillableIcon.vue new file mode 100644 index 00000000..e2a32170 --- /dev/null +++ b/resources/js/Components/Common/Icons/BillableIcon.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/resources/js/Components/Common/Member/MemberCombobox.vue b/resources/js/Components/Common/Member/MemberCombobox.vue index 83714877..8a40ed54 100644 --- a/resources/js/Components/Common/Member/MemberCombobox.vue +++ b/resources/js/Components/Common/Member/MemberCombobox.vue @@ -37,7 +37,7 @@ const filteredMembers = computed(() => { .toLowerCase() .includes(searchValue.value?.toLowerCase()?.trim() || '') && !props.hiddenMembers.some( - (hiddenMember) => hiddenMember.user_id === member.user_id + (hiddenMember) => hiddenMember.id === member.id ) && member.is_placeholder === false ); @@ -54,7 +54,7 @@ onMounted(() => { function resetHighlightedItem() { if (filteredMembers.value.length > 0) { - highlightedItemId.value = filteredMembers.value[0].user_id; + highlightedItemId.value = filteredMembers.value[0].id; } } @@ -65,10 +65,10 @@ function updateSearchValue(event: Event) { const highlightedClientId = highlightedItemId.value; if (highlightedClientId) { const highlightedClient = members.value.find( - (member) => member.user_id === highlightedClientId + (member) => member.id === highlightedClientId ); if (highlightedClient) { - model.value = highlightedClient.user_id; + model.value = highlightedClient.id; } } } else { @@ -94,10 +94,10 @@ function moveHighlightUp() { ); if (currentHightlightedIndex === 0) { highlightedItemId.value = - filteredMembers.value[filteredMembers.value.length - 1].user_id; + filteredMembers.value[filteredMembers.value.length - 1].id; } else { highlightedItemId.value = - filteredMembers.value[currentHightlightedIndex - 1].user_id; + filteredMembers.value[currentHightlightedIndex - 1].id; } } } @@ -108,10 +108,10 @@ function moveHighlightDown() { highlightedItem.value ); if (currentHightlightedIndex === filteredMembers.value.length - 1) { - highlightedItemId.value = filteredMembers.value[0].user_id; + highlightedItemId.value = filteredMembers.value[0].id; } else { highlightedItemId.value = - filteredMembers.value[currentHightlightedIndex + 1].user_id; + filteredMembers.value[currentHightlightedIndex + 1].id; } } } @@ -119,14 +119,13 @@ function moveHighlightDown() { const highlightedItemId = ref(null); const highlightedItem = computed(() => { return members.value.find( - (member) => member.user_id === highlightedItemId.value + (member) => member.id === highlightedItemId.value ); }); const currentValue = computed(() => { if (model.value) { - return members.value.find((member) => member.user_id === model.value) - ?.name; + return members.value.find((member) => member.id === model.value)?.name; } return searchValue.value; }); @@ -186,18 +185,18 @@ function onUnfocus() {
+ :data-client-id="member.id">
diff --git a/resources/js/Components/Common/Member/MemberMultiselectDropdown.vue b/resources/js/Components/Common/Member/MemberMultiselectDropdown.vue new file mode 100644 index 00000000..5d332749 --- /dev/null +++ b/resources/js/Components/Common/Member/MemberMultiselectDropdown.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/js/Components/Common/Member/MemberTableRow.vue b/resources/js/Components/Common/Member/MemberTableRow.vue index 56c4d3ab..dc4d64a4 100644 --- a/resources/js/Components/Common/Member/MemberTableRow.vue +++ b/resources/js/Components/Common/Member/MemberTableRow.vue @@ -29,7 +29,7 @@ async function invitePlaceholder(id: string) { { params: { organization: organizationId, - membership: id, + member: id, }, } ), diff --git a/resources/js/Components/Common/Project/ProjectColorSelector.vue b/resources/js/Components/Common/Project/ProjectColorSelector.vue index afa1f2c4..4b8fc1de 100644 --- a/resources/js/Components/Common/Project/ProjectColorSelector.vue +++ b/resources/js/Components/Common/Project/ProjectColorSelector.vue @@ -6,15 +6,17 @@ const model = defineModel({ default: '' });