mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
change color palette, change user_id to member_id
This commit is contained in:
@@ -224,7 +224,7 @@ class TimeEntryController extends Controller
|
|||||||
/** @var string $group2Type */
|
/** @var string $group2Type */
|
||||||
$group2Response[] = [
|
$group2Response[] = [
|
||||||
'type' => $group2Type,
|
'type' => $group2Type,
|
||||||
'key' => $group2 === '' ? null : $group2,
|
'key' => $group2 === '' ? null : (string) $group2,
|
||||||
'seconds' => (int) $aggregate->get(0)->aggregate,
|
'seconds' => (int) $aggregate->get(0)->aggregate,
|
||||||
'cost' => (int) $aggregate->get(0)->cost,
|
'cost' => (int) $aggregate->get(0)->cost,
|
||||||
];
|
];
|
||||||
@@ -241,7 +241,7 @@ class TimeEntryController extends Controller
|
|||||||
/** @var string $group1Type */
|
/** @var string $group1Type */
|
||||||
$group1Response[] = [
|
$group1Response[] = [
|
||||||
'type' => $group1Type,
|
'type' => $group1Type,
|
||||||
'key' => $group1 === '' ? null : $group1,
|
'key' => $group1 === '' ? null : (string) $group1,
|
||||||
'seconds' => $group2ResponseSum,
|
'seconds' => $group2ResponseSum,
|
||||||
'cost' => $group2ResponseCost,
|
'cost' => $group2ResponseCost,
|
||||||
'grouped_data' => $group2Response,
|
'grouped_data' => $group2Response,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class ShareInertiaData
|
|||||||
'currency' => $organization->currency,
|
'currency' => $organization->currency,
|
||||||
'membership' => [
|
'membership' => [
|
||||||
'role' => $organization->membership->role,
|
'role' => $organization->membership->role,
|
||||||
|
'id' => $organization->membership->id,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
})->all(),
|
})->all(),
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ return [
|
|||||||
|
|
||||||
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
|
'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),
|
'newsletter_consent' => env('NEWSLETTER_CONSENT', false),
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
'New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||||
await goToProjectsOverview(page);
|
await goToProjectsOverview(page);
|
||||||
await page.getByRole('button', { name: 'Create Project' }).click();
|
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([
|
await Promise.all([
|
||||||
page.getByRole('button', { name: 'Create Project' }).nth(1).click(),
|
page.getByRole('button', { name: 'Create Project' }).nth(1).click(),
|
||||||
page.waitForResponse(
|
page.waitForResponse(
|
||||||
|
|||||||
@@ -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);
|
'New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||||
await goToProjectsOverview(page);
|
await goToProjectsOverview(page);
|
||||||
await page.getByRole('button', { name: 'Create Project' }).click();
|
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([
|
await Promise.all([
|
||||||
page.getByRole('button', { name: 'Create Project' }).nth(1).click(),
|
page.getByRole('button', { name: 'Create Project' }).nth(1).click(),
|
||||||
page.waitForResponse(
|
page.waitForResponse(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ test('test that starting and stopping an empty time entry shows a new time entry
|
|||||||
|
|
||||||
async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) {
|
async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) {
|
||||||
await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(
|
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(
|
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 newTimeEntry = timeEntryRows.first();
|
||||||
const startButton = newTimeEntry.getByTestId('timer_button');
|
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([
|
await Promise.all([
|
||||||
page.waitForResponse(async (response) => {
|
page.waitForResponse(async (response) => {
|
||||||
@@ -341,7 +341,7 @@ test('test that starting a time entry from the overview works', async ({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
startOrStopTimerWithButton(page),
|
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),
|
startOrStopTimerWithButton(page),
|
||||||
expect(startButton).toHaveClass(/bg-accent-300\/50/),
|
expect(startButton).toHaveClass(/bg-accent-300\/70/),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export async function assertThatTimerIsStopped(page: Page) {
|
|||||||
page.locator(
|
page.locator(
|
||||||
'[data-testid="dashboard_timer"] [data-testid="timer_button"]'
|
'[data-testid="dashboard_timer"] [data-testid="timer_button"]'
|
||||||
)
|
)
|
||||||
).toHaveClass(/bg-accent-300\/50/);
|
).toHaveClass(/bg-accent-300\/70/);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stoppedTimeEntryResponse(
|
export async function stoppedTimeEntryResponse(
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ const ProjectMemberResource = z
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
billable_rate: z.union([z.number(), z.null()]),
|
billable_rate: z.union([z.number(), z.null()]),
|
||||||
user_id: z.string(),
|
member_id: z.string(),
|
||||||
project_id: z.string(),
|
project_id: z.string(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const createProjectMember_Body = z
|
const createProjectMember_Body = z
|
||||||
.object({
|
.object({
|
||||||
user_id: z.string().uuid(),
|
member_id: z.string().uuid(),
|
||||||
billable_rate: z.union([z.number(), z.null()]).optional(),
|
billable_rate: z.union([z.number(), z.null()]).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -137,7 +137,7 @@ const TimeEntryResource = z
|
|||||||
const TimeEntryCollection = z.array(TimeEntryResource);
|
const TimeEntryCollection = z.array(TimeEntryResource);
|
||||||
const createTimeEntry_Body = z
|
const createTimeEntry_Body = z
|
||||||
.object({
|
.object({
|
||||||
user_id: z.string().uuid(),
|
member_id: z.string().uuid(),
|
||||||
project_id: z.union([z.string(), z.null()]).optional(),
|
project_id: z.union([z.string(), z.null()]).optional(),
|
||||||
task_id: z.union([z.string(), z.null()]).optional(),
|
task_id: z.union([z.string(), z.null()]).optional(),
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
@@ -147,8 +147,41 @@ const createTimeEntry_Body = z
|
|||||||
tags: z.union([z.array(z.string()), z.null()]).optional(),
|
tags: z.union([z.array(z.string()), z.null()]).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.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
|
const updateTimeEntry_Body = z
|
||||||
.object({
|
.object({
|
||||||
|
member_id: z.string().uuid().optional(),
|
||||||
project_id: z.union([z.string(), z.null()]).optional(),
|
project_id: z.union([z.string(), z.null()]).optional(),
|
||||||
task_id: z.union([z.string(), z.null()]).optional(),
|
task_id: z.union([z.string(), z.null()]).optional(),
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
@@ -184,6 +217,8 @@ export const schemas = {
|
|||||||
TimeEntryResource,
|
TimeEntryResource,
|
||||||
TimeEntryCollection,
|
TimeEntryCollection,
|
||||||
createTimeEntry_Body,
|
createTimeEntry_Body,
|
||||||
|
v1_time_entries_update_multiple_Body,
|
||||||
|
group,
|
||||||
updateTimeEntry_Body,
|
updateTimeEntry_Body,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,7 +232,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: OrganizationResource }).passthrough(),
|
response: z.object({ data: OrganizationResource }).passthrough(),
|
||||||
@@ -228,7 +263,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: OrganizationResource }).passthrough(),
|
response: z.object({ data: OrganizationResource }).passthrough(),
|
||||||
@@ -264,7 +299,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ClientCollection }).passthrough(),
|
response: z.object({ data: ClientCollection }).passthrough(),
|
||||||
@@ -295,7 +330,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ClientResource }).passthrough(),
|
response: z.object({ data: ClientResource }).passthrough(),
|
||||||
@@ -336,12 +371,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'client',
|
name: 'client',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ClientResource }).passthrough(),
|
response: z.object({ data: ClientResource }).passthrough(),
|
||||||
@@ -382,12 +417,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'client',
|
name: 'client',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -429,7 +464,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -497,7 +532,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -535,7 +570,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -608,7 +643,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -649,12 +684,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invitation',
|
name: 'invitation',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -685,12 +720,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'invitation',
|
name: 'invitation',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -716,7 +751,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -777,7 +812,7 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/v1/organizations/:organization/members/:membership',
|
path: '/v1/organizations/:organization/members/:member',
|
||||||
alias: 'updateMember',
|
alias: 'updateMember',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
@@ -789,12 +824,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'membership',
|
name: 'member',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: MemberResource }).passthrough(),
|
response: z.object({ data: MemberResource }).passthrough(),
|
||||||
@@ -823,7 +858,7 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: '/v1/organizations/:organization/members/:membership',
|
path: '/v1/organizations/:organization/members/:member',
|
||||||
alias: 'removeMember',
|
alias: 'removeMember',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
@@ -835,12 +870,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'membership',
|
name: 'member',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -870,7 +905,7 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/v1/organizations/:organization/members/:membership/invite-placeholder',
|
path: '/v1/organizations/:organization/members/:member/invite-placeholder',
|
||||||
alias: 'invitePlaceholder',
|
alias: 'invitePlaceholder',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
@@ -882,12 +917,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'membership',
|
name: 'member',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -929,12 +964,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'projectMember',
|
name: 'projectMember',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ProjectMemberResource }).passthrough(),
|
response: z.object({ data: ProjectMemberResource }).passthrough(),
|
||||||
@@ -975,12 +1010,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'projectMember',
|
name: 'projectMember',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -1006,7 +1041,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.number().int().gte(1).optional(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -1053,6 +1093,16 @@ const endpoints = makeApi([
|
|||||||
description: `Not found`,
|
description: `Not found`,
|
||||||
schema: z.object({ message: z.string() }).passthrough(),
|
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',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ProjectResource }).passthrough(),
|
response: z.object({ data: ProjectResource }).passthrough(),
|
||||||
@@ -1105,12 +1155,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ProjectResource }).passthrough(),
|
response: z.object({ data: ProjectResource }).passthrough(),
|
||||||
@@ -1141,12 +1191,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ProjectResource }).passthrough(),
|
response: z.object({ data: ProjectResource }).passthrough(),
|
||||||
@@ -1187,12 +1237,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -1229,12 +1279,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
@@ -1297,12 +1347,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: ProjectMemberResource }).passthrough(),
|
response: z.object({ data: ProjectMemberResource }).passthrough(),
|
||||||
@@ -1349,7 +1399,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TagCollection }).passthrough(),
|
response: z.object({ data: TagCollection }).passthrough(),
|
||||||
@@ -1380,7 +1430,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TagResource }).passthrough(),
|
response: z.object({ data: TagResource }).passthrough(),
|
||||||
@@ -1421,12 +1471,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag',
|
name: 'tag',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TagResource }).passthrough(),
|
response: z.object({ data: TagResource }).passthrough(),
|
||||||
@@ -1467,12 +1517,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag',
|
name: 'tag',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -1509,7 +1559,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'project_id',
|
name: 'project_id',
|
||||||
@@ -1587,7 +1637,7 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TaskResource }).passthrough(),
|
response: z.object({ data: TaskResource }).passthrough(),
|
||||||
@@ -1628,12 +1678,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'task',
|
name: 'task',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TaskResource }).passthrough(),
|
response: z.object({ data: TaskResource }).passthrough(),
|
||||||
@@ -1674,12 +1724,12 @@ const endpoints = makeApi([
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'task',
|
name: 'task',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
@@ -1718,10 +1768,10 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user_id',
|
name: 'member_id',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.string().uuid().optional(),
|
schema: z.string().uuid().optional(),
|
||||||
},
|
},
|
||||||
@@ -1740,6 +1790,11 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.enum(['true', 'false']).optional(),
|
schema: z.enum(['true', 'false']).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'billable',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.enum(['true', 'false']).optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'limit',
|
name: 'limit',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
@@ -1750,6 +1805,26 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.enum(['true', 'false']).optional(),
|
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(),
|
response: z.object({ data: TimeEntryCollection }).passthrough(),
|
||||||
errors: [
|
errors: [
|
||||||
@@ -1789,7 +1864,7 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TimeEntryResource }).passthrough(),
|
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',
|
method: 'put',
|
||||||
path: '/v1/organizations/:organization/time-entries/:timeEntry',
|
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',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeEntry',
|
name: 'timeEntry',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.object({ data: TimeEntryResource }).passthrough(),
|
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',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeEntry',
|
name: 'timeEntry',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
schema: z.string().uuid(),
|
schema: z.string(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
response: z.null(),
|
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',
|
method: 'get',
|
||||||
path: '/v1/users/me/time-entries/active',
|
path: '/v1/users/me/time-entries/active',
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "solidtime",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"radix-vue": "^1.5.2",
|
"radix-vue": "^1.5.2",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"vue-echarts": "^6.6.9"
|
"vue-echarts": "^6.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inertiajs/vue3": "^1.0.0",
|
"@inertiajs/vue3": "^1.0.0",
|
||||||
@@ -5735,9 +5735,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-echarts": {
|
"node_modules/vue-echarts": {
|
||||||
"version": "6.6.9",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.2.tgz",
|
||||||
"integrity": "sha512-mojIq3ZvsjabeVmDthhAUDV8Kgf2Rr/X4lV4da7gEFd1fP05gcSJ0j7wa7HQkW5LlFmF2gdCJ8p4Chas6NNIQQ==",
|
"integrity": "sha512-SG8Vmszhx24KjtySsk361DogZLRkPCyLhgoyh7iN1eH3WGJ0kyl3k0g4QiSJqK0+F1Ej0HDopq4A5OGcBlAwzw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resize-detector": "^0.3.0",
|
"resize-detector": "^0.3.0",
|
||||||
|
|||||||
@@ -47,6 +47,6 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"radix-vue": "^1.5.2",
|
"radix-vue": "^1.5.2",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"vue-echarts": "^6.6.9"
|
"vue-echarts": "^6.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,40 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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{
|
--color-accent-primary: 14, 165, 233; /* sky-500 */
|
||||||
--theme-color-default-background: #0b0d1c;
|
--color-accent-secondary: 56, 189, 248;
|
||||||
--theme-color-icon-default: #42466C;
|
--color-accent-tertiary: 125, 211, 252;
|
||||||
--theme-color-card-background: #13152B;
|
--color-accent-quaternary: 186, 230, 253;
|
||||||
--theme-color-card-background-active: #1C1E34;
|
|
||||||
--theme-color-card-background-separator: #1c2033;
|
--theme-color-default-background: var(--color-bg-primary);
|
||||||
--theme-color-card-border: #1c2033;
|
--theme-color-icon-default: var(--color-text-tertiary);
|
||||||
--theme-color-card-border-active: #2A3461;
|
--theme-color-icon-active: rgb(var(--color-text-tertiary));
|
||||||
--theme-color-default-background-separator: #141a2f;
|
--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: var(--theme-color-card-background);
|
||||||
--theme-color-tab-background-active: var(--theme-color-card-background-active);
|
--theme-color-tab-background-active: var(--theme-color-card-background-active);
|
||||||
--theme-color-tab-border: var(--theme-color-card-border);
|
--theme-color-tab-border: var(--theme-color-card-border);
|
||||||
@@ -21,17 +45,15 @@
|
|||||||
--theme-color-row-heading-border: var(--theme-color-card-border);
|
--theme-color-row-heading-border: var(--theme-color-card-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
*{
|
* {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
body {
|
||||||
body{
|
|
||||||
background-color: var(--theme-color-default-background);
|
background-color: var(--theme-color-default-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ const borderClasses = computed(() => {
|
|||||||
:is="tag"
|
:is="tag"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
props.class,
|
|
||||||
badgeClasses[size],
|
badgeClasses[size],
|
||||||
borderClasses,
|
borderClasses,
|
||||||
'rounded inline-flex items-center font-semibold text-white'
|
'rounded inline-flex items-center font-semibold text-white',
|
||||||
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
getOrganizationCurrencySymbol,
|
getOrganizationCurrencySymbol,
|
||||||
} from '../../utils/money';
|
} from '../../utils/money';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
name: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
const model = defineModel({
|
const model = defineModel({
|
||||||
default: null,
|
default: null,
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -51,13 +55,14 @@ function formatCents(modelValue: number) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<TextInput
|
<TextInput
|
||||||
id="projectMemberRate"
|
:id="name"
|
||||||
ref="projectMemberRateInput"
|
ref="projectMemberRateInput"
|
||||||
:modelValue="formatCents(model)"
|
:modelValue="formatCents(model)"
|
||||||
@blur="updateRate($event.target.value)"
|
@blur="updateRate($event.target.value)"
|
||||||
type="text"
|
type="text"
|
||||||
|
:name="name"
|
||||||
placeholder="Billable Rate"
|
placeholder="Billable Rate"
|
||||||
class="mt-1 block w-full"
|
class="mt-2 block w-full"
|
||||||
autocomplete="teamMemberRate" />
|
autocomplete="teamMemberRate" />
|
||||||
<span>
|
<span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import BillableIcon from '@/Components/Common/Icons/BillableIcon.vue';
|
||||||
const active = defineModel({ default: false });
|
const active = defineModel({ default: false });
|
||||||
const emit = defineEmits(['changed']);
|
const emit = defineEmits(['changed']);
|
||||||
function toggleBillable() {
|
function toggleBillable() {
|
||||||
@@ -19,7 +20,7 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const iconColorClasses = computed(() => {
|
const iconColorClasses = computed(() => {
|
||||||
if (active.value) {
|
if (active.value) {
|
||||||
return 'text-accent-200/80 focus:text-accent-200 hover:text-accent-200';
|
return 'text-accent-300 focus:text-accent-200 hover:text-accent-200';
|
||||||
} else {
|
} else {
|
||||||
return 'text-icon-default focus:text-icon-active hover:text-icon-active';
|
return 'text-icon-default focus:text-icon-active hover:text-icon-active';
|
||||||
}
|
}
|
||||||
@@ -49,18 +50,7 @@ const iconSizeWrapperClasses =
|
|||||||
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<svg
|
<BillableIcon :class="iconSizeClasses"></BillableIcon>
|
||||||
:class="iconSizeClasses"
|
|
||||||
viewBox="0 0 8 14"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M4 1V13M1 10.182L1.879 10.841C3.05 11.72 4.949 11.72 6.121 10.841C7.293 9.962 7.293 8.538 6.121 7.659C5.536 7.219 4.768 7 4 7C3.275 7 2.55 6.78 1.997 6.341C0.891 5.462 0.891 4.038 1.997 3.159C3.103 2.28 4.897 2.28 6.003 3.159L6.418 3.489"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
37
resources/js/Components/Common/GroupedItemsCountButton.vue
Normal file
37
resources/js/Components/Common/GroupedItemsCountButton.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
expanded?: boolean;
|
||||||
|
size: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
expanded: false,
|
||||||
|
size: 'w-7 h-7',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandedStatusClasses = computed(() => {
|
||||||
|
if (props.expanded) {
|
||||||
|
return 'border-card-border border bg-card-background-active text-white';
|
||||||
|
}
|
||||||
|
return 'border-card-border border bg-card-background text-muted';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
'font-medium rounded flex items-center transition justify-center',
|
||||||
|
expandedStatusClasses,
|
||||||
|
props.size
|
||||||
|
)
|
||||||
|
">
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
14
resources/js/Components/Common/Icons/BillableIcon.vue
Normal file
14
resources/js/Components/Common/Icons/BillableIcon.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4 1V13M1 10.182L1.879 10.841C3.05 11.72 4.949 11.72 6.121 10.841C7.293 9.962 7.293 8.538 6.121 7.659C5.536 7.219 4.768 7 4 7C3.275 7 2.55 6.78 1.997 6.341C0.891 5.462 0.891 4.038 1.997 3.159C3.103 2.28 4.897 2.28 6.003 3.159L6.418 3.489"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -37,7 +37,7 @@ const filteredMembers = computed(() => {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(searchValue.value?.toLowerCase()?.trim() || '') &&
|
.includes(searchValue.value?.toLowerCase()?.trim() || '') &&
|
||||||
!props.hiddenMembers.some(
|
!props.hiddenMembers.some(
|
||||||
(hiddenMember) => hiddenMember.user_id === member.user_id
|
(hiddenMember) => hiddenMember.id === member.id
|
||||||
) &&
|
) &&
|
||||||
member.is_placeholder === false
|
member.is_placeholder === false
|
||||||
);
|
);
|
||||||
@@ -54,7 +54,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
function resetHighlightedItem() {
|
function resetHighlightedItem() {
|
||||||
if (filteredMembers.value.length > 0) {
|
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;
|
const highlightedClientId = highlightedItemId.value;
|
||||||
if (highlightedClientId) {
|
if (highlightedClientId) {
|
||||||
const highlightedClient = members.value.find(
|
const highlightedClient = members.value.find(
|
||||||
(member) => member.user_id === highlightedClientId
|
(member) => member.id === highlightedClientId
|
||||||
);
|
);
|
||||||
if (highlightedClient) {
|
if (highlightedClient) {
|
||||||
model.value = highlightedClient.user_id;
|
model.value = highlightedClient.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -94,10 +94,10 @@ function moveHighlightUp() {
|
|||||||
);
|
);
|
||||||
if (currentHightlightedIndex === 0) {
|
if (currentHightlightedIndex === 0) {
|
||||||
highlightedItemId.value =
|
highlightedItemId.value =
|
||||||
filteredMembers.value[filteredMembers.value.length - 1].user_id;
|
filteredMembers.value[filteredMembers.value.length - 1].id;
|
||||||
} else {
|
} else {
|
||||||
highlightedItemId.value =
|
highlightedItemId.value =
|
||||||
filteredMembers.value[currentHightlightedIndex - 1].user_id;
|
filteredMembers.value[currentHightlightedIndex - 1].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +108,10 @@ function moveHighlightDown() {
|
|||||||
highlightedItem.value
|
highlightedItem.value
|
||||||
);
|
);
|
||||||
if (currentHightlightedIndex === filteredMembers.value.length - 1) {
|
if (currentHightlightedIndex === filteredMembers.value.length - 1) {
|
||||||
highlightedItemId.value = filteredMembers.value[0].user_id;
|
highlightedItemId.value = filteredMembers.value[0].id;
|
||||||
} else {
|
} else {
|
||||||
highlightedItemId.value =
|
highlightedItemId.value =
|
||||||
filteredMembers.value[currentHightlightedIndex + 1].user_id;
|
filteredMembers.value[currentHightlightedIndex + 1].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,14 +119,13 @@ function moveHighlightDown() {
|
|||||||
const highlightedItemId = ref<string | null>(null);
|
const highlightedItemId = ref<string | null>(null);
|
||||||
const highlightedItem = computed(() => {
|
const highlightedItem = computed(() => {
|
||||||
return members.value.find(
|
return members.value.find(
|
||||||
(member) => member.user_id === highlightedItemId.value
|
(member) => member.id === highlightedItemId.value
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentValue = computed(() => {
|
const currentValue = computed(() => {
|
||||||
if (model.value) {
|
if (model.value) {
|
||||||
return members.value.find((member) => member.user_id === model.value)
|
return members.value.find((member) => member.id === model.value)?.name;
|
||||||
?.name;
|
|
||||||
}
|
}
|
||||||
return searchValue.value;
|
return searchValue.value;
|
||||||
});
|
});
|
||||||
@@ -186,18 +185,18 @@ function onUnfocus() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="member in filteredMembers"
|
v-for="member in filteredMembers"
|
||||||
:key="member.user_id"
|
:key="member.id"
|
||||||
role="option"
|
role="option"
|
||||||
:value="member.user_id"
|
:value="member.id"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-card-background-active':
|
'bg-card-background-active':
|
||||||
member.user_id === highlightedItemId,
|
member.id === highlightedItemId,
|
||||||
}"
|
}"
|
||||||
@click="updateMember(member.user_id)"
|
@click="updateMember(member.id)"
|
||||||
data-testid="client_dropdown_entries"
|
data-testid="client_dropdown_entries"
|
||||||
:data-client-id="member.user_id">
|
:data-client-id="member.id">
|
||||||
<ClientDropdownItem
|
<ClientDropdownItem
|
||||||
:selected="isMemberSelected(member.user_id)"
|
:selected="isMemberSelected(member.id)"
|
||||||
:name="member.name"></ClientDropdownItem>
|
:name="member.name"></ClientDropdownItem>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MultiselectDropdown from '@/Components/Common/MultiselectDropdown.vue';
|
||||||
|
import { useMembersStore } from '@/utils/useMembers';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import type { Member } from '@/utils/api';
|
||||||
|
|
||||||
|
const membersStore = useMembersStore();
|
||||||
|
const { members } = storeToRefs(membersStore);
|
||||||
|
|
||||||
|
function getKeyFromItem(item: Member) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameForItem(item: Member) {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MultiselectDropdown
|
||||||
|
searchPlaceholder="Search for a Member..."
|
||||||
|
:items="members"
|
||||||
|
:get-key-from-item="getKeyFromItem"
|
||||||
|
:get-name-for-item="getNameForItem">
|
||||||
|
<template #trigger>
|
||||||
|
<slot name="trigger"></slot>
|
||||||
|
</template>
|
||||||
|
</MultiselectDropdown>
|
||||||
|
</template>
|
||||||
@@ -29,7 +29,7 @@ async function invitePlaceholder(id: string) {
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
membership: id,
|
member: id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ const model = defineModel<string>({ default: '' });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-3">
|
<div>
|
||||||
<Dropdown align="bottom">
|
<Dropdown align="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div
|
<button
|
||||||
:style="{
|
class="p-2 bg-input-background hover:bg-tertiary transition rounded-full border border-input-border">
|
||||||
backgroundColor: model,
|
<div
|
||||||
boxShadow: `var(--tw-ring-inset) 0 0 0 calc(5px + var(--tw-ring-offset-width)) ${model}30`,
|
:style="{
|
||||||
}"
|
backgroundColor: model,
|
||||||
class="w-4 h-4 rounded-full cursor-pointer"></div>
|
}"
|
||||||
|
class="w-5 h-5 rounded-full cursor-pointer"></div>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="text-white grid grid-cols-6 gap-3 px-3 py-3">
|
<div class="text-white grid grid-cols-6 gap-3 px-3 py-3">
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import PrimaryButton from '@/Components/PrimaryButton.vue';
|
|||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
import ClientDropdown from '@/Components/Common/Client/ClientDropdown.vue';
|
import ClientDropdown from '@/Components/Common/Client/ClientDropdown.vue';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
import Badge from '@/Components/Common/Badge.vue';
|
import Badge from '@/Components/Common/Badge.vue';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
|
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
|
||||||
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
||||||
|
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import InputLabel from '@/Components/InputLabel.vue';
|
||||||
|
|
||||||
const { createProject } = useProjectsStore();
|
const { createProject } = useProjectsStore();
|
||||||
const { clients } = storeToRefs(useClientsStore());
|
const { clients } = storeToRefs(useClientsStore());
|
||||||
@@ -63,33 +64,47 @@ const currentClientName = computed(() => {
|
|||||||
<div
|
<div
|
||||||
class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
||||||
<div class="flex-1 flex items-center">
|
<div class="flex-1 flex items-center">
|
||||||
<ProjectColorSelector
|
<div class="text-center pr-5">
|
||||||
v-model="project.color"></ProjectColorSelector>
|
<InputLabel for="color" value="Color" />
|
||||||
<TextInput
|
<ProjectColorSelector
|
||||||
id="projectName"
|
class="mt-2.5"
|
||||||
ref="projectNameInput"
|
v-model="project.color"></ProjectColorSelector>
|
||||||
v-model="project.name"
|
</div>
|
||||||
type="text"
|
<div class="w-full">
|
||||||
placeholder="Project Name"
|
<InputLabel for="projectName" value="Project name" />
|
||||||
@keydown.enter="submit()"
|
<TextInput
|
||||||
class="mt-1 block w-full"
|
id="projectName"
|
||||||
required
|
name="projectName"
|
||||||
autocomplete="projectName" />
|
ref="projectNameInput"
|
||||||
|
v-model="project.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="The next big thing"
|
||||||
|
@keydown.enter="submit()"
|
||||||
|
class="mt-2 block w-full"
|
||||||
|
required
|
||||||
|
autocomplete="projectName" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:max-w-[120px]">
|
<div class="sm:max-w-[120px]">
|
||||||
<BillableRateInput v-model="project.billable_rate" />
|
<InputLabel for="billableRate" value="Billable Rate" />
|
||||||
|
<BillableRateInput
|
||||||
|
v-model="project.billable_rate"
|
||||||
|
name="billableRate" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ClientDropdown v-model="project.client_id">
|
<InputLabel for="client" value="Client" />
|
||||||
|
<ClientDropdown class="mt-2" v-model="project.client_id">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<Badge size="large">
|
<Badge
|
||||||
<div
|
class="bg-input-background cursor-pointer hover:bg-tertiary"
|
||||||
:class="
|
size="xlarge">
|
||||||
twMerge('inline-block rounded-full')
|
<div class="flex items-center space-x-2">
|
||||||
"></div>
|
<UserCircleIcon
|
||||||
<span>
|
class="w-5 text-icon-default"></UserCircleIcon>
|
||||||
{{ currentClientName }}
|
<span>
|
||||||
</span>
|
{{ currentClientName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
</ClientDropdown>
|
</ClientDropdown>
|
||||||
@@ -97,7 +112,7 @@ const currentClientName = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
class="ms-3"
|
class="ms-3"
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ const currentClientName = computed(() => {
|
|||||||
autocomplete="projectName" />
|
autocomplete="projectName" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:max-w-[120px]">
|
<div class="sm:max-w-[120px]">
|
||||||
<BillableRateInput v-model="project.billable_rate" />
|
<BillableRateInput
|
||||||
|
v-model="project.billable_rate"
|
||||||
|
name="billable_rate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<ClientDropdown v-model="project.client_id">
|
<ClientDropdown v-model="project.client_id">
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MultiselectDropdown from '@/Components/Common/MultiselectDropdown.vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
|
import type { Project } from '@/utils/api';
|
||||||
|
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const { projects } = storeToRefs(projectsStore);
|
||||||
|
|
||||||
|
function getKeyFromItem(item: Project) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameForItem(item: Project) {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MultiselectDropdown
|
||||||
|
searchPlaceholder="Search for a Project..."
|
||||||
|
:items="projects"
|
||||||
|
:get-key-from-item="getKeyFromItem"
|
||||||
|
:get-name-for-item="getNameForItem">
|
||||||
|
<template #trigger>
|
||||||
|
<slot name="trigger"></slot>
|
||||||
|
</template>
|
||||||
|
</MultiselectDropdown>
|
||||||
|
</template>
|
||||||
@@ -18,7 +18,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const projectMember = ref<CreateProjectMemberBody>({
|
const projectMember = ref<CreateProjectMemberBody>({
|
||||||
user_id: '',
|
member_id: '',
|
||||||
billable_rate: null,
|
billable_rate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ async function submit() {
|
|||||||
await createProjectMember(props.projectId, projectMember.value);
|
await createProjectMember(props.projectId, projectMember.value);
|
||||||
show.value = false;
|
show.value = false;
|
||||||
projectMember.value = {
|
projectMember.value = {
|
||||||
user_id: '',
|
member_id: '',
|
||||||
billable_rate: null,
|
billable_rate: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,10 +49,11 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
<div class="col-span-3 sm:col-span-2">
|
<div class="col-span-3 sm:col-span-2">
|
||||||
<MemberCombobox
|
<MemberCombobox
|
||||||
:hidden-members="props.existingMembers"
|
:hidden-members="props.existingMembers"
|
||||||
v-model="projectMember.user_id"></MemberCombobox>
|
v-model="projectMember.member_id"></MemberCombobox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
<div class="col-span-3 sm:col-span-1 flex-1">
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
|
name="billable_rate"
|
||||||
v-model="
|
v-model="
|
||||||
projectMember.billable_rate
|
projectMember.billable_rate
|
||||||
"></BillableRateInput>
|
"></BillableRateInput>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function deleteProjectMember() {
|
|||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = storeToRefs(useMembersStore());
|
||||||
const member = computed(() => {
|
const member = computed(() => {
|
||||||
return members.value.find(
|
return members.value.find(
|
||||||
(member) => member.user_id === props.projectMember.user_id
|
(member) => member.id === props.projectMember.member_id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
|
|||||||
import TimeEntryMoreOptionsDropdown from '@/Components/Common/TimeEntry/TimeEntryMoreOptionsDropdown.vue';
|
import TimeEntryMoreOptionsDropdown from '@/Components/Common/TimeEntry/TimeEntryMoreOptionsDropdown.vue';
|
||||||
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||||
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
|
import BillableToggleButton from '@/Components/Common/BillableToggleButton.vue';
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
import {
|
import {
|
||||||
formatHumanReadableDuration,
|
formatHumanReadableDuration,
|
||||||
formatStartEnd,
|
formatStartEnd,
|
||||||
} from '../../../utils/time';
|
} from '../../../utils/time';
|
||||||
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
|
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
|
||||||
|
import GroupedItemsCountButton from '@/Components/Common/GroupedItemsCountButton.vue';
|
||||||
|
|
||||||
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
const currentTimeEntryStore = useCurrentTimeEntryStore();
|
||||||
const { stopTimer } = currentTimeEntryStore;
|
const { stopTimer } = currentTimeEntryStore;
|
||||||
@@ -99,13 +99,6 @@ function updateProjectAndTask(projectId: string, taskId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
|
|
||||||
const expandedStatusClasses = computed(() => {
|
|
||||||
if (expanded.value) {
|
|
||||||
return 'border-card-border border bg-card-background-active text-white';
|
|
||||||
}
|
|
||||||
return 'border-card-border border bg-card-background text-muted';
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -118,18 +111,11 @@ const expandedStatusClasses = computed(() => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
||||||
<button
|
<GroupedItemsCountButton
|
||||||
@click="expanded = !expanded"
|
:expanded="expanded"
|
||||||
:class="
|
@click="expanded = !expanded">
|
||||||
twMerge(
|
{{ timeEntry?.timeEntries?.length }}
|
||||||
expandedStatusClasses,
|
</GroupedItemsCountButton>
|
||||||
'font-medium w-7 h-7 rounded flex items-center transition justify-center'
|
|
||||||
)
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
{{ timeEntry?.timeEntries?.length }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<TimeEntryDescriptionInput
|
<TimeEntryDescriptionInput
|
||||||
@changed="updateTimeEntryDescription"
|
@changed="updateTimeEntryDescription"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function onChange(event: Event) {
|
|||||||
@blur="onChange"
|
@blur="onChange"
|
||||||
@keydown.enter="onChange"
|
@keydown.enter="onChange"
|
||||||
placeholder="Add a description"
|
placeholder="Add a description"
|
||||||
class="text-white placeholder-muted font-medium bg-transparent hover:bg-card-background rounded-lg border border-transparent hover:border-card-border" />
|
class="text-white font-medium bg-transparent focus-visible:ring-0 hover:bg-card-background rounded-lg border border-transparent hover:border-card-border" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ const timeEntryTags = computed<Tag[]>(() => {
|
|||||||
<template>
|
<template>
|
||||||
<TagDropdown @changed="emit('changed', model)" v-model="model">
|
<TagDropdown @changed="emit('changed', model)" v-model="model">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button data-testid="time_entry_tag_dropdown">
|
<button
|
||||||
|
data-testid="time_entry_tag_dropdown"
|
||||||
|
class="opacity-50 group-hover:opacity-100 transition">
|
||||||
<TagBadge
|
<TagBadge
|
||||||
:border="false"
|
:border="false"
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { useTasksStore } from '@/utils/useTasks';
|
|||||||
import ProjectDropdownItem from '@/Components/Common/Project/ProjectDropdownItem.vue';
|
import ProjectDropdownItem from '@/Components/Common/Project/ProjectDropdownItem.vue';
|
||||||
import type { Project, Task } from '@/utils/api';
|
import type { Project, Task } from '@/utils/api';
|
||||||
import ProjectBadge from '@/Components/Common/Project/ProjectBadge.vue';
|
import ProjectBadge from '@/Components/Common/Project/ProjectBadge.vue';
|
||||||
|
import Badge from '@/Components/Common/Badge.vue';
|
||||||
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
|
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const projectStore = useProjectsStore();
|
||||||
const { projects } = storeToRefs(projectStore);
|
const { projects } = storeToRefs(projectStore);
|
||||||
@@ -94,9 +97,7 @@ const filteredProjects = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function addClientIfNoneExists() {
|
async function addClientIfNoneExists() {
|
||||||
if (highlightedItemId.value) {
|
setProjectAndClientBasedOnHighlightedItem();
|
||||||
setProjectAndClientBasedOnHighlightedItem();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProjectSelected(project: Project) {
|
function isProjectSelected(project: Project) {
|
||||||
@@ -142,131 +143,122 @@ function updateSearchValue(event: Event) {
|
|||||||
const emit = defineEmits(['update:modelValue', 'changed']);
|
const emit = defineEmits(['update:modelValue', 'changed']);
|
||||||
|
|
||||||
function moveHighlightUp() {
|
function moveHighlightUp() {
|
||||||
if (highlightedItemId.value) {
|
const currentHighlightedIndex = filteredProjects.value.findIndex(
|
||||||
const currentHighlightedIndex = filteredProjects.value.findIndex(
|
(projectWithTasks) =>
|
||||||
|
projectWithTasks.project.id === highlightedItemId.value
|
||||||
|
);
|
||||||
|
// check if it is a project id
|
||||||
|
if (currentHighlightedIndex === -1) {
|
||||||
|
// the ID is a task ID
|
||||||
|
const currentProjectWithTasks = filteredProjects.value.find(
|
||||||
(projectWithTasks) =>
|
(projectWithTasks) =>
|
||||||
projectWithTasks.project.id === highlightedItemId.value
|
projectWithTasks.tasks.some(
|
||||||
);
|
|
||||||
// check if it is a project id
|
|
||||||
if (currentHighlightedIndex === -1) {
|
|
||||||
// the ID is a task ID
|
|
||||||
const currentProjectWithTasks = filteredProjects.value.find(
|
|
||||||
(projectWithTasks) =>
|
|
||||||
projectWithTasks.tasks.some(
|
|
||||||
(task) => task.id === highlightedItemId.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (currentProjectWithTasks) {
|
|
||||||
const taskIndex = currentProjectWithTasks.tasks.findIndex(
|
|
||||||
(task) => task.id === highlightedItemId.value
|
(task) => task.id === highlightedItemId.value
|
||||||
);
|
)
|
||||||
if (taskIndex === -1) {
|
);
|
||||||
return;
|
if (currentProjectWithTasks) {
|
||||||
}
|
const taskIndex = currentProjectWithTasks.tasks.findIndex(
|
||||||
if (taskIndex === 0) {
|
(task) => task.id === highlightedItemId.value
|
||||||
// highlight the project if it was the first task before
|
);
|
||||||
highlightedItemId.value =
|
if (taskIndex === -1) {
|
||||||
currentProjectWithTasks.project.id;
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
highlightedItemId.value =
|
|
||||||
currentProjectWithTasks.tasks[taskIndex - 1].id;
|
|
||||||
}
|
}
|
||||||
|
if (taskIndex === 0) {
|
||||||
|
// highlight the project if it was the first task before
|
||||||
|
highlightedItemId.value = currentProjectWithTasks.project.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
highlightedItemId.value =
|
||||||
|
currentProjectWithTasks.tasks[taskIndex - 1].id;
|
||||||
}
|
}
|
||||||
if (currentHighlightedIndex === 0) {
|
}
|
||||||
// highlight the last project or the last project of the last project
|
if (currentHighlightedIndex === 0) {
|
||||||
const lastProject =
|
// highlight the last project or the last project of the last project
|
||||||
filteredProjects.value[filteredProjects.value.length - 1];
|
const lastProject =
|
||||||
if (lastProject.tasks.length > 0) {
|
filteredProjects.value[filteredProjects.value.length - 1];
|
||||||
// highlight last task of last project
|
if (lastProject.tasks.length > 0) {
|
||||||
highlightedItemId.value =
|
// highlight last task of last project
|
||||||
lastProject.tasks[lastProject.tasks.length - 1].id;
|
highlightedItemId.value =
|
||||||
} else {
|
lastProject.tasks[lastProject.tasks.length - 1].id;
|
||||||
highlightedItemId.value =
|
|
||||||
filteredProjects.value[
|
|
||||||
filteredProjects.value.length - 1
|
|
||||||
].project.id;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const previousProject =
|
highlightedItemId.value =
|
||||||
filteredProjects.value[currentHighlightedIndex - 1];
|
filteredProjects.value[
|
||||||
if (previousProject.tasks.length > 0) {
|
filteredProjects.value.length - 1
|
||||||
// highlight last task of previous project
|
].project.id;
|
||||||
highlightedItemId.value =
|
}
|
||||||
previousProject.tasks[previousProject.tasks.length - 1].id;
|
} else {
|
||||||
} else {
|
const previousProject =
|
||||||
highlightedItemId.value =
|
filteredProjects.value[currentHighlightedIndex - 1];
|
||||||
filteredProjects.value[
|
if (previousProject.tasks.length > 0) {
|
||||||
currentHighlightedIndex - 1
|
// highlight last task of previous project
|
||||||
].project.id;
|
highlightedItemId.value =
|
||||||
}
|
previousProject.tasks[previousProject.tasks.length - 1].id;
|
||||||
|
} else {
|
||||||
|
highlightedItemId.value =
|
||||||
|
filteredProjects.value[currentHighlightedIndex - 1].project.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveHighlightDown() {
|
function moveHighlightDown() {
|
||||||
if (highlightedItemId.value) {
|
const currentHighlightedIndex = filteredProjects.value.findIndex(
|
||||||
const currentHighlightedIndex = filteredProjects.value.findIndex(
|
(projectWithTasks) =>
|
||||||
|
projectWithTasks.project.id === highlightedItemId.value
|
||||||
|
);
|
||||||
|
// check if it is a project id
|
||||||
|
if (currentHighlightedIndex === -1) {
|
||||||
|
// the ID is a task ID
|
||||||
|
const currentProjectWithTasks = filteredProjects.value.find(
|
||||||
(projectWithTasks) =>
|
(projectWithTasks) =>
|
||||||
projectWithTasks.project.id === highlightedItemId.value
|
projectWithTasks.tasks.some(
|
||||||
);
|
|
||||||
// check if it is a project id
|
|
||||||
if (currentHighlightedIndex === -1) {
|
|
||||||
// the ID is a task ID
|
|
||||||
const currentProjectWithTasks = filteredProjects.value.find(
|
|
||||||
(projectWithTasks) =>
|
|
||||||
projectWithTasks.tasks.some(
|
|
||||||
(task) => task.id === highlightedItemId.value
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (currentProjectWithTasks) {
|
|
||||||
const taskIndex = currentProjectWithTasks.tasks.findIndex(
|
|
||||||
(task) => task.id === highlightedItemId.value
|
(task) => task.id === highlightedItemId.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (currentProjectWithTasks) {
|
||||||
|
const taskIndex = currentProjectWithTasks.tasks.findIndex(
|
||||||
|
(task) => task.id === highlightedItemId.value
|
||||||
|
);
|
||||||
|
if (taskIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (taskIndex === currentProjectWithTasks.tasks.length - 1) {
|
||||||
|
// highlight the next project if it was the last task in current project
|
||||||
|
const projectIndex = filteredProjects.value.indexOf(
|
||||||
|
currentProjectWithTasks
|
||||||
);
|
);
|
||||||
if (taskIndex === -1) {
|
if (projectIndex === filteredProjects.value.length - 1) {
|
||||||
return;
|
// highlight the first project if it was the last project
|
||||||
|
highlightedItemId.value =
|
||||||
|
filteredProjects.value[0].project.id;
|
||||||
|
} else {
|
||||||
|
highlightedItemId.value =
|
||||||
|
filteredProjects.value[projectIndex + 1].project.id;
|
||||||
}
|
}
|
||||||
if (taskIndex === currentProjectWithTasks.tasks.length - 1) {
|
return;
|
||||||
// highlight the next project if it was the last task in current project
|
|
||||||
const projectIndex = filteredProjects.value.indexOf(
|
|
||||||
currentProjectWithTasks
|
|
||||||
);
|
|
||||||
if (projectIndex === filteredProjects.value.length - 1) {
|
|
||||||
// highlight the first project if it was the last project
|
|
||||||
highlightedItemId.value =
|
|
||||||
filteredProjects.value[0].project.id;
|
|
||||||
} else {
|
|
||||||
highlightedItemId.value =
|
|
||||||
filteredProjects.value[projectIndex + 1].project.id;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
highlightedItemId.value =
|
|
||||||
currentProjectWithTasks.tasks[taskIndex + 1].id;
|
|
||||||
}
|
}
|
||||||
|
highlightedItemId.value =
|
||||||
|
currentProjectWithTasks.tasks[taskIndex + 1].id;
|
||||||
}
|
}
|
||||||
if (currentHighlightedIndex === filteredProjects.value.length - 1) {
|
}
|
||||||
// highlight the first project or the last project of the last project
|
if (currentHighlightedIndex === filteredProjects.value.length - 1) {
|
||||||
const lastProject =
|
// highlight the first project or the last project of the last project
|
||||||
filteredProjects.value[filteredProjects.value.length - 1];
|
const lastProject =
|
||||||
if (lastProject.tasks.length > 0) {
|
filteredProjects.value[filteredProjects.value.length - 1];
|
||||||
// highlight last task of last project
|
if (lastProject.tasks.length > 0) {
|
||||||
highlightedItemId.value = lastProject.tasks[0].id;
|
// highlight last task of last project
|
||||||
} else {
|
highlightedItemId.value = lastProject.tasks[0].id;
|
||||||
highlightedItemId.value = filteredProjects.value[0].project.id;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const currentProjectWithTasks =
|
highlightedItemId.value = filteredProjects.value[0].project.id;
|
||||||
filteredProjects.value[currentHighlightedIndex];
|
}
|
||||||
if (currentProjectWithTasks.tasks.length > 0) {
|
} else {
|
||||||
// highlight last task of previous project
|
const currentProjectWithTasks =
|
||||||
highlightedItemId.value = currentProjectWithTasks.tasks[0].id;
|
filteredProjects.value[currentHighlightedIndex];
|
||||||
} else {
|
if (currentProjectWithTasks.tasks.length > 0) {
|
||||||
highlightedItemId.value =
|
// highlight last task of previous project
|
||||||
filteredProjects.value[
|
highlightedItemId.value = currentProjectWithTasks.tasks[0].id;
|
||||||
currentHighlightedIndex + 1
|
} else {
|
||||||
].project.id;
|
highlightedItemId.value =
|
||||||
}
|
filteredProjects.value[currentHighlightedIndex + 1].project.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,10 +299,23 @@ function selectProject(projectId: string) {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
emit('changed', project.value, task.value);
|
emit('changed', project.value, task.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showCreateProject = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dropdown width="120" v-model="open" :closeOnContentClick="true">
|
<div v-if="projects.length === 0">
|
||||||
|
<Badge
|
||||||
|
@click="showCreateProject = true"
|
||||||
|
size="large"
|
||||||
|
class="cursor-pointer hover:bg-tertiary">
|
||||||
|
<PlusIcon class="-ml-1 w-5"></PlusIcon>
|
||||||
|
<span>Add new project</span>
|
||||||
|
</Badge>
|
||||||
|
<ProjectCreateModal
|
||||||
|
v-model:show="showCreateProject"></ProjectCreateModal>
|
||||||
|
</div>
|
||||||
|
<Dropdown v-else width="120" v-model="open" :closeOnContentClick="true">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ProjectBadge
|
<ProjectBadge
|
||||||
ref="projectDropdownTrigger"
|
ref="projectDropdownTrigger"
|
||||||
@@ -319,7 +324,7 @@ function selectProject(projectId: string) {
|
|||||||
:border="showBadgeBorder"
|
:border="showBadgeBorder"
|
||||||
tag="button"
|
tag="button"
|
||||||
:name="selectedProjectName"
|
:name="selectedProjectName"
|
||||||
class="focus:border-input-border-active focus:outline-0 focus:bg-card-background-separator hover:bg-card-background-separator">
|
class="focus:border-border-tertiary focus:outline-0 focus:bg-card-background-separator hover:bg-card-background-separator">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<span>
|
<span>
|
||||||
{{ selectedProjectName }}
|
{{ selectedProjectName }}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const iconColorClasses = computed(() => {
|
|||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
iconColorClasses,
|
iconColorClasses,
|
||||||
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full w-7 sm:w-10 h-7 sm:h-10 flex items-center justify-center'
|
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus-visible:bg-card-background-separator hover:bg-card-background-separator rounded-full w-7 sm:w-10 h-7 sm:h-10 flex items-center justify-center'
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<TagIcon class="w-5 sm:w-6 h-5 sm:h-6"></TagIcon>
|
<TagIcon class="w-5 sm:w-6 h-5 sm:h-6"></TagIcon>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const buttonColorClasses = computed(() => {
|
|||||||
if (props.active) {
|
if (props.active) {
|
||||||
return 'bg-red-400/80 hover:bg-red-500/80 focus:bg-red-500/80';
|
return 'bg-red-400/80 hover:bg-red-500/80 focus:bg-red-500/80';
|
||||||
} else {
|
} else {
|
||||||
return 'bg-accent-300/50 hover:bg-accent-400/70 focus:bg-accent-400/70';
|
return 'bg-accent-300/70 hover:bg-accent-400/70 focus:bg-accent-400/70';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
formatHumanReadableDuration,
|
formatHumanReadableDuration,
|
||||||
getDayJsInstance,
|
getDayJsInstance,
|
||||||
} from '@/utils/time';
|
} from '@/utils/time';
|
||||||
|
import { useCssVar } from '@vueuse/core';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dailyHoursTracked: { duration: number; date: string }[];
|
dailyHoursTracked: { duration: number; date: string }[];
|
||||||
@@ -40,6 +41,8 @@ const max = Math.max(
|
|||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const backgroundColor = useCssVar('--color-bg-secondary');
|
||||||
|
const itemBackgroundColor = useCssVar('--color-bg-tertiary');
|
||||||
const option = ref({
|
const option = ref({
|
||||||
tooltip: {},
|
tooltip: {},
|
||||||
visualMap: {
|
visualMap: {
|
||||||
@@ -50,7 +53,7 @@ const option = ref({
|
|||||||
left: 'center',
|
left: 'center',
|
||||||
top: 'center',
|
top: 'center',
|
||||||
inRange: {
|
inRange: {
|
||||||
color: ['#242940', '#2DBE45'],
|
color: [itemBackgroundColor.value, '#2DBE45'],
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
@@ -74,8 +77,9 @@ const option = ref({
|
|||||||
.format('YYYY-MM-DD'),
|
.format('YYYY-MM-DD'),
|
||||||
],
|
],
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
|
color: 'transparent',
|
||||||
borderWidth: 8,
|
borderWidth: 8,
|
||||||
borderColor: '#13152B',
|
borderColor: backgroundColor.value,
|
||||||
},
|
},
|
||||||
yearLabel: { show: false },
|
yearLabel: { show: false },
|
||||||
},
|
},
|
||||||
@@ -85,6 +89,8 @@ const option = ref({
|
|||||||
data: props.dailyHoursTracked.map((el) => [el.date, el.duration]),
|
data: props.dailyHoursTracked.map((el) => [el.date, el.duration]),
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.05)',
|
||||||
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueFormatter: (value: number, dataIndex: number) => {
|
valueFormatter: (value: number, dataIndex: number) => {
|
||||||
@@ -104,10 +110,9 @@ const option = ref({
|
|||||||
<DashboardCard title="Activity Graph" :icon="BoltIcon">
|
<DashboardCard title="Activity Graph" :icon="BoltIcon">
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<v-chart
|
<v-chart
|
||||||
:autoresize="true"
|
|
||||||
class="chart"
|
class="chart"
|
||||||
:option="option"
|
:option="option"
|
||||||
style="height: 260px" />
|
style="height: 260px; background-color: transparent" />
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="flex flex-col">
|
<section class="flex flex-col">
|
||||||
<CardTitle :title="title" :icon="icon"></CardTitle>
|
<CardTitle :title="title" :icon="icon"></CardTitle>
|
||||||
<div
|
<div
|
||||||
class="rounded-lg bg-card-background border border-card-border flex-1 flex items-stretch">
|
class="rounded-lg bg-card-background border border-card-border flex-1 flex items-stretch shadow-card">
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import VChart from 'vue-echarts';
|
import VChart from 'vue-echarts';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useCssVar } from '@vueuse/core';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
history: number[];
|
history: number[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const accentColor = useCssVar('--color-accent-quaternary');
|
||||||
|
|
||||||
const seriesData = props.history.map((el) => {
|
const seriesData = props.history.map((el) => {
|
||||||
return {
|
return {
|
||||||
value: el,
|
value: el,
|
||||||
...{
|
...{
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(125,156,188,1)',
|
borderColor: 'rgba(' + accentColor.value + ',0.8)',
|
||||||
borderRadius: [2, 2, 0, 0],
|
borderRadius: [2, 2, 0, 0],
|
||||||
color: 'rgba(125,156,188,1)',
|
color: 'rgba(' + accentColor.value + ',0.8)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
import { useCssVar } from '@vueuse/core';
|
||||||
|
import { formatHumanReadableDuration } from '@/utils/time';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
@@ -23,6 +25,8 @@ use([
|
|||||||
|
|
||||||
provide(THEME_KEY, 'dark');
|
provide(THEME_KEY, 'dark');
|
||||||
|
|
||||||
|
const backgroundColor = useCssVar('--theme-color-default-background');
|
||||||
|
|
||||||
function hexToRGBA(hex: string, opacity = 1) {
|
function hexToRGBA(hex: string, opacity = 1) {
|
||||||
// Remove the hash at the start if it's there
|
// Remove the hash at the start if it's there
|
||||||
hex = hex.replace(/^#/, '');
|
hex = hex.replace(/^#/, '');
|
||||||
@@ -60,7 +64,7 @@ const seriesData = props.weeklyProjectOverview.map((el) => {
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 15,
|
borderRadius: 15,
|
||||||
// TODO: Fix dynamic color
|
// TODO: Fix dynamic color
|
||||||
borderColor: '#0b0d1c',
|
borderColor: backgroundColor.value,
|
||||||
borderWidth: 18,
|
borderWidth: 18,
|
||||||
color: new LinearGradient(0, 0, 0, 1, [
|
color: new LinearGradient(0, 0, 0, 1, [
|
||||||
{
|
{
|
||||||
@@ -77,13 +81,23 @@ const seriesData = props.weeklyProjectOverview.map((el) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const option = ref({
|
const option = ref({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
bottom: 'bottom',
|
||||||
|
},
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
label: {
|
label: {
|
||||||
// TODO: Muted color make dynamic
|
show: false,
|
||||||
color: '#D9DCFB',
|
},
|
||||||
fontWeight: 'bold',
|
tooltip: {
|
||||||
|
valueFormatter: (value: number) => {
|
||||||
|
return formatHumanReadableDuration(value);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: seriesData,
|
data: seriesData,
|
||||||
radius: ['30%', '65%'],
|
radius: ['30%', '65%'],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import ProjectsChartCard from '@/Components/Dashboard/ProjectsChartCard.vue';
|
|||||||
import { formatHumanReadableDuration } from '@/utils/time';
|
import { formatHumanReadableDuration } from '@/utils/time';
|
||||||
import { formatCents } from '@/utils/money';
|
import { formatCents } from '@/utils/money';
|
||||||
import { getWeekStart } from '@/utils/useUser';
|
import { getWeekStart } from '@/utils/useUser';
|
||||||
|
import { useCssVar } from '@vueuse/core';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
@@ -47,6 +48,7 @@ const props = defineProps<{
|
|||||||
duration: number;
|
duration: number;
|
||||||
}[];
|
}[];
|
||||||
}>();
|
}>();
|
||||||
|
const accentColor = useCssVar('--color-accent-quaternary');
|
||||||
|
|
||||||
const seriesData = props.weeklyHistory.map((el) => {
|
const seriesData = props.weeklyHistory.map((el) => {
|
||||||
return {
|
return {
|
||||||
@@ -56,23 +58,34 @@ const seriesData = props.weeklyHistory.map((el) => {
|
|||||||
borderColor: new LinearGradient(0, 0, 0, 1, [
|
borderColor: new LinearGradient(0, 0, 0, 1, [
|
||||||
{
|
{
|
||||||
offset: 0,
|
offset: 0,
|
||||||
color: 'rgba(125,156,188,1)',
|
color: 'rgba(' + accentColor.value + ',0.7)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
offset: 1,
|
offset: 1,
|
||||||
color: 'rgba(125,156,188,0.7)',
|
color: 'rgba(' + accentColor.value + ',0.5)',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
borderWidth: 3,
|
emphasis: {
|
||||||
|
color: new LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(' + accentColor.value + ',0.9)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(' + accentColor.value + ',0.7)',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
borderRadius: [12, 12, 0, 0],
|
borderRadius: [12, 12, 0, 0],
|
||||||
color: new LinearGradient(0, 0, 0, 1, [
|
color: new LinearGradient(0, 0, 0, 1, [
|
||||||
{
|
{
|
||||||
offset: 0,
|
offset: 0,
|
||||||
color: 'rgba(125,156,188,0.9)',
|
color: 'rgba(' + accentColor.value + ',0.7)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
offset: 1,
|
offset: 1,
|
||||||
color: 'rgba(125,156,188,0.4)',
|
color: 'rgba(' + accentColor.value + ',0.5)',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@@ -106,6 +119,8 @@ const weekdays = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const markLineColor = useCssVar('--color-border-secondary');
|
||||||
|
|
||||||
const option = ref({
|
const option = ref({
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
@@ -120,12 +135,6 @@ const option = ref({
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: weekdays.value,
|
data: weekdays.value,
|
||||||
markLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: 'rgba(125,156,188,0.1)',
|
|
||||||
type: 'dashed',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'transparent', // Set desired color here
|
color: 'transparent', // Set desired color here
|
||||||
@@ -147,7 +156,7 @@ const option = ref({
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(125,156,188,0.2)', // Set desired color here
|
color: markLineColor.value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const close = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row justify-end px-6 py-4 border-t border-card-background-separator bg-card-background text-end">
|
class="flex flex-row justify-end px-6 py-4 border-t border-card-background-separator bg-default-background rounded-b-2xl text-end">
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { flip, type Placement, useFloating } from '@floating-ui/vue';
|
import { flip, type Placement, useFloating } from '@floating-ui/vue';
|
||||||
import { offset } from '@floating-ui/vue';
|
import { offset } from '@floating-ui/vue';
|
||||||
import { autoUpdate } from '@floating-ui/vue';
|
import { autoUpdate } from '@floating-ui/vue';
|
||||||
@@ -7,12 +7,10 @@ import { autoUpdate } from '@floating-ui/vue';
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
align: Placement;
|
align: Placement;
|
||||||
width: string;
|
|
||||||
closeOnContentClick: boolean;
|
closeOnContentClick: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
align: 'bottom-start',
|
align: 'bottom-start',
|
||||||
width: '48',
|
|
||||||
closeOnContentClick: true,
|
closeOnContentClick: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -39,12 +37,6 @@ function onContentClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthClass = computed(() => {
|
|
||||||
return {
|
|
||||||
48: 'w-48',
|
|
||||||
}[props.width.toString()];
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleOpen() {
|
function toggleOpen() {
|
||||||
open.value = !open.value;
|
open.value = !open.value;
|
||||||
if (open.value === true) {
|
if (open.value === true) {
|
||||||
@@ -82,7 +74,6 @@ const { floatingStyles } = useFloating(reference, floating, {
|
|||||||
v-show="open"
|
v-show="open"
|
||||||
ref="floating"
|
ref="floating"
|
||||||
class="z-50"
|
class="z-50"
|
||||||
:class="[widthClass]"
|
|
||||||
:style="floatingStyles"
|
:style="floatingStyles"
|
||||||
@click="onContentClick">
|
@click="onContentClick">
|
||||||
<transition
|
<transition
|
||||||
@@ -94,7 +85,7 @@ const { floatingStyles } = useFloating(reference, floating, {
|
|||||||
leave-to-class="transform opacity-0 scale-95">
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
v-if="open"
|
||||||
class="rounded-lg ring-1 relative ring-black ring-opacity-5 border border-card-border overflow-none bg-card-background shadow-lg">
|
class="rounded-lg ring-1 relative ring-black ring-opacity-5 border border-card-border overflow-none shadow-dropdown bg-card-background">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const maxWidthClass = computed(() => {
|
|||||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
<div
|
<div
|
||||||
v-show="show"
|
v-show="show"
|
||||||
class="mb-6 bg-card-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto"
|
class="mb-6 bg-default-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto"
|
||||||
:class="maxWidthClass">
|
:class="maxWidthClass">
|
||||||
<slot v-if="show" />
|
<slot v-if="show" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const sizeClasses = {
|
|||||||
:type="type"
|
:type="type"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 ease-in-out',
|
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:border-input-border-active focus:outline-none focus:ring-0 disabled:opacity-25 ease-in-out',
|
||||||
sizeClasses[props.size]
|
sizeClasses[props.size]
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function updateValue(event: Event) {
|
|||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
class="border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm"
|
class="border-input-border border bg-input-background text-white focus:ring-input-border-active focus:ring-0 focus-visible:border-input-border-active rounded-md shadow-sm"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:name="name"
|
:name="name"
|
||||||
@input="updateValue" />
|
@input="updateValue" />
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ function switchToTimeEntryOrganization() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center relative" data-testid="dashboard_timer">
|
<div class="flex items-center relative" data-testid="dashboard_timer">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:flex-row w-full rounded-lg bg-card-background border-card-border border transition">
|
class="flex flex-col sm:flex-row w-full rounded-lg bg-card-background border-card-border border transition shadow-card">
|
||||||
<div class="flex-1 flex items-center pr-6">
|
<div class="flex-1 flex items-center pr-6">
|
||||||
<input
|
<input
|
||||||
placeholder="What are you working on?"
|
placeholder="What are you working on?"
|
||||||
@@ -194,7 +194,7 @@ function switchToTimeEntryOrganization() {
|
|||||||
v-model="currentTimeEntry.description"
|
v-model="currentTimeEntry.description"
|
||||||
@keydown.enter="startTimerIfNotActive"
|
@keydown.enter="startTimerIfNotActive"
|
||||||
@blur="updateTimeEntry"
|
@blur="updateTimeEntry"
|
||||||
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-base sm:text-lg text-white focus:bg-card-background-active font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
|
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-base sm:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
|
||||||
type="text" />
|
type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between pl-2">
|
<div class="flex items-center justify-between pl-2">
|
||||||
@@ -226,7 +226,7 @@ function switchToTimeEntryOrganization() {
|
|||||||
@blur="updateTimerAndStartLiveTimerUpdate"
|
@blur="updateTimerAndStartLiveTimerUpdate"
|
||||||
@keydown.enter="onTimeEntryEnterPress"
|
@keydown.enter="onTimeEntryEnterPress"
|
||||||
v-model="currentTime"
|
v-model="currentTime"
|
||||||
class="w-[110px] sm:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-sm sm:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition focus:bg-card-background-active"
|
class="w-[110px] sm:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-sm sm:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
|
||||||
type="text" />
|
type="text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MainContainer from '@/Pages/MainContainer.vue';
|
import MainContainer from '@/Pages/MainContainer.vue';
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
import { TagIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import TagTable from '@/Components/Common/Tag/TagTable.vue';
|
import TagTable from '@/Components/Common/Tag/TagTable.vue';
|
||||||
import TagCreateModal from '@/Components/Common/Tag/TagCreateModal.vue';
|
import TagCreateModal from '@/Components/Common/Tag/TagCreateModal.vue';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import { canCreateTags } from '@/utils/permissions';
|
import { canCreateTags } from '@/utils/permissions';
|
||||||
|
|
||||||
const createTag = ref(false);
|
const createTag = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ const createTag = ref(false);
|
|||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||||
<div class="flex items-center space-x-6">
|
<div class="flex items-center space-x-6">
|
||||||
<PageTitle :icon="FolderIcon" title="Tags"> </PageTitle>
|
<PageTitle :icon="TagIcon" title="Tags"> </PageTitle>
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateTags()"
|
v-if="canCreateTags()"
|
||||||
|
|||||||
@@ -68,12 +68,10 @@ const groupedTimeEntries = computed(() => {
|
|||||||
e.billable === entry.billable &&
|
e.billable === entry.billable &&
|
||||||
e.description === entry.description
|
e.description === entry.description
|
||||||
);
|
);
|
||||||
console.log(oldEntriesIndex);
|
|
||||||
if (oldEntriesIndex !== -1 && newDailyEntries[oldEntriesIndex]) {
|
if (oldEntriesIndex !== -1 && newDailyEntries[oldEntriesIndex]) {
|
||||||
newDailyEntries[oldEntriesIndex].timeEntries.push(entry);
|
newDailyEntries[oldEntriesIndex].timeEntries.push(entry);
|
||||||
|
|
||||||
// Add up durations for time entries of the same type
|
// Add up durations for time entries of the same type
|
||||||
console.log(newDailyEntries[oldEntriesIndex], entry?.duration);
|
|
||||||
newDailyEntries[oldEntriesIndex].duration =
|
newDailyEntries[oldEntriesIndex].duration =
|
||||||
(newDailyEntries[oldEntriesIndex].duration ?? 0) +
|
(newDailyEntries[oldEntriesIndex].duration ?? 0) +
|
||||||
(entry?.duration ?? 0);
|
(entry?.duration ?? 0);
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export interface Task {
|
|||||||
organization: Organization;
|
organization: Organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OrganizationWithMembership = Organization & { membership: Membership };
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
// columns
|
// columns
|
||||||
id: string;
|
id: string;
|
||||||
@@ -98,6 +100,7 @@ export interface User {
|
|||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
clients: Client[];
|
clients: Client[];
|
||||||
current_team: Organization;
|
current_team: Organization;
|
||||||
|
all_teams: OrganizationWithMembership[];
|
||||||
owned_teams: Organization[];
|
owned_teams: Organization[];
|
||||||
teams: Organization[];
|
teams: Organization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export const useNotificationsStore = defineStore('notifications', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
throw new Error('Failed to handle API request', { cause: error });
|
||||||
}
|
}
|
||||||
throw new Error('Failed to handle API request');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { addNotification, notifications, handleApiRequestNotifications };
|
return { addNotification, notifications, handleApiRequestNotifications };
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { api } from '../../../openapi.json.client';
|
|||||||
import type { TimeEntry } from '@/utils/api';
|
import type { TimeEntry } from '@/utils/api';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import { getCurrentOrganizationId, getCurrentUserId } from '@/utils/useUser';
|
import {
|
||||||
|
getCurrentMembershipId,
|
||||||
|
getCurrentOrganizationId,
|
||||||
|
getCurrentUserId,
|
||||||
|
} from '@/utils/useUser';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
@@ -77,9 +81,9 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startTimer() {
|
async function startTimer() {
|
||||||
const user = getCurrentUserId();
|
|
||||||
const organization = getCurrentOrganizationId();
|
const organization = getCurrentOrganizationId();
|
||||||
if (organization) {
|
const membership = getCurrentMembershipId();
|
||||||
|
if (organization && membership) {
|
||||||
const startTime =
|
const startTime =
|
||||||
currentTimeEntry.value.start !== ''
|
currentTimeEntry.value.start !== ''
|
||||||
? currentTimeEntry.value.start
|
? currentTimeEntry.value.start
|
||||||
@@ -87,7 +91,7 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
const response = await handleApiRequestNotifications(
|
const response = await handleApiRequestNotifications(
|
||||||
api.createTimeEntry(
|
api.createTimeEntry(
|
||||||
{
|
{
|
||||||
user_id: user,
|
member_id: membership,
|
||||||
start: startTime,
|
start: startTime,
|
||||||
description: currentTimeEntry.value?.description,
|
description: currentTimeEntry.value?.description,
|
||||||
project_id: currentTimeEntry.value?.project_id,
|
project_id: currentTimeEntry.value?.project_id,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const useMembersStore = defineStore('members', () => {
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
organization: organization,
|
organization: organization,
|
||||||
membership: membershipId,
|
member: membershipId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getCurrentOrganizationId, getCurrentUserId } from '@/utils/useUser';
|
import {
|
||||||
|
getCurrentMembershipId,
|
||||||
|
getCurrentOrganizationId,
|
||||||
|
} from '@/utils/useUser';
|
||||||
import { api } from '../../../openapi.json.client';
|
import { api } from '../../../openapi.json.client';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import type { CreateTimeEntryBody, TimeEntry } from '@/utils/api';
|
import type { CreateTimeEntryBody, TimeEntry } from '@/utils/api';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
|
||||||
export type TimeEntriesGroupedByType = TimeEntry & { timeEntries: TimeEntry[] };
|
export type TimeEntriesGroupedByType = TimeEntry & { timeEntries: TimeEntry[] };
|
||||||
|
|
||||||
export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
||||||
@@ -12,6 +16,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
|
|
||||||
const allTimeEntriesLoaded = ref(false);
|
const allTimeEntriesLoaded = ref(false);
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
async function fetchTimeEntries() {
|
async function fetchTimeEntries() {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
@@ -22,7 +27,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
only_full_dates: 'true',
|
only_full_dates: 'true',
|
||||||
user_id: getCurrentUserId(),
|
member_id: getCurrentMembershipId(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -48,7 +53,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
},
|
},
|
||||||
queries: {
|
queries: {
|
||||||
only_full_dates: 'true',
|
only_full_dates: 'true',
|
||||||
user_id: getCurrentUserId(),
|
member_id: getCurrentMembershipId(),
|
||||||
before: dayjs(latestTimeEntry.start).utc().format(),
|
before: dayjs(latestTimeEntry.start).utc().format(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -84,11 +89,18 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTimeEntry(timeEntry: CreateTimeEntryBody) {
|
async function createTimeEntry(
|
||||||
|
timeEntry: Omit<CreateTimeEntryBody, 'member_id'>
|
||||||
|
) {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
const memberId = getCurrentMembershipId();
|
||||||
|
if (organizationId && memberId !== undefined) {
|
||||||
|
const newTimeEntry = {
|
||||||
|
...timeEntry,
|
||||||
|
member_id: memberId,
|
||||||
|
} as CreateTimeEntryBody;
|
||||||
await handleApiRequestNotifications(
|
await handleApiRequestNotifications(
|
||||||
api.createTimeEntry(timeEntry, {
|
api.createTimeEntry(newTimeEntry, {
|
||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ function getCurrentOrganizationId() {
|
|||||||
return page.props.auth.user.current_team_id;
|
return page.props.auth.user.current_team_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentMembershipId() {
|
||||||
|
return page.props.auth.user.all_teams.find(
|
||||||
|
(team) => team.id === getCurrentOrganizationId()
|
||||||
|
)?.membership.id;
|
||||||
|
}
|
||||||
|
|
||||||
function getUserTimezone() {
|
function getUserTimezone() {
|
||||||
return page.props.auth.user.timezone;
|
return page.props.auth.user.timezone;
|
||||||
}
|
}
|
||||||
@@ -27,4 +33,5 @@ export {
|
|||||||
getCurrentUserId,
|
getCurrentUserId,
|
||||||
getUserTimezone,
|
getUserTimezone,
|
||||||
getWeekStart,
|
getWeekStart,
|
||||||
|
getCurrentMembershipId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import defaultTheme from 'tailwindcss/defaultTheme';
|
|||||||
import forms from '@tailwindcss/forms';
|
import forms from '@tailwindcss/forms';
|
||||||
import typography from '@tailwindcss/typography';
|
import typography from '@tailwindcss/typography';
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import("tailwindcss").Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||||
@@ -11,9 +11,12 @@ export default {
|
|||||||
'./resources/views/**/*.blade.php',
|
'./resources/views/**/*.blade.php',
|
||||||
'./resources/js/**/*.vue',
|
'./resources/js/**/*.vue',
|
||||||
],
|
],
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
boxShadow: {
|
||||||
|
'card': '0 4px 7px 0px rgb(0 0 0 / 30%)',
|
||||||
|
'dropdown': '0 4px 7px 0px rgb(0 0 0 / 40%)',
|
||||||
|
},
|
||||||
containers: {
|
containers: {
|
||||||
'2xs': '16rem',
|
'2xs': '16rem',
|
||||||
},
|
},
|
||||||
@@ -21,27 +24,38 @@ export default {
|
|||||||
sans: ['Outfit', ...defaultTheme.fontFamily.sans],
|
sans: ['Outfit', ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'white': '#D9DCFB',
|
'primary': 'var(--color-bg-primary)',
|
||||||
|
'secondary': 'var(--color-bg-secondary)',
|
||||||
|
'tertiary': 'var(--color-bg-tertiary)',
|
||||||
|
'quaternary': 'var(--color-bg-quaternary)',
|
||||||
|
'text-primary': 'var(--color-text-primary)',
|
||||||
|
'text-secondary': 'var(--color-text-secondary)',
|
||||||
|
'text-tertiary': 'var(--color-text-tertiary)',
|
||||||
|
'text-quaternary': 'var(--color-text-quaternary)',
|
||||||
|
'border-primary': 'var(--color-border-primary)',
|
||||||
|
'border-secondary': 'var(--color-border-secondary)',
|
||||||
|
'border-tertiary': 'var(--color-border-tertiary)',
|
||||||
'default-background': 'var(--theme-color-default-background)',
|
'default-background': 'var(--theme-color-default-background)',
|
||||||
'default-background-separator': '#13152B',
|
'default-background-separator':
|
||||||
|
'var(--theme-color-default-background-separator)',
|
||||||
'card-background': 'var(--theme-color-card-background)',
|
'card-background': 'var(--theme-color-card-background)',
|
||||||
'card-background-active':
|
'card-background-active':
|
||||||
'var(--theme-color-card-background-active)',
|
'var(--theme-color-card-background-active)',
|
||||||
'card-background-separator': '#262A51',
|
'card-background-separator':
|
||||||
|
'var(--theme-color-card-background-separator)',
|
||||||
'card-border': 'var(--theme-color-card-border)',
|
'card-border': 'var(--theme-color-card-border)',
|
||||||
'card-border-active': 'var(--theme-color-card-border-active)',
|
'card-border-active': 'var(--theme-color-card-border-active)',
|
||||||
'muted': '#8F93B7',
|
'muted': 'var(--theme-color-muted-text)',
|
||||||
'icon-default': 'var(--theme-color-icon-default)',
|
'icon-default': 'var(--theme-color-icon-default)',
|
||||||
'tab-background': 'var(--theme-color-tab-background)',
|
'tab-background': 'var(--theme-color-tab-background)',
|
||||||
'tab-background-active':
|
'tab-background-active':
|
||||||
'var(--theme-color-tab-background-active)',
|
'var(--theme-color-tab-background-active)',
|
||||||
'tab-border': 'var(--theme-color-tab-border)',
|
'tab-border': 'var(--theme-color-tab-border)',
|
||||||
'icon-active': '#787DA8',
|
'icon-active': 'var(--theme-color-icon-active)',
|
||||||
'menu-active': '#13152B',
|
'menu-active': 'var(--theme-color-menu-active)',
|
||||||
'input-placeholder': '#42466C',
|
'input-border': 'var(--theme-color-input-border)',
|
||||||
'input-border': '#242740',
|
'input-border-active': 'var(--color-input-border-active)',
|
||||||
'input-border-active': '#797EA8',
|
'input-background': 'var(--theme-color-input-background)',
|
||||||
'input-background': '#030513',
|
|
||||||
'button-secondary-background':
|
'button-secondary-background':
|
||||||
'var(--theme-color-card-background)',
|
'var(--theme-color-card-background)',
|
||||||
'button-secondary-background-hover':
|
'button-secondary-background-hover':
|
||||||
@@ -52,17 +66,10 @@ export default {
|
|||||||
'var(--theme-color-row-heading-background)',
|
'var(--theme-color-row-heading-background)',
|
||||||
'row-heading-border': 'var(--theme-color-row-heading-border)',
|
'row-heading-border': 'var(--theme-color-row-heading-border)',
|
||||||
'accent': {
|
'accent': {
|
||||||
'50': '#eff7ff',
|
'200': 'rgba(var(--color-accent-quaternary), <alpha-value>)',
|
||||||
'100': '#daecff',
|
'300': 'rgba(var(--color-accent-tertiary), <alpha-value>)',
|
||||||
'200': '#b0d7ff',
|
'400': 'rgba(var(--color-accent-secondary), <alpha-value>)',
|
||||||
'300': '#91caff',
|
'500': 'rgba(var(--color-accent-primary), <alpha-value>)',
|
||||||
'400': '#5eadfc',
|
|
||||||
'500': '#388bf9',
|
|
||||||
'600': '#226cee',
|
|
||||||
'700': '#1a57db',
|
|
||||||
'800': '#1c46b1',
|
|
||||||
'900': '#1c3f8c',
|
|
||||||
'950': '#162755',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user