add format check, update prettier rules, apply rules consistently

This commit is contained in:
Gregor Vostrak
2025-07-29 18:58:31 +02:00
parent b11672732b
commit a3d7cdaf4d
323 changed files with 3728 additions and 5579 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 }"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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">&#8230;</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>

View File

@@ -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">&#8230;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"