mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 04:32:15 +01:00
add notice when time tracker is running in a different organization
This commit is contained in:
@@ -256,7 +256,7 @@ test('test that updating a the duration in the overview works on blur', async ({
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
newTimeEntry.getByTestId('time_entry_duration_input')
|
newTimeEntry.getByTestId('time_entry_duration_input')
|
||||||
).toHaveValue('00h 20min');
|
).toHaveValue('0h 20min');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test that start stop button stops running timer
|
// Test that start stop button stops running timer
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ const TimeEntryResource = z
|
|||||||
description: z.union([z.string(), z.null()]),
|
description: z.union([z.string(), z.null()]),
|
||||||
task_id: z.union([z.string(), z.null()]),
|
task_id: z.union([z.string(), z.null()]),
|
||||||
project_id: z.union([z.string(), z.null()]),
|
project_id: z.union([z.string(), z.null()]),
|
||||||
|
organization_id: z.string(),
|
||||||
user_id: z.string(),
|
user_id: z.string(),
|
||||||
tags: z.array(z.string()),
|
tags: z.array(z.string()),
|
||||||
billable: z.boolean(),
|
billable: z.boolean(),
|
||||||
@@ -391,6 +392,17 @@ const endpoints = makeApi([
|
|||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
@@ -623,6 +635,78 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'delete',
|
||||||
|
path: '/v1/organizations/:organization/invitations/:invitation',
|
||||||
|
alias: 'removeInvitation',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'Body',
|
||||||
|
schema: z.object({}).partial().passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organization',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string().uuid(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'invitation',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string().uuid(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.null(),
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/v1/organizations/:organization/invitations/:invitation/resend',
|
||||||
|
alias: 'resendInvitationEmail',
|
||||||
|
requestFormat: 'json',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'Body',
|
||||||
|
schema: z.object({}).partial().passthrough(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'organization',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string().uuid(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'invitation',
|
||||||
|
type: 'Path',
|
||||||
|
schema: z.string().uuid(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response: z.null(),
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/v1/organizations/:organization/members',
|
path: '/v1/organizations/:organization/members',
|
||||||
@@ -761,6 +845,17 @@ const endpoints = makeApi([
|
|||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
@@ -1102,6 +1197,17 @@ const endpoints = makeApi([
|
|||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
@@ -1371,6 +1477,17 @@ const endpoints = makeApi([
|
|||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
@@ -1567,6 +1684,17 @@ const endpoints = makeApi([
|
|||||||
],
|
],
|
||||||
response: z.null(),
|
response: z.null(),
|
||||||
errors: [
|
errors: [
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
description: `API exception`,
|
||||||
|
schema: z
|
||||||
|
.object({
|
||||||
|
error: z.boolean(),
|
||||||
|
key: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
description: `Authorization error`,
|
description: `Authorization error`,
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ function updateSearchValue(event: Event) {
|
|||||||
const emit = defineEmits(['update:modelValue', 'changed']);
|
const emit = defineEmits(['update:modelValue', 'changed']);
|
||||||
|
|
||||||
function updateMember(newValue: string | null) {
|
function updateMember(newValue: string | null) {
|
||||||
console.log(newValue);
|
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
model.value = newValue;
|
model.value = newValue;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ function moveHighlightDown() {
|
|||||||
filteredTags.value[currentHightlightedIndex + 1].id;
|
filteredTags.value[currentHightlightedIndex + 1].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('move down');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedItemId = ref<string | null>(null);
|
const highlightedItemId = ref<string | null>(null);
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
|
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
|
||||||
import Dropdown from '@/Components/Dropdown.vue';
|
import Dropdown from '@/Components/Dropdown.vue';
|
||||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
import DropdownLink from '@/Components/DropdownLink.vue';
|
||||||
import { router, usePage } from '@inertiajs/vue3';
|
import { usePage } from '@inertiajs/vue3';
|
||||||
import type { Organization, User } from '@/types/models';
|
import type { Organization, User } from '@/types/models';
|
||||||
import { isBillingActivated } from '@/utils/billing';
|
import { isBillingActivated } from '@/utils/billing';
|
||||||
import { initializeStores } from '@/utils/init';
|
|
||||||
import { canUpdateOrganization } from '@/utils/permissions';
|
import { canUpdateOrganization } from '@/utils/permissions';
|
||||||
|
import { switchOrganization } from '@/utils/useOrganization';
|
||||||
|
|
||||||
const page = usePage<{
|
const page = usePage<{
|
||||||
jetstream: {
|
jetstream: {
|
||||||
@@ -22,19 +22,8 @@ const page = usePage<{
|
|||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const switchToTeam = (team: Organization) => {
|
const switchToTeam = (organization: Organization) => {
|
||||||
router.put(
|
switchOrganization(organization.id);
|
||||||
route('current-team.update'),
|
|
||||||
{
|
|
||||||
team_id: team.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
preserveState: false,
|
|
||||||
onSuccess: () => {
|
|
||||||
initializeStores();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { storeToRefs } from 'pinia';
|
|||||||
import parse from 'parse-duration';
|
import parse from 'parse-duration';
|
||||||
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
|
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
|
||||||
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import { switchOrganization } from '@/utils/useOrganization';
|
||||||
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
|
|
||||||
const page = usePage<{
|
const page = usePage<{
|
||||||
auth: {
|
auth: {
|
||||||
@@ -148,10 +151,39 @@ function onTimeEntryEnterPress() {
|
|||||||
const activeElement = document.activeElement as HTMLElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
activeElement?.blur();
|
activeElement?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRunningInDifferentOrganization = computed(() => {
|
||||||
|
return (
|
||||||
|
currentTimeEntry.value.organization_id &&
|
||||||
|
getCurrentOrganizationId() &&
|
||||||
|
currentTimeEntry.value.organization_id !== getCurrentOrganizationId()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchToTimeEntryOrganization() {
|
||||||
|
if (currentTimeEntry.value.organization_id) {
|
||||||
|
switchOrganization(currentTimeEntry.value.organization_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
|
||||||
|
v-if="isRunningInDifferentOrganization">
|
||||||
|
<div
|
||||||
|
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div>
|
||||||
|
<div class="flex space-x-3 items-center w-full z-20 justify-center">
|
||||||
|
<span class="text-sm text-white">
|
||||||
|
The Timer is running in a different organization.
|
||||||
|
</span>
|
||||||
|
<SecondaryButton @click="switchToTimeEntryOrganization"
|
||||||
|
>Switch to organization</SecondaryButton
|
||||||
|
>
|
||||||
|
</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">
|
||||||
@@ -199,11 +231,13 @@ function onTimeEntryEnterPress() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
|
<div
|
||||||
|
class="pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
|
||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
:active="isActive"
|
:active="isActive"
|
||||||
@changed="onToggleButtonPress"
|
@changed="onToggleButtonPress"
|
||||||
size="large"></TimeTrackerStartStop>
|
size="large"></TimeTrackerStartStop>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const page = usePage<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
function currentUserHasPermission(permission: string) {
|
function currentUserHasPermission(permission: string) {
|
||||||
console.log(page.props.auth.permissions);
|
|
||||||
if (Array.isArray(page.props.auth.permissions)) {
|
if (Array.isArray(page.props.auth.permissions)) {
|
||||||
return page.props.auth.permissions.includes(permission);
|
return page.props.auth.permissions.includes(permission);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const emptyTimeEntry = {
|
|||||||
project_id: null,
|
project_id: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
billable: false,
|
billable: false,
|
||||||
|
organization_id: '',
|
||||||
} as TimeEntry;
|
} as TimeEntry;
|
||||||
|
|
||||||
export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
||||||
@@ -56,20 +57,10 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
|
|||||||
async function fetchCurrentTimeEntry() {
|
async function fetchCurrentTimeEntry() {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const timeEntriesResponse = await handleApiRequestNotifications(
|
const timeEntriesResponse = await api.getMyActiveTimeEntry({});
|
||||||
api.getTimeEntries({
|
|
||||||
queries: {
|
|
||||||
active: 'true',
|
|
||||||
user_id: getCurrentUserId(),
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
organization: organizationId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (timeEntriesResponse?.data) {
|
if (timeEntriesResponse?.data) {
|
||||||
if (timeEntriesResponse.data.length === 1) {
|
if (timeEntriesResponse.data) {
|
||||||
currentTimeEntry.value = timeEntriesResponse.data[0];
|
currentTimeEntry.value = timeEntriesResponse.data;
|
||||||
} else {
|
} else {
|
||||||
currentTimeEntry.value = { ...emptyTimeEntry };
|
currentTimeEntry.value = { ...emptyTimeEntry };
|
||||||
}
|
}
|
||||||
|
|||||||
17
resources/js/utils/useOrganization.ts
Normal file
17
resources/js/utils/useOrganization.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { router } from '@inertiajs/vue3';
|
||||||
|
import { initializeStores } from '@/utils/init';
|
||||||
|
|
||||||
|
export function switchOrganization(organizationId: string) {
|
||||||
|
router.put(
|
||||||
|
route('current-team.update'),
|
||||||
|
{
|
||||||
|
team_id: organizationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preserveState: false,
|
||||||
|
onSuccess: () => {
|
||||||
|
initializeStores();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user