mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +01:00
add format check, update prettier rules, apply rules consistently
This commit is contained in:
@@ -13,9 +13,7 @@ defineProps<{
|
||||
<template>
|
||||
<AppLayout title="API Tokens">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-text-primary leading-tight">
|
||||
API Tokens
|
||||
</h2>
|
||||
<h2 class="font-semibold text-xl text-text-primary leading-tight">API Tokens</h2>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -63,14 +63,11 @@ const manageApiTokenPermissions = (token: Token) => {
|
||||
};
|
||||
|
||||
const updateApiToken = () => {
|
||||
updateApiTokenForm.put(
|
||||
route('api-tokens.update', managingPermissionsFor.value?.id),
|
||||
{
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (managingPermissionsFor.value = null),
|
||||
}
|
||||
);
|
||||
updateApiTokenForm.put(route('api-tokens.update', managingPermissionsFor.value?.id), {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (managingPermissionsFor.value = null),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmApiTokenDeletion = (token: Token) => {
|
||||
@@ -78,14 +75,11 @@ const confirmApiTokenDeletion = (token: Token) => {
|
||||
};
|
||||
|
||||
const deleteApiToken = () => {
|
||||
deleteApiTokenForm.delete(
|
||||
route('api-tokens.destroy', apiTokenBeingDeleted.value?.id),
|
||||
{
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (apiTokenBeingDeleted.value = null),
|
||||
}
|
||||
);
|
||||
deleteApiTokenForm.delete(route('api-tokens.destroy', apiTokenBeingDeleted.value?.id), {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => (apiTokenBeingDeleted.value = null),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -96,8 +90,8 @@ const deleteApiToken = () => {
|
||||
<template #title> Create API Token </template>
|
||||
|
||||
<template #description>
|
||||
API tokens allow third-party services to authenticate with our
|
||||
application on your behalf.
|
||||
API tokens allow third-party services to authenticate with our application on your
|
||||
behalf.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
@@ -110,9 +104,7 @@ const deleteApiToken = () => {
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autofocus />
|
||||
<InputError
|
||||
:message="createApiTokenForm.errors.name"
|
||||
class="mt-2" />
|
||||
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Token Permissions -->
|
||||
@@ -120,14 +112,10 @@ const deleteApiToken = () => {
|
||||
<InputLabel for="permissions" value="Permissions" />
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="permission in availablePermissions"
|
||||
:key="permission">
|
||||
<div v-for="permission in availablePermissions" :key="permission">
|
||||
<label class="flex items-center">
|
||||
<Checkbox
|
||||
v-model:checked="
|
||||
createApiTokenForm.permissions
|
||||
"
|
||||
v-model:checked="createApiTokenForm.permissions"
|
||||
:value="permission" />
|
||||
<span class="ms-2 text-sm text-text-secondary">{{
|
||||
permission
|
||||
@@ -139,9 +127,7 @@ const deleteApiToken = () => {
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage
|
||||
:on="createApiTokenForm.recentlySuccessful"
|
||||
class="me-3">
|
||||
<ActionMessage :on="createApiTokenForm.recentlySuccessful" class="me-3">
|
||||
Created.
|
||||
</ActionMessage>
|
||||
|
||||
@@ -162,8 +148,7 @@ const deleteApiToken = () => {
|
||||
<template #title> Manage API Tokens </template>
|
||||
|
||||
<template #description>
|
||||
You may delete any of your existing tokens if they are
|
||||
no longer needed.
|
||||
You may delete any of your existing tokens if they are no longer needed.
|
||||
</template>
|
||||
|
||||
<!-- API Token List -->
|
||||
@@ -178,18 +163,14 @@ const deleteApiToken = () => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center ms-2">
|
||||
<div
|
||||
v-if="token.last_used_ago"
|
||||
class="text-sm text-gray-400">
|
||||
<div v-if="token.last_used_ago" class="text-sm text-gray-400">
|
||||
Last used {{ token.last_used_ago }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="availablePermissions.length > 0"
|
||||
class="cursor-pointer ms-6 text-sm text-gray-400 underline"
|
||||
@click="
|
||||
manageApiTokenPermissions(token)
|
||||
">
|
||||
@click="manageApiTokenPermissions(token)">
|
||||
Permissions
|
||||
</button>
|
||||
|
||||
@@ -212,8 +193,7 @@ const deleteApiToken = () => {
|
||||
|
||||
<template #content>
|
||||
<div>
|
||||
Please copy your new API token. For your security, it won't
|
||||
be shown again.
|
||||
Please copy your new API token. For your security, it won't be shown again.
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -224,39 +204,29 @@ const deleteApiToken = () => {
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="displayingToken = false">
|
||||
Close
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="displayingToken = false"> Close </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- API Token Permissions Modal -->
|
||||
<DialogModal
|
||||
:show="managingPermissionsFor != null"
|
||||
@close="managingPermissionsFor = null">
|
||||
<DialogModal :show="managingPermissionsFor != null" @close="managingPermissionsFor = null">
|
||||
<template #title> API Token Permissions </template>
|
||||
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="permission in availablePermissions"
|
||||
:key="permission">
|
||||
<div v-for="permission in availablePermissions" :key="permission">
|
||||
<label class="flex items-center">
|
||||
<Checkbox
|
||||
v-model:checked="updateApiTokenForm.permissions"
|
||||
:value="permission" />
|
||||
<span class="ms-2 text-sm text-muted">{{
|
||||
permission
|
||||
}}</span>
|
||||
<span class="ms-2 text-sm text-muted">{{ permission }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="managingPermissionsFor = null">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="managingPermissionsFor = null"> Cancel </SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
@@ -274,14 +244,10 @@ const deleteApiToken = () => {
|
||||
@close="apiTokenBeingDeleted = null">
|
||||
<template #title> Delete API Token </template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to delete this API token?
|
||||
</template>
|
||||
<template #content> Are you sure you would like to delete this API token? </template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="apiTokenBeingDeleted = null">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="apiTokenBeingDeleted = null"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
|
||||
@@ -34,8 +34,8 @@ const submit = () => {
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-text-secondary">
|
||||
This is a secure area of the application. Please confirm your
|
||||
password before continuing.
|
||||
This is a secure area of the application. Please confirm your password before
|
||||
continuing.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
@@ -29,9 +29,8 @@ const submit = () => {
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-text-secondary">
|
||||
Forgot your password? No problem. Just let us know your email
|
||||
address and we will email you a password reset link that will allow
|
||||
you to choose a new one.
|
||||
Forgot your password? No problem. Just let us know your email address and we will email
|
||||
you a password reset link that will allow you to choose a new one.
|
||||
</div>
|
||||
|
||||
<div v-if="status" class="mb-4 font-medium text-sm text-green-400">
|
||||
|
||||
@@ -101,9 +101,7 @@ const page = usePage<{
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirm Password" />
|
||||
<InputLabel for="password_confirmation" value="Confirm Password" />
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
@@ -111,9 +109,7 @@ const page = usePage<{
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password" />
|
||||
<InputError
|
||||
class="mt-2"
|
||||
:message="form.errors.password_confirmation" />
|
||||
<InputError class="mt-2" :message="form.errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -125,10 +121,7 @@ const page = usePage<{
|
||||
class="mt-4">
|
||||
<InputLabel for="terms">
|
||||
<div class="flex items-center">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
v-model:checked="form.terms"
|
||||
name="terms" />
|
||||
<Checkbox id="terms" v-model:checked="form.terms" name="terms" />
|
||||
|
||||
<div class="ms-2">
|
||||
I agree to the
|
||||
@@ -160,13 +153,10 @@ const page = usePage<{
|
||||
name="newsletter_consent" />
|
||||
|
||||
<div class="ms-2">
|
||||
I agree to receive emails about product related
|
||||
updates
|
||||
I agree to receive emails about product related updates
|
||||
</div>
|
||||
</div>
|
||||
<InputError
|
||||
class="mt-2"
|
||||
:message="form.errors.newsletter_consent" />
|
||||
<InputError class="mt-2" :message="form.errors.newsletter_consent" />
|
||||
</InputLabel>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ const submit = () => {
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirm Password" />
|
||||
<InputLabel for="password_confirmation" value="Confirm Password" />
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
@@ -71,9 +69,7 @@ const submit = () => {
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="new-password" />
|
||||
<InputError
|
||||
class="mt-2"
|
||||
:message="form.errors.password_confirmation" />
|
||||
<InputError class="mt-2" :message="form.errors.password_confirmation" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
|
||||
@@ -47,13 +47,13 @@ const submit = () => {
|
||||
|
||||
<div class="mb-4 text-sm text-text-secondary">
|
||||
<template v-if="!recovery">
|
||||
Please confirm access to your account by entering the
|
||||
authentication code provided by your authenticator application.
|
||||
Please confirm access to your account by entering the authentication code provided
|
||||
by your authenticator application.
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Please confirm access to your account by entering one of your
|
||||
emergency recovery codes.
|
||||
Please confirm access to your account by entering one of your emergency recovery
|
||||
codes.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ const submit = () => {
|
||||
form.post(route('verification.send'));
|
||||
};
|
||||
|
||||
const verificationLinkSent = computed(
|
||||
() => props.status === 'verification-link-sent'
|
||||
);
|
||||
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,16 +26,13 @@ const verificationLinkSent = computed(
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-text-secondary">
|
||||
Before continuing, could you verify your email address by clicking
|
||||
on the link we just emailed to you? If you didn't receive the email,
|
||||
we will gladly send you another.
|
||||
Before continuing, could you verify your email address by clicking on the link we just
|
||||
emailed to you? If you didn't receive the email, we will gladly send you another.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="verificationLinkSent"
|
||||
class="mb-4 font-medium text-sm text-green-400">
|
||||
A new verification link has been sent to the email address you
|
||||
provided in your profile settings.
|
||||
<div v-if="verificationLinkSent" class="mb-4 font-medium text-sm text-green-400">
|
||||
A new verification link has been sent to the email address you provided in your profile
|
||||
settings.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
@@ -40,24 +40,12 @@ const shownClients = computed(() => {
|
||||
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||
<PageTitle :icon="UserCircleIcon" title="Clients"> </PageTitle>
|
||||
<TabBar
|
||||
v-model="activeTab"
|
||||
>
|
||||
<TabBarItem
|
||||
value="active"
|
||||
>Active</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
value="archived"
|
||||
>
|
||||
Archived
|
||||
</TabBarItem>
|
||||
<TabBar v-model="activeTab">
|
||||
<TabBarItem value="active">Active</TabBarItem>
|
||||
<TabBarItem value="archived"> Archived </TabBarItem>
|
||||
</TabBar>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
v-if="canCreateClients()"
|
||||
:icon="PlusIcon"
|
||||
@click="createClient = true"
|
||||
<SecondaryButton v-if="canCreateClients()" :icon="PlusIcon" @click="createClient = true"
|
||||
>Create Client</SecondaryButton
|
||||
>
|
||||
<ClientCreateModal v-model:show="createClient"></ClientCreateModal>
|
||||
|
||||
@@ -1,52 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import TimeTracker from "@/Components/TimeTracker.vue";
|
||||
import RecentlyTrackedTasksCard from "@/Components/Dashboard/RecentlyTrackedTasksCard.vue";
|
||||
import LastSevenDaysCard from "@/Components/Dashboard/LastSevenDaysCard.vue";
|
||||
import TeamActivityCard from "@/Components/Dashboard/TeamActivityCard.vue";
|
||||
import ThisWeekOverview from "@/Components/Dashboard/ThisWeekOverview.vue";
|
||||
import ActivityGraphCard from "@/Components/Dashboard/ActivityGraphCard.vue";
|
||||
import MainContainer from "@/packages/ui/src/MainContainer.vue";
|
||||
import { canViewMembers } from "@/utils/permissions";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import TimeTracker from '@/Components/TimeTracker.vue';
|
||||
import RecentlyTrackedTasksCard from '@/Components/Dashboard/RecentlyTrackedTasksCard.vue';
|
||||
import LastSevenDaysCard from '@/Components/Dashboard/LastSevenDaysCard.vue';
|
||||
import TeamActivityCard from '@/Components/Dashboard/TeamActivityCard.vue';
|
||||
import ThisWeekOverview from '@/Components/Dashboard/ThisWeekOverview.vue';
|
||||
import ActivityGraphCard from '@/Components/Dashboard/ActivityGraphCard.vue';
|
||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||
import { canViewMembers } from '@/utils/permissions';
|
||||
import { useQueryClient } from '@tanstack/vue-query';
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const refreshDashboardData = () => {
|
||||
// Invalidate all dashboard queries to trigger refetching
|
||||
queryClient.invalidateQueries({ queryKey: ["latestTasks"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["lastSevenDays"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["dailyTrackedHours"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["latestTeamActivity"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["weeklyProjectOverview"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["totalWeeklyTime"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["totalWeeklyBillableTime"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["totalWeeklyBillableAmount"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["weeklyHistory"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["timeEntries"] });
|
||||
queryClient.invalidateQueries({ queryKey: ['latestTasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['lastSevenDays'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['dailyTrackedHours'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['latestTeamActivity'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['weeklyProjectOverview'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['totalWeeklyTime'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['totalWeeklyBillableTime'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['totalWeeklyBillableAmount'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['weeklyHistory'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Dashboard" data-testid="dashboard_view">
|
||||
<MainContainer
|
||||
class="pt-5 sm:pt-8 pb-4 sm:pb-6 border-b border-default-background-separator">
|
||||
<TimeTracker @change="refreshDashboardData"></TimeTracker>
|
||||
</MainContainer>
|
||||
<MainContainer
|
||||
class="pt-5 sm:pt-8 pb-4 sm:pb-6 border-b border-default-background-separator">
|
||||
<TimeTracker @change="refreshDashboardData"></TimeTracker>
|
||||
</MainContainer>
|
||||
|
||||
<MainContainer
|
||||
class="grid gap-5 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pt-3 sm:pt-5 pb-4 sm:pb-6 border-b border-default-background-separator items-stretch">
|
||||
<RecentlyTrackedTasksCard></RecentlyTrackedTasksCard>
|
||||
<LastSevenDaysCard></LastSevenDaysCard>
|
||||
<ActivityGraphCard></ActivityGraphCard>
|
||||
<TeamActivityCard
|
||||
v-if="canViewMembers()"
|
||||
class="flex lg:hidden xl:flex">
|
||||
</TeamActivityCard>
|
||||
</MainContainer>
|
||||
<MainContainer class="py-5">
|
||||
<ThisWeekOverview></ThisWeekOverview>
|
||||
</MainContainer>
|
||||
<MainContainer
|
||||
class="grid gap-5 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pt-3 sm:pt-5 pb-4 sm:pb-6 border-b border-default-background-separator items-stretch">
|
||||
<RecentlyTrackedTasksCard></RecentlyTrackedTasksCard>
|
||||
<LastSevenDaysCard></LastSevenDaysCard>
|
||||
<ActivityGraphCard></ActivityGraphCard>
|
||||
<TeamActivityCard v-if="canViewMembers()" class="flex lg:hidden xl:flex">
|
||||
</TeamActivityCard>
|
||||
</MainContainer>
|
||||
<MainContainer class="py-5">
|
||||
<ThisWeekOverview></ThisWeekOverview>
|
||||
</MainContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -12,8 +12,7 @@ import ExportData from '@/Pages/Teams/Partials/ExportData.vue';
|
||||
<MainContainer
|
||||
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||
<div class="flex items-center space-x-6">
|
||||
<PageTitle :icon="ArrowsRightLeftIcon" title="Import / Export">
|
||||
</PageTitle>
|
||||
<PageTitle :icon="ArrowsRightLeftIcon" title="Import / Export"> </PageTitle>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<MainContainer class="py-6 space-y-4">
|
||||
|
||||
@@ -29,17 +29,9 @@ const activeTab = ref<'all' | 'invitations'>('all');
|
||||
class="py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||
<div class="flex items-center space-x-4 sm:space-x-6">
|
||||
<PageTitle :icon="UserGroupIcon" title="Members"> </PageTitle>
|
||||
<TabBar
|
||||
v-model="activeTab"
|
||||
>
|
||||
<TabBarItem
|
||||
value="all"
|
||||
>All</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
value="invitations"
|
||||
>Invitations</TabBarItem
|
||||
>
|
||||
<TabBar v-model="activeTab">
|
||||
<TabBarItem value="all">All</TabBarItem>
|
||||
<TabBarItem value="invitations">Invitations</TabBarItem>
|
||||
</TabBar>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import {computed, ref, inject, type ComputedRef} from 'vue';
|
||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import {
|
||||
api,
|
||||
type ApiToken,
|
||||
type CreateApiTokenBody
|
||||
} from '@/packages/api/src';
|
||||
import SectionBorder from "@/Components/SectionBorder.vue";
|
||||
import DangerButton from "@/packages/ui/src/Buttons/DangerButton.vue";
|
||||
import TextInput from "../../../packages/ui/src/Input/TextInput.vue";
|
||||
import SecondaryButton from "../../../packages/ui/src/Buttons/SecondaryButton.vue";
|
||||
import DialogModal from "@/packages/ui/src/DialogModal.vue";
|
||||
import InputError from "@/packages/ui/src/Input/InputError.vue";
|
||||
import ActionMessage from "@/Components/ActionMessage.vue";
|
||||
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
|
||||
import ActionSection from "@/Components/ActionSection.vue";
|
||||
import {useForm} from "@inertiajs/vue3";
|
||||
import {useMutation, useQuery, useQueryClient} from "@tanstack/vue-query";
|
||||
import {useNotificationsStore} from "@/utils/notification";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
import { formatDateTimeLocalized} from "../../../packages/ui/src/utils/time";
|
||||
import {ClockIcon} from "@heroicons/vue/20/solid";
|
||||
import { api, type ApiToken, type CreateApiTokenBody } from '@/packages/api/src';
|
||||
import SectionBorder from '@/Components/SectionBorder.vue';
|
||||
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
|
||||
import TextInput from '../../../packages/ui/src/Input/TextInput.vue';
|
||||
import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import InputError from '@/packages/ui/src/Input/InputError.vue';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
|
||||
import ActionSection from '@/Components/ActionSection.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { formatDateTimeLocalized } from '../../../packages/ui/src/utils/time';
|
||||
import { ClockIcon } from '@heroicons/vue/20/solid';
|
||||
import type { Organization } from '@/packages/api/src';
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
@@ -37,7 +33,7 @@ const { copy, copied, isSupported } = useClipboard();
|
||||
|
||||
const organization = inject<ComputedRef<Organization>>('organization');
|
||||
|
||||
async function createApiToken(){
|
||||
async function createApiToken() {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
createApiTokenMutation.mutateAsync({
|
||||
@@ -57,21 +53,20 @@ const createApiTokenForm = useForm({
|
||||
name: '',
|
||||
});
|
||||
|
||||
function confirmApiTokenDeletion (token: ApiToken) {
|
||||
function confirmApiTokenDeletion(token: ApiToken) {
|
||||
apiTokenBeingDeleted.value = token;
|
||||
}
|
||||
|
||||
function confirmApiTokenRevocation(token: ApiToken){
|
||||
function confirmApiTokenRevocation(token: ApiToken) {
|
||||
apiTokenBeingRevoked.value = token;
|
||||
}
|
||||
|
||||
const displayingToken = ref(false);
|
||||
|
||||
async function deleteApiToken () {
|
||||
if(apiTokenBeingDeleted.value){
|
||||
async function deleteApiToken() {
|
||||
if (apiTokenBeingDeleted.value) {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
deleteApiTokenMutation.mutateAsync(apiTokenBeingDeleted.value!.id),
|
||||
() => deleteApiTokenMutation.mutateAsync(apiTokenBeingDeleted.value!.id),
|
||||
'API Token successfully deleted',
|
||||
'There was an error while deleting the API Token',
|
||||
() => {
|
||||
@@ -79,13 +74,12 @@ async function deleteApiToken () {
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function revokeApiToken () {
|
||||
if(apiTokenBeingRevoked.value){
|
||||
async function revokeApiToken() {
|
||||
if (apiTokenBeingRevoked.value) {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
revokeApiTokenMutation.mutateAsync(apiTokenBeingRevoked.value!.id),
|
||||
() => revokeApiTokenMutation.mutateAsync(apiTokenBeingRevoked.value!.id),
|
||||
'API Token successfully revoked',
|
||||
'There was an error while revoking the API Token',
|
||||
() => {
|
||||
@@ -93,25 +87,23 @@ async function revokeApiToken () {
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const { data: sharedReportResponseData } = useQuery({
|
||||
queryKey: ['api-tokens'],
|
||||
queryFn: () =>
|
||||
api.getApiTokens(),
|
||||
queryFn: () => api.getApiTokens(),
|
||||
});
|
||||
|
||||
const tokens = computed(() => {
|
||||
return sharedReportResponseData.value?.data ?? [];
|
||||
})
|
||||
});
|
||||
|
||||
const createApiTokenMutation = useMutation({
|
||||
mutationFn: async (apiToken: CreateApiTokenBody) => {
|
||||
return await api.createApiToken(apiToken);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -124,7 +116,7 @@ const deleteApiTokenMutation = useMutation({
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -137,12 +129,9 @@ const revokeApiTokenMutation = useMutation({
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -152,8 +141,8 @@ const revokeApiTokenMutation = useMutation({
|
||||
<template #title> Create API Token </template>
|
||||
|
||||
<template #description>
|
||||
API tokens allow third-party services to authenticate with our
|
||||
application on your behalf.
|
||||
API tokens allow third-party services to authenticate with our application on your
|
||||
behalf.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
@@ -165,22 +154,17 @@ const revokeApiTokenMutation = useMutation({
|
||||
v-model="createApiTokenForm.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full" />
|
||||
<InputError
|
||||
:message="createApiTokenForm.errors.name"
|
||||
class="mt-2" />
|
||||
<div class="text-text-tertiary text-sm pt-3 flex space-x-1.5 font-medium items-center">
|
||||
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
|
||||
<div
|
||||
class="text-text-tertiary text-sm pt-3 flex space-x-1.5 font-medium items-center">
|
||||
<ClockIcon class="w-4"></ClockIcon>
|
||||
<span>
|
||||
API Tokens are valid for 1 year
|
||||
</span>
|
||||
<span> API Tokens are valid for 1 year </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage
|
||||
:on="createApiTokenForm.recentlySuccessful"
|
||||
class="me-3">
|
||||
<ActionMessage :on="createApiTokenForm.recentlySuccessful" class="me-3">
|
||||
Created.
|
||||
</ActionMessage>
|
||||
|
||||
@@ -201,8 +185,8 @@ const revokeApiTokenMutation = useMutation({
|
||||
<template #title> Manage API Tokens </template>
|
||||
|
||||
<template #description>
|
||||
You may delete or revoke any of your existing tokens if they are
|
||||
no longer needed.
|
||||
You may delete or revoke any of your existing tokens if they are no longer
|
||||
needed.
|
||||
</template>
|
||||
|
||||
<!-- API Token List -->
|
||||
@@ -216,21 +200,31 @@ const revokeApiTokenMutation = useMutation({
|
||||
<div>{{ token.name }}</div>
|
||||
<div class="text-sm text-text-tertiary space-x-3">
|
||||
<span v-if="token.created_at">
|
||||
Created at {{ formatDateTimeLocalized(token.created_at, organization?.date_format, organization?.time_format) }}
|
||||
Created at
|
||||
{{
|
||||
formatDateTimeLocalized(
|
||||
token.created_at,
|
||||
organization?.date_format,
|
||||
organization?.time_format
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="token.expires_at">
|
||||
Expires at {{ formatDateTimeLocalized(token.expires_at, organization?.date_format, organization?.time_format) }}
|
||||
</span>
|
||||
<span v-if="token.revoked">
|
||||
Revoked
|
||||
Expires at
|
||||
{{
|
||||
formatDateTimeLocalized(
|
||||
token.expires_at,
|
||||
organization?.date_format,
|
||||
organization?.time_format
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="token.revoked"> Revoked </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center ms-2">
|
||||
<div
|
||||
v-if="token.last_used_ago"
|
||||
class="text-sm text-gray-400">
|
||||
<div v-if="token.last_used_ago" class="text-sm text-gray-400">
|
||||
Last used {{ token.last_used_ago }}
|
||||
</div>
|
||||
<button
|
||||
@@ -260,25 +254,26 @@ const revokeApiTokenMutation = useMutation({
|
||||
|
||||
<template #content>
|
||||
<div>
|
||||
Please copy your new API token. For your security, it won't
|
||||
be shown again.
|
||||
Please copy your new API token. For your security, it won't be shown again.
|
||||
<strong>This token is valid for one year</strong> unless you revoke it.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
</div>
|
||||
<div></div>
|
||||
|
||||
<div class="flex gap-2 pt-6 w-full">
|
||||
<TextInput v-if="newToken" disabled :model-value="newToken" class="flex-1 text-gray-500"></TextInput>
|
||||
<PrimaryButton v-if="isSupported" @click="copy(newToken)">{{ copied ? 'Copied!' : 'Copy Token' }}</PrimaryButton>
|
||||
<TextInput
|
||||
v-if="newToken"
|
||||
disabled
|
||||
:model-value="newToken"
|
||||
class="flex-1 text-gray-500"></TextInput>
|
||||
<PrimaryButton v-if="isSupported" @click="copy(newToken)">{{
|
||||
copied ? 'Copied!' : 'Copy Token'
|
||||
}}</PrimaryButton>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="displayingToken = false">
|
||||
Close
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="displayingToken = false"> Close </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
@@ -288,14 +283,10 @@ const revokeApiTokenMutation = useMutation({
|
||||
@close="apiTokenBeingDeleted = null">
|
||||
<template #title> Delete API Token </template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to delete this API token?
|
||||
</template>
|
||||
<template #content> Are you sure you would like to delete this API token? </template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="apiTokenBeingDeleted = null">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="apiTokenBeingDeleted = null"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
@@ -312,14 +303,10 @@ const revokeApiTokenMutation = useMutation({
|
||||
@close="apiTokenBeingRevoked = null">
|
||||
<template #title> Revoke API Token </template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to revoke this API token?
|
||||
</template>
|
||||
<template #content> Are you sure you would like to revoke this API token? </template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="apiTokenBeingRevoked = null">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="apiTokenBeingRevoked = null"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
|
||||
@@ -45,15 +45,13 @@ const closeModal = () => {
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-text-secondary">
|
||||
Once your account is deleted, all of its resources and data will
|
||||
be permanently deleted. Before deleting your account, please
|
||||
download any data or information that you wish to retain.
|
||||
Once your account is deleted, all of its resources and data will be permanently
|
||||
deleted. Before deleting your account, please download any data or information that
|
||||
you wish to retain.
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<DangerButton @click="confirmUserDeletion">
|
||||
Delete Account
|
||||
</DangerButton>
|
||||
<DangerButton @click="confirmUserDeletion"> Delete Account </DangerButton>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account Confirmation Modal -->
|
||||
@@ -61,10 +59,9 @@ const closeModal = () => {
|
||||
<template #title> Delete Account </template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you want to delete your account? Once your
|
||||
account is deleted, all of its resources and data will be
|
||||
permanently deleted. Please enter your password to confirm
|
||||
you would like to permanently delete your account.
|
||||
Are you sure you want to delete your account? Once your account is deleted, all
|
||||
of its resources and data will be permanently deleted. Please enter your
|
||||
password to confirm you would like to permanently delete your account.
|
||||
|
||||
<div class="mt-4">
|
||||
<TextInput
|
||||
@@ -76,16 +73,12 @@ const closeModal = () => {
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="deleteUser" />
|
||||
|
||||
<InputError
|
||||
:message="form.errors.password"
|
||||
class="mt-2" />
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
|
||||
@@ -48,25 +48,20 @@ const closeModal = () => {
|
||||
<template #title> Browser Sessions </template>
|
||||
|
||||
<template #description>
|
||||
Manage and log out your active sessions on other browsers and
|
||||
devices.
|
||||
Manage and log out your active sessions on other browsers and devices.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-text-secondary">
|
||||
If necessary, you may log out of all of your other browser
|
||||
sessions across all of your devices. Some of your recent
|
||||
sessions are listed below; however, this list may not be
|
||||
exhaustive. If you feel your account has been compromised, you
|
||||
should also update your password.
|
||||
If necessary, you may log out of all of your other browser sessions across all of
|
||||
your devices. Some of your recent sessions are listed below; however, this list may
|
||||
not be exhaustive. If you feel your account has been compromised, you should also
|
||||
update your password.
|
||||
</div>
|
||||
|
||||
<!-- Other Browser Sessions -->
|
||||
<div v-if="sessions.length > 0" class="mt-5 space-y-6">
|
||||
<div
|
||||
v-for="(session, i) in sessions"
|
||||
:key="i"
|
||||
class="flex items-center">
|
||||
<div v-for="(session, i) in sessions" :key="i" class="flex items-center">
|
||||
<div>
|
||||
<svg
|
||||
v-if="session.agent.is_desktop"
|
||||
@@ -99,17 +94,9 @@ const closeModal = () => {
|
||||
|
||||
<div class="ms-3">
|
||||
<div class="text-sm text-text-primary font-medium">
|
||||
{{
|
||||
session.agent.platform
|
||||
? session.agent.platform
|
||||
: 'Unknown'
|
||||
}}
|
||||
{{ session.agent.platform ? session.agent.platform : 'Unknown' }}
|
||||
-
|
||||
{{
|
||||
session.agent.browser
|
||||
? session.agent.browser
|
||||
: 'Unknown'
|
||||
}}
|
||||
{{ session.agent.browser ? session.agent.browser : 'Unknown' }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -121,9 +108,7 @@ const closeModal = () => {
|
||||
class="text-green-500 font-semibold"
|
||||
>This device</span
|
||||
>
|
||||
<span v-else
|
||||
>Last active {{ session.last_active }}</span
|
||||
>
|
||||
<span v-else>Last active {{ session.last_active }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,9 +120,7 @@ const closeModal = () => {
|
||||
Log Out Other Browser Sessions
|
||||
</PrimaryButton>
|
||||
|
||||
<ActionMessage :on="form.recentlySuccessful" class="ms-3">
|
||||
Done.
|
||||
</ActionMessage>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="ms-3"> Done. </ActionMessage>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
@@ -145,9 +128,8 @@ const closeModal = () => {
|
||||
<template #title> Log Out Other Browser Sessions </template>
|
||||
|
||||
<template #content>
|
||||
Please enter your password to confirm you would like to log
|
||||
out of your other browser sessions across all of your
|
||||
devices.
|
||||
Please enter your password to confirm you would like to log out of your other
|
||||
browser sessions across all of your devices.
|
||||
|
||||
<div class="mt-4">
|
||||
<TextInput
|
||||
@@ -159,16 +141,12 @@ const closeModal = () => {
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="logoutOtherBrowserSessions" />
|
||||
|
||||
<InputError
|
||||
:message="form.errors.password"
|
||||
class="mt-2" />
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import InputLabel from "../../../packages/ui/src/Input/InputLabel.vue";
|
||||
import FormSection from "@/Components/FormSection.vue";
|
||||
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import { usePreferredColorScheme } from '@vueuse/core';
|
||||
import { themeSetting } from "@/utils/theme";
|
||||
|
||||
const preferredColor = usePreferredColorScheme()
|
||||
import { themeSetting } from '@/utils/theme';
|
||||
|
||||
const preferredColor = usePreferredColorScheme();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection>
|
||||
<template #title> Theme</template>
|
||||
|
||||
<template #description>
|
||||
Choose how you want solidtime to look on your device
|
||||
</template>
|
||||
<template #description> Choose how you want solidtime to look on your device </template>
|
||||
|
||||
<template #form>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="theme" value="Theme" />
|
||||
<select
|
||||
@@ -31,15 +26,14 @@ const preferredColor = usePreferredColorScheme()
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
<p v-if="themeSetting === 'system'" class="w-full text-sm text-text-tertiary font-medium mt-2">
|
||||
<p
|
||||
v-if="themeSetting === 'system'"
|
||||
class="w-full text-sm text-text-tertiary font-medium mt-2">
|
||||
System default: {{ preferredColor }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</FormSection>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -51,12 +51,7 @@ const enableTwoFactorAuthentication = () => {
|
||||
{},
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () =>
|
||||
Promise.all([
|
||||
showQrCode(),
|
||||
showSetupKey(),
|
||||
showRecoveryCodes(),
|
||||
]),
|
||||
onSuccess: () => Promise.all([showQrCode(), showSetupKey(), showRecoveryCodes()]),
|
||||
onFinish: () => {
|
||||
enabling.value = false;
|
||||
confirming.value = props.requiresConfirmation;
|
||||
@@ -97,9 +92,7 @@ const confirmTwoFactorAuthentication = () => {
|
||||
};
|
||||
|
||||
const regenerateRecoveryCodes = () => {
|
||||
axios
|
||||
.post(route('two-factor.recovery-codes'))
|
||||
.then(() => showRecoveryCodes());
|
||||
axios.post(route('two-factor.recovery-codes')).then(() => showRecoveryCodes());
|
||||
};
|
||||
|
||||
const disableTwoFactorAuthentication = () => {
|
||||
@@ -120,8 +113,7 @@ const disableTwoFactorAuthentication = () => {
|
||||
<template #title> Two Factor Authentication </template>
|
||||
|
||||
<template #description>
|
||||
Add additional security to your account using two factor
|
||||
authentication.
|
||||
Add additional security to your account using two factor authentication.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
@@ -143,10 +135,9 @@ const disableTwoFactorAuthentication = () => {
|
||||
|
||||
<div class="mt-3 max-w-xl text-sm text-text-secondary">
|
||||
<p>
|
||||
When two factor authentication is enabled, you will be
|
||||
prompted for a secure, random token during authentication.
|
||||
You may retrieve this token from your phone's Google
|
||||
Authenticator application.
|
||||
When two factor authentication is enabled, you will be prompted for a secure,
|
||||
random token during authentication. You may retrieve this token from your
|
||||
phone's Google Authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -154,29 +145,21 @@ const disableTwoFactorAuthentication = () => {
|
||||
<div v-if="qrCode">
|
||||
<div class="mt-4 max-w-xl text-sm text-text-secondary">
|
||||
<p v-if="confirming" class="font-semibold">
|
||||
To finish enabling two factor authentication, scan
|
||||
the following QR code using your phone's
|
||||
authenticator application or enter the setup key and
|
||||
To finish enabling two factor authentication, scan the following QR code
|
||||
using your phone's authenticator application or enter the setup key and
|
||||
provide the generated OTP code.
|
||||
</p>
|
||||
|
||||
<p v-else>
|
||||
Two factor authentication is now enabled. Scan the
|
||||
following QR code using your phone's authenticator
|
||||
application or enter the setup key.
|
||||
Two factor authentication is now enabled. Scan the following QR code
|
||||
using your phone's authenticator application or enter the setup key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 p-2 inline-block bg-white"
|
||||
v-html="qrCode" />
|
||||
<div class="mt-4 p-2 inline-block bg-white" v-html="qrCode" />
|
||||
|
||||
<div
|
||||
v-if="setupKey"
|
||||
class="mt-4 max-w-xl text-sm text-text-secondary">
|
||||
<p class="font-semibold">
|
||||
Setup Key: <span v-html="setupKey"></span>
|
||||
</p>
|
||||
<div v-if="setupKey" class="mt-4 max-w-xl text-sm text-text-secondary">
|
||||
<p class="font-semibold">Setup Key: <span v-html="setupKey"></span></p>
|
||||
</div>
|
||||
|
||||
<div v-if="confirming" class="mt-4">
|
||||
@@ -193,19 +176,16 @@ const disableTwoFactorAuthentication = () => {
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="confirmTwoFactorAuthentication" />
|
||||
|
||||
<InputError
|
||||
:message="confirmationForm.errors.code"
|
||||
class="mt-2" />
|
||||
<InputError :message="confirmationForm.errors.code" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="recoveryCodes.length > 0 && !confirming">
|
||||
<div class="mt-4 max-w-xl text-sm text-muted">
|
||||
<p class="font-semibold">
|
||||
Store these recovery codes in a secure password
|
||||
manager. They can be used to recover access to your
|
||||
account if your two factor authentication device is
|
||||
lost.
|
||||
Store these recovery codes in a secure password manager. They can be
|
||||
used to recover access to your account if your two factor authentication
|
||||
device is lost.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -220,8 +200,7 @@ const disableTwoFactorAuthentication = () => {
|
||||
|
||||
<div class="mt-5">
|
||||
<div v-if="!twoFactorEnabled">
|
||||
<ConfirmsPassword
|
||||
@confirmed="enableTwoFactorAuthentication">
|
||||
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
:class="{ 'opacity-25': enabling }"
|
||||
@@ -232,8 +211,7 @@ const disableTwoFactorAuthentication = () => {
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ConfirmsPassword
|
||||
@confirmed="confirmTwoFactorAuthentication">
|
||||
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||
<PrimaryButton
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
@@ -260,8 +238,7 @@ const disableTwoFactorAuthentication = () => {
|
||||
</SecondaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword
|
||||
@confirmed="disableTwoFactorAuthentication">
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<SecondaryButton
|
||||
v-if="confirming"
|
||||
:class="disabling ? 'opacity-25' : ''"
|
||||
@@ -270,8 +247,7 @@ const disableTwoFactorAuthentication = () => {
|
||||
</SecondaryButton>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword
|
||||
@confirmed="disableTwoFactorAuthentication">
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<DangerButton
|
||||
v-if="!confirming"
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
|
||||
@@ -55,9 +55,7 @@ const updatePassword = () => {
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password" />
|
||||
<InputError
|
||||
:message="form.errors.current_password"
|
||||
class="mt-2" />
|
||||
<InputError :message="form.errors.current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
@@ -73,29 +71,21 @@ const updatePassword = () => {
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirm Password" />
|
||||
<InputLabel for="password_confirmation" value="Confirm Password" />
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password" />
|
||||
<InputError
|
||||
:message="form.errors.password_confirmation"
|
||||
class="mt-2" />
|
||||
<InputError :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3">
|
||||
Saved.
|
||||
</ActionMessage>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3"> Saved. </ActionMessage>
|
||||
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing">
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -28,11 +28,7 @@ const photoPreview = ref<ArrayBuffer | undefined | string | null>(null);
|
||||
const photoInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const updateProfileInformation = () => {
|
||||
if (
|
||||
photoInput.value &&
|
||||
photoInput.value.files &&
|
||||
photoInput.value.files?.length > 0
|
||||
) {
|
||||
if (photoInput.value && photoInput.value.files && photoInput.value.files?.length > 0) {
|
||||
form.photo = photoInput.value?.files[0];
|
||||
}
|
||||
|
||||
@@ -100,9 +96,7 @@ const page = usePage<{
|
||||
|
||||
<template #form>
|
||||
<!-- Profile Photo -->
|
||||
<div
|
||||
v-if="page.props.jetstream.managesProfilePhotos"
|
||||
class="col-span-6 sm:col-span-4">
|
||||
<div v-if="page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4">
|
||||
<!-- Profile Photo File Input -->
|
||||
<input
|
||||
id="photo"
|
||||
@@ -125,15 +119,10 @@ const page = usePage<{
|
||||
<div v-show="photoPreview" class="mt-2">
|
||||
<span
|
||||
class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
:style="
|
||||
'background-image: url(\'' + photoPreview + '\');'
|
||||
" />
|
||||
:style="'background-image: url(\'' + photoPreview + '\');'" />
|
||||
</div>
|
||||
|
||||
<SecondaryButton
|
||||
class="mt-2 me-2"
|
||||
type="button"
|
||||
@click.prevent="selectNewPhoto">
|
||||
<SecondaryButton class="mt-2 me-2" type="button" @click.prevent="selectNewPhoto">
|
||||
Select A New Photo
|
||||
</SecondaryButton>
|
||||
|
||||
@@ -175,8 +164,7 @@ const page = usePage<{
|
||||
|
||||
<div
|
||||
v-if="
|
||||
page.props.jetstream.hasEmailVerification &&
|
||||
user.email_verified_at === null
|
||||
page.props.jetstream.hasEmailVerification && user.email_verified_at === null
|
||||
">
|
||||
<p class="text-sm mt-2 text-text-primary">
|
||||
Your email address is unverified.
|
||||
@@ -194,8 +182,7 @@ const page = usePage<{
|
||||
<div
|
||||
v-show="verificationLinkSent"
|
||||
class="mt-2 font-medium text-sm text-green-400">
|
||||
A new verification link has been sent to your email
|
||||
address.
|
||||
A new verification link has been sent to your email address.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,8 +198,7 @@ const page = usePage<{
|
||||
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option value="" disabled>Select a Timezone</option>
|
||||
<option
|
||||
v-for="(timezoneTranslated, timezoneKey) in $page.props
|
||||
.timezones"
|
||||
v-for="(timezoneTranslated, timezoneKey) in $page.props.timezones"
|
||||
:key="timezoneKey"
|
||||
:value="timezoneKey">
|
||||
{{ timezoneTranslated }}
|
||||
@@ -232,8 +218,7 @@ const page = usePage<{
|
||||
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option value="" disabled>Select a week day</option>
|
||||
<option
|
||||
v-for="(weekdayTranslated, weekdayKey) in $page.props
|
||||
.weekdays"
|
||||
v-for="(weekdayTranslated, weekdayKey) in $page.props.weekdays"
|
||||
:key="weekdayKey"
|
||||
:value="weekdayKey">
|
||||
{{ weekdayTranslated }}
|
||||
@@ -244,13 +229,9 @@ const page = usePage<{
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3">
|
||||
Saved.
|
||||
</ActionMessage>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3"> Saved. </ActionMessage>
|
||||
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing">
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -9,8 +9,8 @@ import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfile
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import type { User } from '@/types/models';
|
||||
import type { Session } from '@/types/jetstream';
|
||||
import ApiTokensForm from "@/Pages/Profile/Partials/ApiTokensForm.vue";
|
||||
import ThemeForm from "@/Pages/Profile/Partials/ThemeForm.vue";
|
||||
import ApiTokensForm from '@/Pages/Profile/Partials/ApiTokensForm.vue';
|
||||
import ThemeForm from '@/Pages/Profile/Partials/ThemeForm.vue';
|
||||
|
||||
defineProps<{
|
||||
confirmsTwoFactorAuthentication: boolean;
|
||||
@@ -33,16 +33,13 @@ const page = usePage<{
|
||||
<template>
|
||||
<AppLayout title="Profile">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-text-primary leading-tight">
|
||||
Profile
|
||||
</h2>
|
||||
<h2 class="font-semibold text-xl text-text-primary leading-tight">Profile</h2>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
<div v-if="page.props.jetstream.canUpdateProfileInformation">
|
||||
<UpdateProfileInformationForm
|
||||
:user="page.props.auth.user" />
|
||||
<UpdateProfileInformationForm :user="page.props.auth.user" />
|
||||
|
||||
<SectionBorder />
|
||||
</div>
|
||||
@@ -59,10 +56,7 @@ const page = usePage<{
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
page.props.jetstream.canManageTwoFactorAuthentication
|
||||
">
|
||||
<div v-if="page.props.jetstream.canManageTwoFactorAuthentication">
|
||||
<TwoFactorAuthenticationForm
|
||||
:requires-confirmation="confirmsTwoFactorAuthentication"
|
||||
class="mt-10 sm:mt-0" />
|
||||
@@ -70,15 +64,12 @@ const page = usePage<{
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<LogoutOtherBrowserSessionsForm
|
||||
:sessions="sessions"
|
||||
class="mt-10 sm:mt-0" />
|
||||
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
|
||||
<SectionBorder />
|
||||
|
||||
<ApiTokensForm></ApiTokensForm>
|
||||
|
||||
<template
|
||||
v-if="page.props.jetstream.hasAccountDeletionFeatures">
|
||||
<template v-if="page.props.jetstream.hasAccountDeletionFeatures">
|
||||
<SectionBorder />
|
||||
|
||||
<DeleteUserForm class="mt-10 sm:mt-0" />
|
||||
|
||||
@@ -21,11 +21,7 @@ import Card from '@/Components/Common/Card.vue';
|
||||
import ProjectMemberTable from '@/Components/Common/ProjectMember/ProjectMemberTable.vue';
|
||||
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
|
||||
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
||||
import {
|
||||
canCreateProjects,
|
||||
canCreateTasks,
|
||||
canViewProjectMembers,
|
||||
} from '@/utils/permissions';
|
||||
import { canCreateProjects, canCreateTasks, canViewProjectMembers } from '@/utils/permissions';
|
||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||
import { useTasksStore } from '@/utils/useTasks';
|
||||
@@ -40,11 +36,7 @@ const { projects } = storeToRefs(useProjectsStore());
|
||||
const organization = inject<ComputedRef<Organization>>('organization');
|
||||
|
||||
const project = computed(() => {
|
||||
return (
|
||||
projects.value.find(
|
||||
(project) => project.id === route().params.project
|
||||
) ?? null
|
||||
);
|
||||
return projects.value.find((project) => project.id === route().params.project) ?? null;
|
||||
});
|
||||
const createTask = ref(false);
|
||||
const createProjectMember = ref(false);
|
||||
@@ -83,11 +75,8 @@ const shownTasks = computed(() => {
|
||||
<ol role="list" class="flex items-center space-x-2">
|
||||
<li>
|
||||
<div class="flex items-center space-x-6">
|
||||
<Link
|
||||
:href="route('projects')"
|
||||
class="flex items-center space-x-2.5">
|
||||
<FolderIcon
|
||||
class="w-6 text-icon-default"></FolderIcon>
|
||||
<Link :href="route('projects')" class="flex items-center space-x-2.5">
|
||||
<FolderIcon class="w-6 text-icon-default"></FolderIcon>
|
||||
<span class="font-medium">Projects</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -123,8 +112,7 @@ const shownTasks = computed(() => {
|
||||
}}
|
||||
/ h
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="project?.is_billable && !project?.billable_rate">
|
||||
<Badge v-if="project?.is_billable && !project?.billable_rate">
|
||||
Default Rate
|
||||
</Badge>
|
||||
<Badge v-if="!project?.is_billable"> Non-Billable </Badge>
|
||||
@@ -148,16 +136,11 @@ const shownTasks = computed(() => {
|
||||
<div>
|
||||
<CardTitle title="Tasks" :icon="CheckCircleIcon">
|
||||
<template #actions>
|
||||
<div
|
||||
class="w-full items-center flex justify-between">
|
||||
<div class="w-full items-center flex justify-between">
|
||||
<div class="pl-6">
|
||||
<TabBar v-model="activeTab">
|
||||
<TabBarItem value="active"
|
||||
>Active
|
||||
</TabBarItem>
|
||||
<TabBarItem value="done"
|
||||
>Done
|
||||
</TabBarItem>
|
||||
<TabBarItem value="active">Active </TabBarItem>
|
||||
<TabBarItem value="done">Done </TabBarItem>
|
||||
</TabBar>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
@@ -173,25 +156,19 @@ const shownTasks = computed(() => {
|
||||
</template>
|
||||
</CardTitle>
|
||||
<Card>
|
||||
<TaskTable
|
||||
:tasks="shownTasks"
|
||||
:project-id="projectId"></TaskTable>
|
||||
<TaskTable :tasks="shownTasks" :project-id="projectId"></TaskTable>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="canViewProjectMembers()">
|
||||
<CardTitle title="Project Members" :icon="UserGroupIcon">
|
||||
<template #actions>
|
||||
<SecondaryButton
|
||||
:icon="PlusIcon"
|
||||
@click="createProjectMember = true">
|
||||
<SecondaryButton :icon="PlusIcon" @click="createProjectMember = true">
|
||||
Add Member
|
||||
</SecondaryButton>
|
||||
<ProjectMemberCreateModal
|
||||
v-model:show="createProjectMember"
|
||||
:project-id="projectId"
|
||||
:existing-members="
|
||||
projectMembers
|
||||
"></ProjectMemberCreateModal>
|
||||
:existing-members="projectMembers"></ProjectMemberCreateModal>
|
||||
</template>
|
||||
</CardTitle>
|
||||
<Card>
|
||||
|
||||
@@ -13,12 +13,7 @@ import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import type {
|
||||
CreateClientBody,
|
||||
Client,
|
||||
CreateProjectBody,
|
||||
Project,
|
||||
} from '@/packages/api/src';
|
||||
import type { CreateClientBody, Client, CreateProjectBody, Project } from '@/packages/api/src';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import { getCurrentRole } from '@/utils/useUser';
|
||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||
@@ -45,21 +40,16 @@ const shownProjects = computed(() => {
|
||||
return project.is_archived;
|
||||
});
|
||||
});
|
||||
async function createProject(
|
||||
project: CreateProjectBody
|
||||
): Promise<Project | undefined> {
|
||||
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
||||
return await useProjectsStore().createProject(project);
|
||||
}
|
||||
async function createClient(
|
||||
client: CreateClientBody
|
||||
): Promise<Client | undefined> {
|
||||
async function createClient(client: CreateClientBody): Promise<Client | undefined> {
|
||||
return await useClientsStore().createClient(client);
|
||||
}
|
||||
|
||||
const showBillableRate = computed(() => {
|
||||
return !!(
|
||||
getCurrentRole() !== 'employee' ||
|
||||
organization.value?.employees_can_see_billable_rates
|
||||
getCurrentRole() !== 'employee' || organization.value?.employees_can_see_billable_rates
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -70,16 +60,9 @@ const showBillableRate = computed(() => {
|
||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||
<PageTitle :icon="FolderIcon" title="Projects"></PageTitle>
|
||||
<TabBar
|
||||
v-model="activeTab"
|
||||
>
|
||||
<TabBarItem
|
||||
value="active"
|
||||
>Active</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
value="archived"
|
||||
>Archived</TabBarItem>
|
||||
<TabBar v-model="activeTab">
|
||||
<TabBarItem value="active">Active</TabBarItem>
|
||||
<TabBarItem value="archived">Archived</TabBarItem>
|
||||
</TabBar>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import ReportingOverview from "@/Components/Common/Reporting/ReportingOverview.vue";
|
||||
|
||||
|
||||
import ReportingOverview from '@/Components/Common/Reporting/ReportingOverview.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
title="Reporting"
|
||||
data-testid="reporting_view"
|
||||
class="overflow-hidden">
|
||||
<AppLayout title="Reporting" data-testid="reporting_view" class="overflow-hidden">
|
||||
<ReportingOverview></ReportingOverview>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +18,7 @@ import DateRangePicker from '@/packages/ui/src/Input/DateRangePicker.vue';
|
||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||
import ReportingRoundingControls from '@/Components/Common/Reporting/ReportingRoundingControls.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import {
|
||||
getDayJsInstance,
|
||||
getLocalizedDayJs,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||
import {
|
||||
@@ -67,7 +64,7 @@ import type { ExportFormat } from '@/types/reporting';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
import {canCreateProjects, canViewAllTimeEntries} from '@/utils/permissions';
|
||||
import { canCreateProjects, canViewAllTimeEntries } from '@/utils/permissions';
|
||||
import ReportingExportModal from '@/Components/Common/Reporting/ReportingExportModal.vue';
|
||||
|
||||
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
|
||||
@@ -111,20 +108,10 @@ function getFilterAttributes() {
|
||||
const params = {
|
||||
...defaultParams,
|
||||
member_id: !canViewAllTimeEntries() ? getCurrentMembershipId() : undefined,
|
||||
member_ids:
|
||||
selectedMembers.value.length > 0
|
||||
? selectedMembers.value
|
||||
: undefined,
|
||||
project_ids:
|
||||
selectedProjects.value.length > 0
|
||||
? selectedProjects.value
|
||||
: undefined,
|
||||
task_ids:
|
||||
selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||
client_ids:
|
||||
selectedClients.value.length > 0
|
||||
? selectedClients.value
|
||||
: undefined,
|
||||
member_ids: selectedMembers.value.length > 0 ? selectedMembers.value : undefined,
|
||||
project_ids: selectedProjects.value.length > 0 ? selectedProjects.value : undefined,
|
||||
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
||||
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
billable: billable.value !== null ? billable.value : undefined,
|
||||
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
||||
@@ -137,8 +124,7 @@ const currentTimeEntryStore = useCurrentTimeEntryStore();
|
||||
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
||||
const { setActiveState, startLiveTimer } = currentTimeEntryStore;
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
|
||||
useTimeEntriesStore();
|
||||
const { createTimeEntry, updateTimeEntry, updateTimeEntries } = useTimeEntriesStore();
|
||||
|
||||
const { tags } = storeToRefs(useTagsStore());
|
||||
|
||||
@@ -190,15 +176,11 @@ async function createTag(name: string) {
|
||||
return await useTagsStore().createTag(name);
|
||||
}
|
||||
|
||||
async function createProject(
|
||||
project: CreateProjectBody
|
||||
): Promise<Project | undefined> {
|
||||
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
||||
return await useProjectsStore().createProject(project);
|
||||
}
|
||||
|
||||
async function createClient(
|
||||
body: CreateClientBody
|
||||
): Promise<Client | undefined> {
|
||||
async function createClient(body: CreateClientBody): Promise<Client | undefined> {
|
||||
return await useClientsStore().createClient(body);
|
||||
}
|
||||
|
||||
@@ -261,10 +243,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
title="Reporting"
|
||||
data-testid="reporting_view"
|
||||
class="overflow-hidden">
|
||||
<AppLayout title="Reporting" data-testid="reporting_view" class="overflow-hidden">
|
||||
<ReportingExportModal
|
||||
v-model:show="showExportModal"
|
||||
:export-url="exportUrl"></ReportingExportModal>
|
||||
@@ -274,15 +253,12 @@ async function downloadExport(format: ExportFormat) {
|
||||
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
||||
<ReportingTabNavbar active="detailed"></ReportingTabNavbar>
|
||||
</div>
|
||||
<ReportingExportButton
|
||||
:download="downloadExport"></ReportingExportButton>
|
||||
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
|
||||
</MainContainer>
|
||||
|
||||
<div class="py-2.5 w-full border-b border-default-background-separator">
|
||||
<MainContainer
|
||||
class="sm:flex space-y-4 sm:space-y-0 justify-between">
|
||||
<div
|
||||
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
|
||||
<MainContainer class="sm:flex space-y-4 sm:space-y-0 justify-between">
|
||||
<div class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
|
||||
<div class="text-sm font-medium">Filters</div>
|
||||
<MemberMultiselectDropdown
|
||||
v-model="selectedMembers"
|
||||
@@ -364,11 +340,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:active="billable !== null"
|
||||
:title="
|
||||
billable === 'false'
|
||||
? 'Non Billable'
|
||||
: 'Billable'
|
||||
"
|
||||
:title="billable === 'false' ? 'Non Billable' : 'Billable'"
|
||||
:icon="BillableIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</SelectDropdown>
|
||||
@@ -440,14 +412,9 @@ async function downloadExport(format: ExportFormat) {
|
||||
</div>
|
||||
<div v-if="timeEntries.length === 0">
|
||||
<div class="text-center pt-12">
|
||||
<ClockIcon
|
||||
class="w-8 text-icon-default inline pb-2"></ClockIcon>
|
||||
<h3 class="text-text-primary font-semibold">
|
||||
No time entries found
|
||||
</h3>
|
||||
<p class="pb-5">
|
||||
Adjust the filters to see more time entries!
|
||||
</p>
|
||||
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>
|
||||
<h3 class="text-text-primary font-semibold">No time entries found</h3>
|
||||
<p class="pb-5">Adjust the filters to see more time entries!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,18 +426,13 @@ async function downloadExport(format: ExportFormat) {
|
||||
class="flex justify-center items-center py-8"
|
||||
:sibling-count="1"
|
||||
show-edges>
|
||||
<PaginationList
|
||||
v-slot="{ items }"
|
||||
class="flex items-center space-x-1 relative">
|
||||
<div
|
||||
class="pr-2 flex items-center space-x-1 border-r border-border-primary mr-1">
|
||||
<PaginationList v-slot="{ items }" class="flex items-center space-x-1 relative">
|
||||
<div class="pr-2 flex items-center space-x-1 border-r border-border-primary mr-1">
|
||||
<PaginationFirst class="navigation-item">
|
||||
<ChevronDoubleLeftIcon class="w-4">
|
||||
</ChevronDoubleLeftIcon>
|
||||
<ChevronDoubleLeftIcon class="w-4"> </ChevronDoubleLeftIcon>
|
||||
</PaginationFirst>
|
||||
<PaginationPrev class="mr-4 navigation-item">
|
||||
<ChevronLeftIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-primary">
|
||||
<ChevronLeftIcon class="w-4 text-text-tertiary hover:text-text-primary">
|
||||
</ChevronLeftIcon>
|
||||
</PaginationPrev>
|
||||
</div>
|
||||
@@ -490,8 +452,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
<div class="px-2">…</div>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
<div
|
||||
class="!ml-2 pl-2 flex items-center space-x-1 border-l border-border-primary">
|
||||
<div class="!ml-2 pl-2 flex items-center space-x-1 border-l border-border-primary">
|
||||
<PaginationNext class="navigation-item">
|
||||
<ChevronRightIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-primary"></ChevronRightIcon>
|
||||
|
||||
@@ -28,10 +28,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
||||
import ReportTable from '@/Components/Common/Report/ReportTable.vue';
|
||||
import {
|
||||
isAllowedToPerformPremiumAction,
|
||||
isBillingActivated,
|
||||
} from '@/utils/billing';
|
||||
import { isAllowedToPerformPremiumAction, isBillingActivated } from '@/utils/billing';
|
||||
import { canManageBilling, canUpdateOrganization } from '@/utils/permissions';
|
||||
import PrimaryButton from '../packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
@@ -70,10 +67,7 @@ watch(currentPage, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
title="Reporting"
|
||||
data-testid="reporting_view"
|
||||
class="overflow-hidden">
|
||||
<AppLayout title="Reporting" data-testid="reporting_view" class="overflow-hidden">
|
||||
<MainContainer
|
||||
class="py-3 sm:py-5 min-h-[79px] border-b border-default-background-separator flex justify-between items-center">
|
||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||
@@ -97,13 +91,9 @@ watch(currentPage, () => {
|
||||
<strong>please upgrade to a paid plan</strong>.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<Link v-if="isBillingActivated() && canManageBilling()" href="/billing">
|
||||
<PrimaryButton
|
||||
v-if="
|
||||
isBillingActivated() && canUpdateOrganization()
|
||||
"
|
||||
v-if="isBillingActivated() && canUpdateOrganization()"
|
||||
type="button"
|
||||
class="mt-6"
|
||||
:icon="CreditCardIcon">
|
||||
@@ -126,18 +116,13 @@ watch(currentPage, () => {
|
||||
class="flex justify-center items-center py-8"
|
||||
:sibling-count="1"
|
||||
show-edges>
|
||||
<PaginationList
|
||||
v-slot="{ items }"
|
||||
class="flex items-center space-x-1 relative">
|
||||
<div
|
||||
class="pr-2 flex items-center space-x-1 border-r border-border-primary mr-1">
|
||||
<PaginationList v-slot="{ items }" class="flex items-center space-x-1 relative">
|
||||
<div class="pr-2 flex items-center space-x-1 border-r border-border-primary mr-1">
|
||||
<PaginationFirst class="navigation-item">
|
||||
<ChevronDoubleLeftIcon class="w-4">
|
||||
</ChevronDoubleLeftIcon>
|
||||
<ChevronDoubleLeftIcon class="w-4"> </ChevronDoubleLeftIcon>
|
||||
</PaginationFirst>
|
||||
<PaginationPrev class="mr-4 navigation-item">
|
||||
<ChevronLeftIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-primary">
|
||||
<ChevronLeftIcon class="w-4 text-text-tertiary hover:text-text-primary">
|
||||
</ChevronLeftIcon>
|
||||
</PaginationPrev>
|
||||
</div>
|
||||
@@ -157,8 +142,7 @@ watch(currentPage, () => {
|
||||
<div class="px-2">…</div>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
<div
|
||||
class="!ml-2 pl-2 flex items-center space-x-1 border-l border-border-primary">
|
||||
<div class="!ml-2 pl-2 flex items-center space-x-1 border-l border-border-primary">
|
||||
<PaginationNext class="navigation-item">
|
||||
<ChevronRightIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-primary"></ChevronRightIcon>
|
||||
|
||||
@@ -57,8 +57,7 @@ const reportNumberFormat = computed(() => {
|
||||
});
|
||||
|
||||
const reportCurrencyFormat = computed(() => {
|
||||
return (sharedReportResponseData.value?.currency_format ??
|
||||
'symbol-before') as CurrencyFormat;
|
||||
return (sharedReportResponseData.value?.currency_format ?? 'symbol-before') as CurrencyFormat;
|
||||
});
|
||||
|
||||
const reportDateFormat = computed(() => {
|
||||
@@ -126,8 +125,7 @@ const groupedPieChartData = computed(() => {
|
||||
return {
|
||||
value: entry.seconds,
|
||||
name: emptyPlaceholder[
|
||||
aggregatedTableTimeEntries.value?.grouped_type ??
|
||||
'project'
|
||||
aggregatedTableTimeEntries.value?.grouped_type ?? 'project'
|
||||
],
|
||||
color: '#CCCCCC',
|
||||
};
|
||||
@@ -135,9 +133,7 @@ const groupedPieChartData = computed(() => {
|
||||
return {
|
||||
value: entry.seconds,
|
||||
name: entry.description,
|
||||
color:
|
||||
entry.color ??
|
||||
getRandomColorWithSeed(entry.description ?? 'none'),
|
||||
color: entry.color ?? getRandomColorWithSeed(entry.description ?? 'none'),
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
@@ -150,17 +146,14 @@ const tableData = computed(() => {
|
||||
cost: entry.cost,
|
||||
description:
|
||||
entry.description ??
|
||||
emptyPlaceholder[
|
||||
aggregatedTableTimeEntries.value?.grouped_type ?? 'project'
|
||||
],
|
||||
emptyPlaceholder[aggregatedTableTimeEntries.value?.grouped_type ?? 'project'],
|
||||
grouped_data:
|
||||
entry.grouped_data?.map((el) => {
|
||||
return {
|
||||
seconds: el.seconds,
|
||||
cost: el.cost,
|
||||
description:
|
||||
el.description ??
|
||||
emptyPlaceholder[entry.grouped_type ?? 'project'],
|
||||
el.description ?? emptyPlaceholder[entry.grouped_type ?? 'project'],
|
||||
};
|
||||
}) ?? [],
|
||||
};
|
||||
@@ -194,9 +187,7 @@ onMounted(async () => {
|
||||
<div class="pt-10 w-full px-3 relative">
|
||||
<ReportingChart
|
||||
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:grouped-data="
|
||||
aggregatedGraphTimeEntries?.grouped_data
|
||||
"></ReportingChart>
|
||||
:grouped-data="aggregatedGraphTimeEntries?.grouped_data"></ReportingChart>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<MainContainer>
|
||||
@@ -208,13 +199,9 @@ onMounted(async () => {
|
||||
Group by
|
||||
<strong class="px-2">{{ getGroupLabel(group) }}</strong>
|
||||
and
|
||||
<strong class="px-2">{{
|
||||
getGroupLabel(subGroup)
|
||||
}}</strong>
|
||||
<strong class="px-2">{{ getGroupLabel(subGroup) }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="grid items-center"
|
||||
style="grid-template-columns: 1fr 100px 150px">
|
||||
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
|
||||
<div
|
||||
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-tertiary [&>*]:pb-1.5 [&>*]:pt-1 text-text-secondary text-sm">
|
||||
<div class="pl-6">Name</div>
|
||||
@@ -224,8 +211,7 @@ onMounted(async () => {
|
||||
<template
|
||||
v-if="
|
||||
aggregatedTableTimeEntries?.grouped_data &&
|
||||
aggregatedTableTimeEntries.grouped_data
|
||||
?.length > 0
|
||||
aggregatedTableTimeEntries.grouped_data?.length > 0
|
||||
">
|
||||
<ReportingRow
|
||||
v-for="entry in tableData"
|
||||
@@ -238,8 +224,7 @@ onMounted(async () => {
|
||||
<div class="flex items-center pl-6 font-medium">
|
||||
<span>Total</span>
|
||||
</div>
|
||||
<div
|
||||
class="justify-end flex items-center font-medium">
|
||||
<div class="justify-end flex items-center font-medium">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
@@ -248,8 +233,7 @@ onMounted(async () => {
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
class="justify-end pr-6 flex items-center font-medium">
|
||||
<div class="justify-end pr-6 flex items-center font-medium">
|
||||
{{
|
||||
formatCents(
|
||||
aggregatedTableTimeEntries.cost,
|
||||
@@ -272,8 +256,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 lg:px-4">
|
||||
<ReportingPieChart
|
||||
:data="groupedPieChartData"></ReportingPieChart>
|
||||
<ReportingPieChart :data="groupedPieChartData"></ReportingPieChart>
|
||||
</div>
|
||||
</div>
|
||||
</MainContainer>
|
||||
|
||||
@@ -70,9 +70,7 @@ const page = usePage<{
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing">
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Create
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -28,22 +28,17 @@ const deleteTeam = () => {
|
||||
<ActionSection>
|
||||
<template #title> Delete Organization </template>
|
||||
|
||||
<template #description>
|
||||
Permanently delete this organization.
|
||||
</template>
|
||||
<template #description> Permanently delete this organization. </template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-text-secondary">
|
||||
Once a organization is deleted, all of its resources and data
|
||||
will be permanently deleted. Before deleting this organization,
|
||||
please download any data or information regarding this
|
||||
organization that you wish to retain.
|
||||
Once a organization is deleted, all of its resources and data will be permanently
|
||||
deleted. Before deleting this organization, please download any data or information
|
||||
regarding this organization that you wish to retain.
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<DangerButton @click="confirmTeamDeletion">
|
||||
Delete Organization
|
||||
</DangerButton>
|
||||
<DangerButton @click="confirmTeamDeletion"> Delete Organization </DangerButton>
|
||||
</div>
|
||||
|
||||
<!-- Delete Organization Confirmation Modal -->
|
||||
@@ -53,9 +48,8 @@ const deleteTeam = () => {
|
||||
<template #title> Delete Organization </template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you want to delete this organization? Once a
|
||||
organization is deleted, all of its resources and data will
|
||||
be permanently deleted.
|
||||
Are you sure you want to delete this organization? Once a organization is
|
||||
deleted, all of its resources and data will be permanently deleted.
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -6,10 +6,7 @@ import { api, type OrganizationExportResponse } from '@/packages/api/src';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import {
|
||||
ArrowUpOnSquareIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { ArrowUpOnSquareIcon, InformationCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import { CardTitle } from '@/packages/ui/src';
|
||||
import Card from '@/Components/Common/Card.vue';
|
||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||
@@ -28,14 +25,11 @@ async function exportData() {
|
||||
try {
|
||||
const response = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.exportOrganization(
|
||||
undefined,
|
||||
{
|
||||
params: {
|
||||
organization: organizationId,
|
||||
},
|
||||
}
|
||||
),
|
||||
api.exportOrganization(undefined, {
|
||||
params: {
|
||||
organization: organizationId,
|
||||
},
|
||||
}),
|
||||
'Organization data exported successfully.',
|
||||
'Exporting organization data failed.'
|
||||
);
|
||||
@@ -52,10 +46,7 @@ async function exportData() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal
|
||||
closeable
|
||||
:show="showResultModal"
|
||||
@close="showResultModal = false">
|
||||
<DialogModal closeable :show="showResultModal" @close="showResultModal = false">
|
||||
<template #title>The export was successful!</template>
|
||||
<template #content>
|
||||
<div class="pb-6">
|
||||
@@ -69,32 +60,26 @@ async function exportData() {
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showResultModal = false">
|
||||
Close
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="showResultModal = false"> Close </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<div>
|
||||
<CardTitle title="Export Data" :icon="ArrowUpOnSquareIcon"></CardTitle>
|
||||
<Card class="mb-3">
|
||||
<div class="py-2 px-3 sm:px-4 text-sm flex items-center space-x-3">
|
||||
<InformationCircleIcon
|
||||
class="h-5 min-w-0 w-5 text-bg-tertiary" />
|
||||
<InformationCircleIcon class="h-5 min-w-0 w-5 text-bg-tertiary" />
|
||||
<p class="flex-1">
|
||||
Export your solidtime organization data. This will include
|
||||
all clients, projects, tasks, and time entries. You will
|
||||
receive a zip file with json files for each entity.
|
||||
Export your solidtime organization data. This will include all clients,
|
||||
projects, tasks, and time entries. You will receive a zip file with json files
|
||||
for each entity.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div
|
||||
class="py-6 flex-col items-center flex space-y-5 text-center text-sm">
|
||||
<div class="py-6 flex-col items-center flex space-y-5 text-center text-sm">
|
||||
<div>
|
||||
The following organization will be exported: <br />
|
||||
<strong class="font-semibold">{{
|
||||
organization?.name
|
||||
}}</strong>
|
||||
<strong class="font-semibold">{{ organization?.name }}</strong>
|
||||
</div>
|
||||
<PrimaryButton :loading @click="exportData"
|
||||
>Export Organization Data
|
||||
|
||||
@@ -5,10 +5,7 @@ import { useNotificationsStore } from '@/utils/notification';
|
||||
import { api } from '@/packages/api/src';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import { DocumentIcon } from '@heroicons/vue/24/solid';
|
||||
import {
|
||||
ArrowDownOnSquareIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { ArrowDownOnSquareIcon, InformationCircleIcon } from '@heroicons/vue/24/outline';
|
||||
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import type { ImportReport, ImportType } from '@/packages/api/src';
|
||||
@@ -46,16 +43,11 @@ async function importData() {
|
||||
return;
|
||||
}
|
||||
if (files.value?.length !== 1) {
|
||||
addNotification(
|
||||
'error',
|
||||
'Please select the CSV or ZIP file that you want to import'
|
||||
);
|
||||
addNotification('error', 'Please select the CSV or ZIP file that you want to import');
|
||||
return;
|
||||
}
|
||||
const rawBase64String = await toBase64(files.value[0]);
|
||||
const base64String = rawBase64String
|
||||
.split(';')[1]
|
||||
.replace('base64,', '') as string;
|
||||
const base64String = rawBase64String.split(';')[1].replace('base64,', '') as string;
|
||||
const organizationId = getCurrentOrganizationId();
|
||||
if (organizationId !== null) {
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
@@ -129,65 +121,48 @@ const showResultModal = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal
|
||||
closeable
|
||||
:show="showResultModal"
|
||||
@close="showResultModal = false">
|
||||
<DialogModal closeable :show="showResultModal" @close="showResultModal = false">
|
||||
<template #title>Import Result</template>
|
||||
<template #content>
|
||||
<div class="pb-6">
|
||||
The import was successful! Here is an overview of the imported
|
||||
data:
|
||||
The import was successful! Here is an overview of the imported data:
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<div class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<span class="text-text-primary font-semibold">Clients created:</span>
|
||||
{{ reportResult?.report.clients.created }}
|
||||
</div>
|
||||
<div
|
||||
class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<div class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<span class="text-text-primary font-semibold">Projects created:</span>
|
||||
{{ reportResult?.report.projects.created }}
|
||||
</div>
|
||||
<div
|
||||
class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<div class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<span class="text-text-primary font-semibold">Tasks created:</span>
|
||||
{{ reportResult?.report.tasks.created }}
|
||||
</div>
|
||||
<div
|
||||
class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<span class="text-text-primary font-semibold"
|
||||
>Time entries created:</span
|
||||
>
|
||||
<div class="py-2.5 px-3 border-t border-t-card-background-separator">
|
||||
<span class="text-text-primary font-semibold">Time entries created:</span>
|
||||
{{ reportResult?.report.time_entries.created }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showResultModal = false">
|
||||
Close
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="showResultModal = false"> Close </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<div>
|
||||
<CardTitle
|
||||
title="Import Data"
|
||||
:icon="ArrowDownOnSquareIcon"></CardTitle>
|
||||
<CardTitle title="Import Data" :icon="ArrowDownOnSquareIcon"></CardTitle>
|
||||
<Card class="mb-3">
|
||||
<div class="py-2 px-3 sm:px-4 text-sm flex items-center space-x-3">
|
||||
<InformationCircleIcon
|
||||
class="h-5 min-w-0 w-5 text-bg-tertiary" />
|
||||
<InformationCircleIcon class="h-5 min-w-0 w-5 text-bg-tertiary" />
|
||||
<p class="flex-1">
|
||||
Import existing data from Toggl, Clockify or a different
|
||||
solidtime instance. Please select the type of data you want
|
||||
to import and follow the instructions.
|
||||
Import existing data from Toggl, Clockify or a different solidtime instance.
|
||||
Please select the type of data you want to import and follow the instructions.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div
|
||||
class="px-4 py-5 sm:px-5">
|
||||
<div class="px-4 py-5 sm:px-5">
|
||||
<div>
|
||||
<InputLabel for="importType" value="Import Type" />
|
||||
<select
|
||||
@@ -205,15 +180,9 @@ const showResultModal = ref(false);
|
||||
{{ importTypeOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
v-if="currentImporterDescription"
|
||||
class="py-3 text-text-primary">
|
||||
<div class="font-semibold text-text-secondary py-1">
|
||||
Instructions:
|
||||
</div>
|
||||
<div
|
||||
class="max-w-2xl"
|
||||
v-html="currentImporterDescription"></div>
|
||||
<div v-if="currentImporterDescription" class="py-3 text-text-primary">
|
||||
<div class="font-semibold text-text-secondary py-1">Instructions:</div>
|
||||
<div class="max-w-2xl" v-html="currentImporterDescription"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -229,9 +198,7 @@ const showResultModal = ref(false);
|
||||
for="file-upload"
|
||||
class="relative cursor-pointer rounded-md font-semibold text-text-primary focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 focus-within:ring-offset-gray-900 hover:text-indigo-500">
|
||||
<span v-if="files">{{ filenames }}</span>
|
||||
<span v-else
|
||||
>Upload a Toggl/Clockify Export</span
|
||||
>
|
||||
<span v-else>Upload a Toggl/Clockify Export</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
ref="importFile"
|
||||
@@ -250,9 +217,7 @@ const showResultModal = ref(false);
|
||||
|
||||
<div
|
||||
class="flex items-center justify-end px-4 py-3 bg-card-background border-t border-card-background-separator text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
|
||||
<PrimaryButton :loading @click="importData"
|
||||
>Import Data
|
||||
</PrimaryButton>
|
||||
<PrimaryButton :loading @click="importData">Import Data </PrimaryButton>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -40,10 +40,7 @@ async function submit() {
|
||||
}
|
||||
|
||||
function checkForConfirmationModal() {
|
||||
if (
|
||||
organizationBody.value.billable_rate ===
|
||||
organization.value?.billable_rate
|
||||
) {
|
||||
if (organizationBody.value.billable_rate === organization.value?.billable_rate) {
|
||||
submit();
|
||||
} else {
|
||||
showConfirmationModal.value = true;
|
||||
@@ -62,9 +59,7 @@ function checkForConfirmationModal() {
|
||||
<template #form>
|
||||
<OrganizationBillableRateModal
|
||||
v-model:show="showConfirmationModal"
|
||||
:new-billable-rate="
|
||||
organizationBody.billable_rate
|
||||
"
|
||||
:new-billable-rate="organizationBody.billable_rate"
|
||||
@submit="submit"></OrganizationBillableRateModal>
|
||||
<!-- Organization Owner Information -->
|
||||
<div class="col-span-6">
|
||||
@@ -98,9 +93,7 @@ function checkForConfirmationModal() {
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<PrimaryButton @click="checkForConfirmationModal"
|
||||
>Save</PrimaryButton
|
||||
>
|
||||
<PrimaryButton @click="checkForConfirmationModal">Save</PrimaryButton>
|
||||
</template>
|
||||
</FormSection>
|
||||
</template>
|
||||
|
||||
@@ -14,11 +14,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||
import type {
|
||||
DateFormat,
|
||||
TimeFormat,
|
||||
IntervalFormat,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import type { DateFormat, TimeFormat, IntervalFormat } from '@/packages/ui/src/utils/time';
|
||||
import type { CurrencyFormat } from '@/packages/ui/src/utils/money';
|
||||
import type { NumberFormat } from '@/packages/ui/src/utils/number';
|
||||
|
||||
@@ -44,8 +40,7 @@ const form = ref<FormValues>({
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: FormValues) =>
|
||||
updateOrganization(values as UpdateOrganizationBody),
|
||||
mutationFn: (values: FormValues) => updateOrganization(values as UpdateOrganizationBody),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['organization'] });
|
||||
},
|
||||
@@ -56,12 +51,10 @@ onMounted(async () => {
|
||||
if (organization.value) {
|
||||
form.value = {
|
||||
number_format: organization.value.number_format as NumberFormat,
|
||||
currency_format: organization.value
|
||||
.currency_format as CurrencyFormat,
|
||||
currency_format: organization.value.currency_format as CurrencyFormat,
|
||||
date_format: organization.value.date_format as DateFormat,
|
||||
time_format: organization.value.time_format as TimeFormat,
|
||||
interval_format: organization?.value
|
||||
.interval_format as IntervalFormat,
|
||||
interval_format: organization?.value.interval_format as IntervalFormat,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -83,30 +76,17 @@ async function submit() {
|
||||
<!-- Number Format -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="numberFormat"
|
||||
class="mb-2"
|
||||
value="Number Format" />
|
||||
<InputLabel for="numberFormat" class="mb-2" value="Number Format" />
|
||||
<Select v-model="form.number_format">
|
||||
<SelectTrigger id="numberFormat">
|
||||
<SelectValue placeholder="Select number format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="point-comma"
|
||||
>1.111,11</SelectItem
|
||||
>
|
||||
<SelectItem value="comma-point"
|
||||
>1,111.11</SelectItem
|
||||
>
|
||||
<SelectItem value="space-comma"
|
||||
>1 111,11</SelectItem
|
||||
>
|
||||
<SelectItem value="space-point"
|
||||
>1 111.11</SelectItem
|
||||
>
|
||||
<SelectItem value="apostrophe-point"
|
||||
>1'111.11</SelectItem
|
||||
>
|
||||
<SelectItem value="point-comma">1.111,11</SelectItem>
|
||||
<SelectItem value="comma-point">1,111.11</SelectItem>
|
||||
<SelectItem value="space-comma">1 111,11</SelectItem>
|
||||
<SelectItem value="space-point">1 111.11</SelectItem>
|
||||
<SelectItem value="apostrophe-point">1'111.11</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -115,29 +95,18 @@ async function submit() {
|
||||
<!-- Currency Format -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="currencyFormat"
|
||||
class="mb-2"
|
||||
value="Currency Format" />
|
||||
<InputLabel for="currencyFormat" class="mb-2" value="Currency Format" />
|
||||
<Select v-model="form.currency_format">
|
||||
<SelectTrigger id="currencyFormat">
|
||||
<SelectValue placeholder="Select currency format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="iso-code-before-with-space"
|
||||
>EUR 111</SelectItem
|
||||
>
|
||||
<SelectItem value="iso-code-after-with-space"
|
||||
>111 EUR</SelectItem
|
||||
>
|
||||
<SelectItem value="iso-code-before-with-space">EUR 111</SelectItem>
|
||||
<SelectItem value="iso-code-after-with-space">111 EUR</SelectItem>
|
||||
<SelectItem value="symbol-before">€111</SelectItem>
|
||||
<SelectItem value="symbol-after">111€</SelectItem>
|
||||
<SelectItem value="symbol-before-with-space"
|
||||
>€ 111</SelectItem
|
||||
>
|
||||
<SelectItem value="symbol-after-with-space"
|
||||
>111 €</SelectItem
|
||||
>
|
||||
<SelectItem value="symbol-before-with-space">€ 111</SelectItem>
|
||||
<SelectItem value="symbol-after-with-space">111 €</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -146,33 +115,18 @@ async function submit() {
|
||||
<!-- Date Format -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="dateFormat"
|
||||
class="mb-2"
|
||||
value="Date Format" />
|
||||
<InputLabel for="dateFormat" class="mb-2" value="Date Format" />
|
||||
<Select v-model="form.date_format">
|
||||
<SelectTrigger id="dateFormat">
|
||||
<SelectValue placeholder="Select date format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="point-separated-d-m-yyyy"
|
||||
>D.M.YYYY</SelectItem
|
||||
>
|
||||
<SelectItem value="slash-separated-mm-dd-yyyy"
|
||||
>MM/DD/YYYY</SelectItem
|
||||
>
|
||||
<SelectItem value="slash-separated-dd-mm-yyyy"
|
||||
>DD/MM/YYYY</SelectItem
|
||||
>
|
||||
<SelectItem value="hyphen-separated-dd-mm-yyyy"
|
||||
>DD-MM-YYYY</SelectItem
|
||||
>
|
||||
<SelectItem value="hyphen-separated-mm-dd-yyyy"
|
||||
>MM-DD-YYYY</SelectItem
|
||||
>
|
||||
<SelectItem value="hyphen-separated-yyyy-mm-dd"
|
||||
>YYYY-MM-DD</SelectItem
|
||||
>
|
||||
<SelectItem value="point-separated-d-m-yyyy">D.M.YYYY</SelectItem>
|
||||
<SelectItem value="slash-separated-mm-dd-yyyy">MM/DD/YYYY</SelectItem>
|
||||
<SelectItem value="slash-separated-dd-mm-yyyy">DD/MM/YYYY</SelectItem>
|
||||
<SelectItem value="hyphen-separated-dd-mm-yyyy">DD-MM-YYYY</SelectItem>
|
||||
<SelectItem value="hyphen-separated-mm-dd-yyyy">MM-DD-YYYY</SelectItem>
|
||||
<SelectItem value="hyphen-separated-yyyy-mm-dd">YYYY-MM-DD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -181,21 +135,14 @@ async function submit() {
|
||||
<!-- Time Format -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="timeFormat"
|
||||
class="mb-2"
|
||||
value="Time Format" />
|
||||
<InputLabel for="timeFormat" class="mb-2" value="Time Format" />
|
||||
<Select v-model="form.time_format">
|
||||
<SelectTrigger id="timeFormat">
|
||||
<SelectValue placeholder="Select time format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="12-hours"
|
||||
>12-hour clock</SelectItem
|
||||
>
|
||||
<SelectItem value="24-hours"
|
||||
>24-hour clock</SelectItem
|
||||
>
|
||||
<SelectItem value="12-hours">12-hour clock</SelectItem>
|
||||
<SelectItem value="24-hours">24-hour clock</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -204,24 +151,16 @@ async function submit() {
|
||||
<!-- Interval Format -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="intervalFormat"
|
||||
class="mb-2"
|
||||
value="Time Duration Format" />
|
||||
<InputLabel for="intervalFormat" class="mb-2" value="Time Duration Format" />
|
||||
<Select v-model="form.interval_format">
|
||||
<SelectTrigger id="intervalFormat">
|
||||
<SelectValue placeholder="Select interval format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="decimal">Decimal</SelectItem>
|
||||
<SelectItem value="hours-minutes"
|
||||
>12h 3m</SelectItem
|
||||
>
|
||||
<SelectItem value="hours-minutes-colon-separated"
|
||||
>12:03</SelectItem
|
||||
>
|
||||
<SelectItem
|
||||
value="hours-minutes-seconds-colon-separated"
|
||||
<SelectItem value="hours-minutes">12h 3m</SelectItem>
|
||||
<SelectItem value="hours-minutes-colon-separated">12:03</SelectItem>
|
||||
<SelectItem value="hours-minutes-seconds-colon-separated"
|
||||
>12:03:45</SelectItem
|
||||
>
|
||||
</SelectContent>
|
||||
|
||||
@@ -13,11 +13,7 @@ import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import SectionBorder from '@/Components/SectionBorder.vue';
|
||||
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
||||
import type {
|
||||
Organization,
|
||||
OrganizationInvitation,
|
||||
User,
|
||||
} from '@/types/models';
|
||||
import type { Organization, OrganizationInvitation, User } from '@/types/models';
|
||||
import type { Membership, Permissions, Role } from '@/types/jetstream';
|
||||
import { filterRoles } from '@/utils/roles';
|
||||
|
||||
@@ -94,9 +90,7 @@ const confirmLeavingTeam = () => {
|
||||
};
|
||||
|
||||
const leaveTeam = () => {
|
||||
leaveTeamForm.delete(
|
||||
route('team-members.destroy', [props.team.id, page.props.auth.user.id])
|
||||
);
|
||||
leaveTeamForm.delete(route('team-members.destroy', [props.team.id, page.props.auth.user.id]));
|
||||
};
|
||||
|
||||
const confirmTeamMemberRemoval = (teamMember: User) => {
|
||||
@@ -133,15 +127,14 @@ const displayableRole = (role: string) => {
|
||||
<template #title> Add Organization Member</template>
|
||||
|
||||
<template #description>
|
||||
Add a new member to your organization, allowing them to
|
||||
collaborate with you.
|
||||
Add a new member to your organization, allowing them to collaborate with you.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<div class="col-span-6">
|
||||
<div class="max-w-xl text-sm text-muted">
|
||||
Please provide the email address of the person you
|
||||
would like to add to this organization.
|
||||
Please provide the email address of the person you would like to add to
|
||||
this organization.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,19 +146,13 @@ const displayableRole = (role: string) => {
|
||||
v-model="addTeamMemberForm.email"
|
||||
type="email"
|
||||
class="mt-1 block w-full" />
|
||||
<InputError
|
||||
:message="addTeamMemberForm.errors.email"
|
||||
class="mt-2" />
|
||||
<InputError :message="addTeamMemberForm.errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Role -->
|
||||
<div
|
||||
v-if="availableRoles.length > 0"
|
||||
class="col-span-6 lg:col-span-4">
|
||||
<div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4">
|
||||
<InputLabel for="roles" value="Role" />
|
||||
<InputError
|
||||
:message="addTeamMemberForm.errors.role"
|
||||
class="mt-2" />
|
||||
<InputError :message="addTeamMemberForm.errors.role" class="mt-2" />
|
||||
|
||||
<div
|
||||
class="relative z-0 mt-1 border border-card-border rounded-lg cursor-pointer">
|
||||
@@ -177,9 +164,7 @@ const displayableRole = (role: string) => {
|
||||
:class="{
|
||||
'border-t border-card-border focus:border-none rounded-t-none':
|
||||
i > 0,
|
||||
'rounded-b-none':
|
||||
i !=
|
||||
Object.keys(availableRoles).length - 1,
|
||||
'rounded-b-none': i != Object.keys(availableRoles).length - 1,
|
||||
}"
|
||||
@click="addTeamMemberForm.role = role.key">
|
||||
<div
|
||||
@@ -193,18 +178,13 @@ const displayableRole = (role: string) => {
|
||||
<div
|
||||
class="text-sm text-text-primary"
|
||||
:class="{
|
||||
'font-semibold':
|
||||
addTeamMemberForm.role ==
|
||||
role.key,
|
||||
'font-semibold': addTeamMemberForm.role == role.key,
|
||||
}">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
|
||||
<svg
|
||||
v-if="
|
||||
addTeamMemberForm.role ==
|
||||
role.key
|
||||
"
|
||||
v-if="addTeamMemberForm.role == role.key"
|
||||
class="ms-2 h-5 w-5 text-green-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -219,8 +199,7 @@ const displayableRole = (role: string) => {
|
||||
</div>
|
||||
|
||||
<!-- Role Description -->
|
||||
<div
|
||||
class="mt-2 text-xs text-muted text-start">
|
||||
<div class="mt-2 text-xs text-muted text-start">
|
||||
{{ role.description }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,9 +209,7 @@ const displayableRole = (role: string) => {
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage
|
||||
:on="addTeamMemberForm.recentlySuccessful"
|
||||
class="me-3">
|
||||
<ActionMessage :on="addTeamMemberForm.recentlySuccessful" class="me-3">
|
||||
Added.
|
||||
</ActionMessage>
|
||||
|
||||
@@ -245,11 +222,7 @@ const displayableRole = (role: string) => {
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
team.team_invitations.length > 0 &&
|
||||
userPermissions.canAddTeamMembers
|
||||
">
|
||||
<div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers">
|
||||
<SectionBorder />
|
||||
|
||||
<!-- Organization Member Invitations -->
|
||||
@@ -257,9 +230,9 @@ const displayableRole = (role: string) => {
|
||||
<template #title> Pending Organization Invitations</template>
|
||||
|
||||
<template #description>
|
||||
These people have been invited to your organization and have
|
||||
been sent an invitation email. They may join the
|
||||
organization by accepting the email invitation.
|
||||
These people have been invited to your organization and have been sent an
|
||||
invitation email. They may join the organization by accepting the email
|
||||
invitation.
|
||||
</template>
|
||||
|
||||
<!-- Pending Organization Member Invitation List -->
|
||||
@@ -344,9 +317,7 @@ const displayableRole = (role: string) => {
|
||||
|
||||
<!-- Remove Organization Member -->
|
||||
<button
|
||||
v-else-if="
|
||||
userPermissions.canRemoveTeamMembers
|
||||
"
|
||||
v-else-if="userPermissions.canRemoveTeamMembers"
|
||||
class="cursor-pointer ms-6 text-sm text-red-500"
|
||||
@click="confirmTeamMemberRemoval(user)">
|
||||
Remove
|
||||
@@ -359,9 +330,7 @@ const displayableRole = (role: string) => {
|
||||
</div>
|
||||
|
||||
<!-- Role Management Modal -->
|
||||
<DialogModal
|
||||
:show="currentlyManagingRole"
|
||||
@close="currentlyManagingRole = false">
|
||||
<DialogModal :show="currentlyManagingRole" @close="currentlyManagingRole = false">
|
||||
<template #title> Manage Role</template>
|
||||
|
||||
<template #content>
|
||||
@@ -376,25 +345,20 @@ const displayableRole = (role: string) => {
|
||||
:class="{
|
||||
'border-t border-card-border focus:border-none rounded-t-none':
|
||||
i > 0,
|
||||
'rounded-b-none':
|
||||
i !==
|
||||
Object.keys(availableRoles).length - 1,
|
||||
'rounded-b-none': i !== Object.keys(availableRoles).length - 1,
|
||||
}"
|
||||
@click="updateRoleForm.role = role.key">
|
||||
<div
|
||||
:class="{
|
||||
'opacity-50':
|
||||
updateRoleForm.role &&
|
||||
updateRoleForm.role !== role.key,
|
||||
updateRoleForm.role && updateRoleForm.role !== role.key,
|
||||
}">
|
||||
<!-- Role Name -->
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="text-sm text-muted"
|
||||
:class="{
|
||||
'font-semibold':
|
||||
updateRoleForm.role ===
|
||||
role.key,
|
||||
'font-semibold': updateRoleForm.role === role.key,
|
||||
}">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
@@ -425,9 +389,7 @@ const displayableRole = (role: string) => {
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="currentlyManagingRole = false">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="currentlyManagingRole = false"> Cancel </SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
@@ -440,19 +402,13 @@ const displayableRole = (role: string) => {
|
||||
</DialogModal>
|
||||
|
||||
<!-- Leave Organization Confirmation Modal -->
|
||||
<ConfirmationModal
|
||||
:show="confirmingLeavingTeam"
|
||||
@close="confirmingLeavingTeam = false">
|
||||
<ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false">
|
||||
<template #title> Leave Organization</template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to leave this organization?
|
||||
</template>
|
||||
<template #content> Are you sure you would like to leave this organization? </template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="confirmingLeavingTeam = false">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="confirmingLeavingTeam = false"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
@@ -465,20 +421,15 @@ const displayableRole = (role: string) => {
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Remove Organization Member Confirmation Modal -->
|
||||
<ConfirmationModal
|
||||
:show="!!teamMemberBeingRemoved"
|
||||
@close="teamMemberBeingRemoved = null">
|
||||
<ConfirmationModal :show="!!teamMemberBeingRemoved" @close="teamMemberBeingRemoved = null">
|
||||
<template #title> Remove Organization Member</template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you would like to remove this person from the
|
||||
organization?
|
||||
Are you sure you would like to remove this person from the organization?
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<SecondaryButton @click="teamMemberBeingRemoved = null">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<SecondaryButton @click="teamMemberBeingRemoved = null"> Cancel </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
|
||||
@@ -34,9 +34,7 @@ const updateTeamName = () => {
|
||||
<FormSection @submitted="updateTeamName">
|
||||
<template #title> Organization Name</template>
|
||||
|
||||
<template #description>
|
||||
The organization's name and owner information.
|
||||
</template>
|
||||
<template #description> The organization's name and owner information. </template>
|
||||
|
||||
<template #form>
|
||||
<!-- Organization Owner Information -->
|
||||
@@ -61,9 +59,7 @@ const updateTeamName = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<Link v-if="isBillingActivated() && canManageBilling()" href="/billing">
|
||||
<PrimaryButton :icon="CreditCardIcon" type="button">
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
@@ -96,8 +92,7 @@ const updateTeamName = () => {
|
||||
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option value="" disabled>Select a currency</option>
|
||||
<option
|
||||
v-for="(currencyTranslated, currencyKey) in $page.props
|
||||
.currencies"
|
||||
v-for="(currencyTranslated, currencyKey) in $page.props.currencies"
|
||||
:key="currencyKey"
|
||||
:value="currencyKey">
|
||||
{{ currencyKey }} - {{ currencyTranslated }}
|
||||
@@ -108,13 +103,9 @@ const updateTeamName = () => {
|
||||
</template>
|
||||
|
||||
<template v-if="permissions.canUpdateTeam" #actions>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3">
|
||||
Saved.
|
||||
</ActionMessage>
|
||||
<ActionMessage :on="form.recentlySuccessful" class="me-3"> Saved. </ActionMessage>
|
||||
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing">
|
||||
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -29,18 +29,13 @@ defineProps<{
|
||||
<UpdateTeamNameForm :team="team" :permissions="permissions" />
|
||||
|
||||
<SectionBorder />
|
||||
<OrganizationBillableRate
|
||||
v-if="canUpdateOrganization()"
|
||||
:team="team" />
|
||||
<OrganizationBillableRate v-if="canUpdateOrganization()" :team="team" />
|
||||
<SectionBorder />
|
||||
|
||||
<OrganizationFormatSettings
|
||||
v-if="canUpdateOrganization()"
|
||||
:team="team" />
|
||||
<OrganizationFormatSettings v-if="canUpdateOrganization()" :team="team" />
|
||||
<SectionBorder />
|
||||
|
||||
<template
|
||||
v-if="permissions.canDeleteTeam && !team.personal_team">
|
||||
<template v-if="permissions.canDeleteTeam && !team.personal_team">
|
||||
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -33,13 +33,9 @@ import { canCreateProjects } from '@/utils/permissions';
|
||||
|
||||
const timeEntriesStore = useTimeEntriesStore();
|
||||
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
||||
const { updateTimeEntry, fetchTimeEntries, createTimeEntry } =
|
||||
useTimeEntriesStore();
|
||||
const { updateTimeEntry, fetchTimeEntries, createTimeEntry } = useTimeEntriesStore();
|
||||
|
||||
async function updateTimeEntries(
|
||||
ids: string[],
|
||||
changes: UpdateMultipleTimeEntriesChangeset
|
||||
) {
|
||||
async function updateTimeEntries(ids: string[], changes: UpdateMultipleTimeEntriesChangeset) {
|
||||
await useTimeEntriesStore().updateTimeEntries(ids, changes);
|
||||
fetchTimeEntries();
|
||||
}
|
||||
@@ -52,9 +48,7 @@ const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
|
||||
const { setActiveState } = currentTimeEntryStore;
|
||||
const { tags } = storeToRefs(useTagsStore());
|
||||
|
||||
async function startTimeEntry(
|
||||
timeEntry: Omit<CreateTimeEntryBody, 'member_id'>
|
||||
) {
|
||||
async function startTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
||||
if (currentTimeEntry.value.id) {
|
||||
await setActiveState(false);
|
||||
}
|
||||
@@ -69,11 +63,7 @@ function deleteTimeEntries(timeEntries: TimeEntry[]) {
|
||||
}
|
||||
|
||||
watch(isLoadMoreVisible, async (isVisible) => {
|
||||
if (
|
||||
isVisible &&
|
||||
timeEntries.value.length > 0 &&
|
||||
!allTimeEntriesLoaded.value
|
||||
) {
|
||||
if (isVisible && timeEntries.value.length > 0 && !allTimeEntriesLoaded.value) {
|
||||
loading.value = true;
|
||||
await timeEntriesStore.fetchMoreTimeEntries();
|
||||
}
|
||||
@@ -94,14 +84,10 @@ const { clients } = storeToRefs(clientStore);
|
||||
async function createTag(name: string) {
|
||||
return await useTagsStore().createTag(name);
|
||||
}
|
||||
async function createProject(
|
||||
project: CreateProjectBody
|
||||
): Promise<Project | undefined> {
|
||||
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
||||
return await useProjectsStore().createProject(project);
|
||||
}
|
||||
async function createClient(
|
||||
body: CreateClientBody
|
||||
): Promise<Client | undefined> {
|
||||
async function createClient(body: CreateClientBody): Promise<Client | undefined> {
|
||||
return await useClientsStore().createClient(body);
|
||||
}
|
||||
|
||||
@@ -131,8 +117,7 @@ function deleteSelected() {
|
||||
:tags
|
||||
:clients></TimeEntryCreateModal>
|
||||
<AppLayout title="Dashboard" data-testid="time_view">
|
||||
<MainContainer
|
||||
class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
||||
<MainContainer class="pt-5 lg:pt-8 pb-4 lg:pb-6">
|
||||
<div
|
||||
class="lg:flex items-end lg:divide-x divide-default-background-separator divide-y lg:divide-y-0 space-y-2 lg:space-y-0 lg:space-x-2">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -24,9 +24,7 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
|
||||
<div
|
||||
class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-text-primary">
|
||||
<div
|
||||
v-if="canLogin"
|
||||
class="sm:fixed sm:top-0 sm:end-0 p-6 text-end z-10">
|
||||
<div v-if="canLogin" class="sm:fixed sm:top-0 sm:end-0 p-6 text-end z-10">
|
||||
<Link
|
||||
v-if="user"
|
||||
:href="route('dashboard')"
|
||||
@@ -84,18 +82,16 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Documentation
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Laravel has wonderful documentation covering
|
||||
every aspect of the framework. Whether you are a
|
||||
newcomer or have prior experience with Laravel,
|
||||
we recommend reading our documentation from
|
||||
beginning to end.
|
||||
Laravel has wonderful documentation covering every aspect of the
|
||||
framework. Whether you are a newcomer or have prior experience with
|
||||
Laravel, we recommend reading our documentation from beginning to
|
||||
end.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -130,17 +126,15 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Laracasts
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Laracasts offers thousands of video tutorials on
|
||||
Laravel, PHP, and JavaScript development. Check
|
||||
them out, see for yourself, and massively level
|
||||
up your development skills in the process.
|
||||
Laracasts offers thousands of video tutorials on Laravel, PHP, and
|
||||
JavaScript development. Check them out, see for yourself, and
|
||||
massively level up your development skills in the process.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -176,16 +170,14 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Laravel News
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Laravel News is a community driven portal and
|
||||
newsletter aggregating all of the latest and
|
||||
most important news in the Laravel ecosystem,
|
||||
Laravel News is a community driven portal and newsletter aggregating
|
||||
all of the latest and most important news in the Laravel ecosystem,
|
||||
including new package releases and tutorials.
|
||||
</p>
|
||||
</div>
|
||||
@@ -221,15 +213,13 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Vibrant Ecosystem
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
|
||||
Laravel's robust library of first-party tools
|
||||
and libraries, such as
|
||||
Laravel's robust library of first-party tools and libraries, such as
|
||||
<a
|
||||
href="https://forge.laravel.com"
|
||||
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
||||
@@ -250,9 +240,8 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
||||
>Envoyer</a
|
||||
>
|
||||
help you take your projects to the next level.
|
||||
Pair them with powerful open source libraries
|
||||
like
|
||||
help you take your projects to the next level. Pair them with
|
||||
powerful open source libraries like
|
||||
<a
|
||||
href="https://laravel.com/docs/billing"
|
||||
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
||||
@@ -289,10 +278,8 @@ const user = computed(() => page?.props?.auth?.user);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex justify-center mt-16 px-6 sm:items-center sm:justify-between">
|
||||
<div
|
||||
class="text-center text-sm text-gray-500 dark:text-gray-400 sm:text-start">
|
||||
<div class="flex justify-center mt-16 px-6 sm:items-center sm:justify-between">
|
||||
<div class="text-center text-sm text-gray-500 dark:text-gray-400 sm:text-start">
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="https://github.com/sponsors/taylorotwell"
|
||||
|
||||
Reference in New Issue
Block a user