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

@@ -14,8 +14,7 @@ import SectionTitle from './SectionTitle.vue';
</SectionTitle>
<div class="mt-5 md:mt-0 md:col-span-2">
<div
class="px-4 py-5 sm:p-6 bg-card-background shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6 bg-card-background shadow sm:rounded-lg">
<slot name="content" />
</div>
</div>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useTheme } from "@/utils/theme.js";
import { onMounted } from 'vue';
import { useTheme } from '@/utils/theme.js';
onMounted(async () => {
useTheme()
useTheme();
});
</script>

View File

@@ -24,9 +24,7 @@ watchEffect(async () => {
<template>
<div>
<div
v-if="show && message"
class="bg-secondary border-b border-border-secondary">
<div v-if="show && message" class="bg-secondary border-b border-border-secondary">
<div class="mx-auto py-1 px-3 sm:px-6 lg:px-8">
<div class="flex items-center justify-between flex-wrap">
<div class="w-0 flex-1 flex items-center min-w-0">

View File

@@ -1,10 +1,6 @@
<script setup lang="ts">
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import {
CheckBadgeIcon,
XMarkIcon,
XCircleIcon,
} from '@heroicons/vue/16/solid';
import { CheckBadgeIcon, XMarkIcon, XCircleIcon } from '@heroicons/vue/16/solid';
import { Link } from '@inertiajs/vue3';
import { computed } from 'vue';
import {
@@ -18,28 +14,20 @@ import { useSessionStorage } from '@vueuse/core';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { canManageBilling } from '@/utils/permissions';
const hideTrialBanner = useSessionStorage(
'showTrialBanner-' + getCurrentOrganizationId(),
false
);
const hideTrialBanner = useSessionStorage('showTrialBanner-' + getCurrentOrganizationId(), false);
const showTrialBanner = computed(() => isInTrial() && !hideTrialBanner.value);
const hideBlockedBanner = useSessionStorage(
'showBlockedBanner-' + getCurrentOrganizationId(),
false
);
const showBlockedBanner = computed(
() => isBlocked() && !hideBlockedBanner.value
);
const showBlockedBanner = computed(() => isBlocked() && !hideBlockedBanner.value);
const hideFreeUpgradeBanner = useSessionStorage(
'showFreeUpgradeBanner-' + getCurrentOrganizationId(),
false
);
const showFreeUpgradeBanner = computed(
() =>
isFreePlan() &&
!isBlocked() &&
!hideFreeUpgradeBanner.value &&
!showBlackFridayBanner.value
isFreePlan() && !isBlocked() && !hideFreeUpgradeBanner.value && !showBlackFridayBanner.value
);
const hideBlackFridayBanner = useSessionStorage(
'hideBlackFridayBanner-' + getCurrentOrganizationId(),
@@ -62,10 +50,7 @@ const showBlackFridayBanner = computed(() => {
class="bg-tertiary text-xs lg:text-sm pb-1 pt-2 border-b border-border-secondary">
<MainContainer class="flex items-center justify-between">
<div class="flex items-center space-x-1.5">
<svg
class="w-4 mr-1"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<svg class="w-4 mr-1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path
fill="#FF37AD"
d="M22.498 68.97a11.845 11.845 0 1 0 0-23.687c-6.471.098-11.666 5.372-11.666 11.844s5.195 11.746 11.666 11.844m181.393-10.04a11.845 11.845 0 1 0-.003-23.688c-6.471.098-11.665 5.373-11.665 11.845c.001 6.472 5.197 11.745 11.668 11.842" />
@@ -113,8 +98,7 @@ const showBlackFridayBanner = computed(() => {
</div>
</Link>
<button class="p-1" @click="hideBlackFridayBanner = true">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
<XMarkIcon class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
</div>
</MainContainer>
@@ -130,8 +114,8 @@ const showBlackFridayBanner = computed(() => {
Your trial expires in {{ daysLeftInTrial() }} days.
</span>
<span class="hidden md:inline">
To continue using all features & support the development
of solidtime, please upgrade your plan.
To continue using all features & support the development of solidtime,
please upgrade your plan.
</span>
</div>
</div>
@@ -143,8 +127,7 @@ const showBlackFridayBanner = computed(() => {
</div>
</Link>
<button class="p-1" @click="hideTrialBanner = true">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
<XMarkIcon class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
</div>
</MainContainer>
@@ -156,27 +139,22 @@ const showBlackFridayBanner = computed(() => {
<div class="flex items-center space-x-1.5">
<XCircleIcon class="w-4 text-text-primary/50"></XCircleIcon>
<div class="flex-1 space-x-1">
<span class="font-medium">
Your organization is currently blocked.
</span>
<span class="font-medium"> Your organization is currently blocked. </span>
<span class="hidden md:inline">
Please upgrade to a premium plan or remove all users
except the owner to unblock your organization.
Please upgrade to a premium plan or remove all users except the owner to
unblock your organization.
</span>
</div>
</div>
<div class="flex items-center space-x-2">
<Link
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<Link v-if="isBillingActivated() && canManageBilling()" href="/billing">
<div
class="text-text-primary font-semibold uppercase text-xs flex space-x-1 items-center hover:bg-white/10 rounded-lg px-2 py-1.5">
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideBlockedBanner = true">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
<XMarkIcon class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
</div>
</MainContainer>
@@ -188,27 +166,22 @@ const showBlackFridayBanner = computed(() => {
<div class="flex items-center space-x-1.5">
<XCircleIcon class="w-4 text-text-primary/50"></XCircleIcon>
<div class="flex-1 space-x-1">
<span class="font-medium">
You are currently using the Free Plan.
</span>
<span class="font-medium"> You are currently using the Free Plan. </span>
<span class="hidden md:inline">
To unlock all premium features & support the development
of solidtime, please upgrade your plan.</span
To unlock all premium features & support the development of solidtime,
please upgrade your plan.</span
>
</div>
</div>
<div class="flex items-center space-x-2">
<Link
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<Link v-if="isBillingActivated() && canManageBilling()" href="/billing">
<div
class="text-text-primary font-semibold uppercase text-xs flex space-x-1 items-center hover:bg-white/10 rounded-lg px-2 py-1.5">
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideFreeUpgradeBanner = true">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
<XMarkIcon class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
</div>
</MainContainer>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"></script>
<template>
<div class="rounded-lg border overflow-hidden border-card-border bg-card-background shadow-card">
<div
class="rounded-lg border overflow-hidden border-card-border bg-card-background shadow-card">
<slot></slot>
</div>
</template>

View File

@@ -1,9 +1,5 @@
<script setup lang="ts">
import {
ArchiveBoxIcon,
PencilSquareIcon,
TrashIcon,
} from '@heroicons/vue/20/solid';
import { ArchiveBoxIcon, PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
import type { Client } from '@/packages/api/src';
import { canDeleteClients, canUpdateClients } from '@/utils/permissions';
import {

View File

@@ -24,17 +24,10 @@ const createClient = ref(false);
class="grid min-w-full"
style="grid-template-columns: 1fr 150px 200px 80px">
<ClientTableHeading></ClientTableHeading>
<div
v-if="clients.length === 0"
class="col-span-3 py-24 text-center">
<UserCircleIcon
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
<h3 class="text-text-primary font-semibold">
No clients found
</h3>
<p v-if="canCreateClients()" class="pb-5">
Create your first client now!
</p>
<div v-if="clients.length === 0" class="col-span-3 py-24 text-center">
<UserCircleIcon class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
<h3 class="text-text-primary font-semibold">No clients found</h3>
<p v-if="canCreateClients()" class="pb-5">Create your first client now!</p>
<SecondaryButton
v-if="canCreateClients()"
:icon="PlusIcon as Component"

View File

@@ -20,9 +20,7 @@ function deleteClient() {
}
const projectCount = computed(() => {
return projects.value.filter(
(projects) => projects.client_id === props.client.id
).length;
return projects.value.filter((projects) => projects.client_id === props.client.id).length;
});
function archiveClient() {
@@ -37,9 +35,7 @@ const showEditModal = ref(false);
<template>
<TableRow>
<ClientEditModal
v-model:show="showEditModal"
:client="client"></ClientEditModal>
<ClientEditModal v-model:show="showEditModal" :client="client"></ClientEditModal>
<div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
<span>

View File

@@ -20,11 +20,8 @@ onMounted(async () => {
class="grid min-w-full"
style="grid-template-columns: 1fr 1fr 80px">
<InvitationTableHeading></InvitationTableHeading>
<template
v-for="invitation in invitations"
:key="invitation.id">
<InvitationTableRow
:invitation="invitation"></InvitationTableRow>
<template v-for="invitation in invitations" :key="invitation.id">
<InvitationTableRow :invitation="invitation"></InvitationTableRow>
</template>
</div>
</div>

View File

@@ -9,8 +9,7 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
Email
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Role</div>
<div
class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
<span class="sr-only">Edit</span>
</div>
</TableHeading>

View File

@@ -38,15 +38,12 @@ async function resendInvitation() {
if (organizationId) {
await handleApiRequestNotifications(
() =>
api.resendInvitationEmail(
undefined,
{
params: {
invitation: props.invitation.id,
organization: organizationId,
},
}
),
api.resendInvitationEmail(undefined, {
params: {
invitation: props.invitation.id,
organization: organizationId,
},
}),
'Invitation mail sent successfully',
'Error sending invitation mail'
);
@@ -65,9 +62,7 @@ async function resendInvitation() {
</div>
<div
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<InvitationMoreOptionsDropdown
@delete="deleteInvitation"
@resend="resendInvitation" />
<InvitationMoreOptionsDropdown @delete="deleteInvitation" @resend="resendInvitation" />
</div>
</TableRow>
</template>

View File

@@ -42,8 +42,8 @@ defineEmits<{
>.
</p>
<p class="py-1 text-center font-semibold max-w-md mx-auto">
Do you want to update all existing time entries, where the member
billable rate applies as well?
Do you want to update all existing time entries, where the member billable rate applies
as well?
</p>
</BillableRateModal>
</template>

View File

@@ -35,12 +35,8 @@ useFocus(searchInput, { initialValue: true });
const filteredMembers = computed<Member[]>(() => {
return members.value.filter((member) => {
return (
member.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '') &&
!props.hiddenMembers.some(
(hiddenMember) => hiddenMember.member_id === member.id
) &&
member.name.toLowerCase().includes(searchValue.value?.toLowerCase()?.trim() || '') &&
!props.hiddenMembers.some((hiddenMember) => hiddenMember.member_id === member.id) &&
member.is_placeholder === false
);
});

View File

@@ -9,7 +9,7 @@ import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import { useNotificationsStore } from '@/utils/notification';
import { getCurrentOrganizationId } from '@/utils/useUser';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { useMembersStore } from '@/utils/useMembers';
@@ -30,7 +30,7 @@ const deleteMutation = useMutation({
if (!organizationId) {
throw new Error('No organization ID found');
}
return api.removeMember(undefined, {
params: {
member: props.member.id,
@@ -44,7 +44,7 @@ const deleteMutation = useMutation({
onSuccess: () => {
close();
useMembersStore().fetchMembers();
}
},
});
const form = useForm({
@@ -70,77 +70,77 @@ const close = () => {
<template>
<Modal :show="show" max-width="md" @close="close">
<div class="p-6">
<h2 class="text-lg font-medium text-text-primary">
Delete Member
</h2>
<h2 class="text-lg font-medium text-text-primary">Delete Member</h2>
<div class="mt-4 text-sm text-text-secondary">
<p class="mb-4">
Are you sure you want to delete {{ member.name }}? This action cannot be undone.
</p>
<p class="mb-4">
This will permanently delete:
</p>
<p class="mb-4">This will permanently delete:</p>
<ul class="list-disc ml-6 mt-2">
<li>All time entries created by this member</li>
<li>Their project assignments</li>
<li>Their organization membership</li>
</ul>
<ul class="list-disc ml-6 mt-2">
<li>All time entries created by this member</li>
<li>Their project assignments</li>
<li>Their organization membership</li>
</ul>
<p class="pt-4">
<strong>Note:</strong> Deleting time entries will affect all reports and statistics.
If you want to keep the time entries but remove the member from your organization, you can convert them to a placeholder user instead. Placeholder users are not charged and their time entries remain intact for reporting purposes.
<strong>Note:</strong> Deleting time entries will affect all reports and
statistics. If you want to keep the time entries but remove the member from your
organization, you can convert them to a placeholder user instead. Placeholder
users are not charged and their time entries remain intact for reporting
purposes.
</p>
</div>
<form
class="mt-6" @submit="
(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}
">
class="mt-6"
@submit="
(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}
">
<div class="flex items-start">
<form.Field
name="confirmDelete"
:validators="{
onSubmit: ({value}) => {
if (!value) {
return 'You must confirm that you understand the consequences of this action';
}
return '';
<form.Field
name="confirmDelete"
:validators="{
onSubmit: ({ value }) => {
if (!value) {
return 'You must confirm that you understand the consequences of this action';
}
}"
>
return '';
},
}">
<template #default="{ field }">
<div class="flex flex-col">
<div class="flex items-center space-x-3 text-sm">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur"
/>
<InputLabel :for="field.name" class="font-medium text-text-primary">
I understand that this will permanently delete all data related to this member
</InputLabel>
</div>
<InputError class="pl-7 pt-2" :message="field.state.meta.errors[0]" />
<div class="flex items-center space-x-3 text-sm">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur" />
<InputLabel
:for="field.name"
class="font-medium text-text-primary">
I understand that this will permanently delete all data
related to this member
</InputLabel>
</div>
<InputError
class="pl-7 pt-2"
:message="field.state.meta.errors[0]" />
</div>
</template>
</form.Field>
</form.Field>
</div>
<div class="mt-6 flex justify-end space-x-3">
<SecondaryButton @click="close">Cancel</SecondaryButton>
<form.Subscribe>
<template #default="{ canSubmit, isSubmitting }">
<DangerButton
type="submit"
:disabled="!canSubmit"
>
{{ isSubmitting ? 'Deleting...' : 'Delete Member' }}
<DangerButton type="submit" :disabled="!canSubmit">
{{ isSubmitting ? 'Deleting...' : 'Delete Member' }}
</DangerButton>
</template>
</form.Subscribe>
@@ -148,4 +148,4 @@ class="mt-6" @submit="
</form>
</div>
</Modal>
</template>
</template>

View File

@@ -54,10 +54,7 @@ function saveWithChecks() {
showBillableRateModal.value = true;
}, 0);
show.value = false;
} else if (
memberBody.value.role === 'owner' &&
props.member.role !== 'owner'
) {
} else if (memberBody.value.role === 'owner' && props.member.role !== 'owner') {
show.value = false;
showOwnershipTransferConfirmModal.value = true;
} else {
@@ -96,10 +93,7 @@ const roleDescriptionTexts = {
};
const roleDescription = computed(() => {
if (
memberBody.value.role &&
memberBody.value.role in roleDescriptionTexts
) {
if (memberBody.value.role && memberBody.value.role in roleDescriptionTexts) {
return roleDescriptionTexts[memberBody.value.role];
}
return '';
@@ -143,22 +137,14 @@ const roleDescription = computed(() => {
<div>
<InputLabel for="billableType" value="Billable" />
<MemberBillableSelect
v-model="
billableRateSelect
"
v-model="billableRateSelect"
class="mt-2"
name="billableType"></MemberBillableSelect>
</div>
<div
v-if="billableRateSelect === 'custom-rate'"
class="flex-1">
<InputLabel
for="memberBillableRate"
value="Billable Rate" />
<div v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<InputLabel for="memberBillableRate" value="Billable Rate" />
<BillableRateInput
v-model="
memberBody.billable_rate
"
v-model="memberBody.billable_rate"
focus
class="w-full"
:currency="getOrganizationCurrencyString()"

View File

@@ -11,10 +11,7 @@ import type { Role } from '@/types/jetstream';
import { Link, useForm } from '@inertiajs/vue3';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { filterRoles } from '@/utils/roles';
import {
isAllowedToPerformPremiumAction,
isBillingActivated,
} from '@/utils/billing';
import { isAllowedToPerformPremiumAction, isBillingActivated } from '@/utils/billing';
import { CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
import { canManageBilling, canUpdateOrganization } from '@/utils/permissions';
import { api } from '@/packages/api/src';
@@ -44,14 +41,10 @@ const { handleApiRequestNotifications } = useNotificationsStore();
async function submit() {
if (addTeamMemberForm.role === null || addTeamMemberForm.email === '') {
errors.value.email = z
.string()
.email()
.safeParse(addTeamMemberForm.email).success
errors.value.email = z.string().email().safeParse(addTeamMemberForm.email).success
? ''
: 'Please enter a valid email address';
errors.value.role =
addTeamMemberForm.role === null ? 'Please select a role' : '';
errors.value.role = addTeamMemberForm.role === null ? 'Please select a role' : '';
return;
}
@@ -100,21 +93,15 @@ useFocus(clientNameInput, { initialValue: true });
<UserGroupIcon class="w-12"></UserGroupIcon>
</div>
<div class="max-w-sm text-center mx-auto py-4 text-base">
<p class="py-1">
The Free plan is <strong>limited to one member</strong>
</p>
<p class="py-1">The Free plan is <strong>limited to one member</strong></p>
<p class="py-1">
To add new team members to your organization you,
<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">
<CreditCardIcon class="w-5 h-5 me-2" />
@@ -154,8 +141,7 @@ useFocus(clientNameInput, { initialValue: true });
: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
@@ -169,17 +155,13 @@ useFocus(clientNameInput, { initialValue: true });
<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"

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import {ref} from 'vue';
import {api, type Member} from '@/packages/api/src';
import { ref } from 'vue';
import { api, type Member } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import {useMutation} from '@tanstack/vue-query';
import {getCurrentOrganizationId} from "@/utils/useUser";
import {useNotificationsStore} from "@/utils/notification";
import {useMembersStore} from "@/utils/useMembers";
import { useMutation } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import { useMembersStore } from '@/utils/useMembers';
const {handleApiRequestNotifications} = useNotificationsStore();
const { handleApiRequestNotifications } = useNotificationsStore();
const show = defineModel('show', {default: false});
const show = defineModel('show', { default: false });
const saving = ref(false);
const props = defineProps<{
@@ -27,7 +27,7 @@ const turnToPlaceholderMutation = useMutation({
return await api.makePlaceholder(undefined, {
params: {
organization: organizationId,
member: props.member.id
member: props.member.id,
},
});
},
@@ -36,17 +36,15 @@ const turnToPlaceholderMutation = useMutation({
async function submit() {
saving.value = true;
await handleApiRequestNotifications(
() =>
turnToPlaceholderMutation.mutateAsync(),
() => turnToPlaceholderMutation.mutateAsync(),
'Deactivating the member was successful!',
'There was an error deactivating the user.',
() => {
show.value = false;
useMembersStore().fetchMembers()
useMembersStore().fetchMembers();
}
);
}
</script>
<template>
@@ -59,8 +57,9 @@ async function submit() {
<template #content>
<p>
Deactivating the user <strong>{{ member.name }} </strong> will remove the user's access to
the organization. You will not be billed for inactive users and all time entries will be preserved.
Deactivating the user <strong>{{ member.name }} </strong> will remove the user's
access to the organization. You will not be billed for inactive users and all time
entries will be preserved.
</p>
</template>
<template #footer>

View File

@@ -2,14 +2,14 @@
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import {api, type Member} from '@/packages/api/src';
import { api, type Member } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import MemberCombobox from "@/Components/Common/Member/MemberCombobox.vue";
import {UserIcon, ArrowRightIcon} from "@heroicons/vue/24/solid";
import {Badge} from "@/packages/ui/src";
import MemberCombobox from '@/Components/Common/Member/MemberCombobox.vue';
import { UserIcon, ArrowRightIcon } from '@heroicons/vue/24/solid';
import { Badge } from '@/packages/ui/src';
import { useMutation } from '@tanstack/vue-query';
import {getCurrentOrganizationId} from "@/utils/useUser";
import {useNotificationsStore} from "@/utils/notification";
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
const { handleApiRequestNotifications, addNotification } = useNotificationsStore();
const show = defineModel('show', { default: false });
@@ -27,40 +27,36 @@ const mergeMember = useMutation({
if (organizationId === null) {
throw new Error('No current organization id - create report');
}
return await api.mergeMember({
member_id: newMemberId,
}, {
params: {
organization: organizationId,
member: props.member.id
return await api.mergeMember(
{
member_id: newMemberId,
},
});
{
params: {
organization: organizationId,
member: props.member.id,
},
}
);
},
});
async function submit() {
const newMemberId = newMember.value;
if(newMemberId !== ''){
if (newMemberId !== '') {
saving.value = true;
await handleApiRequestNotifications(
() =>
mergeMember.mutateAsync(newMemberId),
() => mergeMember.mutateAsync(newMemberId),
'Members successfully merged!',
'There was an error merging the members.',
() => {
show.value = false;
}
);
} else {
addNotification('error', 'Please select a member to merge into.');
}
else{
addNotification(
'error',
'Please select a member to merge into.',
);
}
}
</script>
<template>
@@ -72,10 +68,14 @@ async function submit() {
</template>
<template #content>
<p>Merging the user <strong>{{ member.name }} </strong> into another one will transfer all time entries to the new user. <strong>This cannot be reverted!</strong></p>
<p>
Merging the user <strong>{{ member.name }} </strong> into another one will transfer
all time entries to the new user. <strong>This cannot be reverted!</strong>
</p>
<div class="py-5 flex flex-col md:flex-row gap-6 items-center">
<div class="flex-1">
<Badge class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
<Badge
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
<UserIcon class="relative z-10 w-4 text-text-secondary"></UserIcon>
<div class="flex-1 font-medium truncate">
{{ member.name }}
@@ -86,9 +86,7 @@ async function submit() {
<ArrowRightIcon class="relative z-10 w-4 text-muted"></ArrowRightIcon>
</div>
<div class="flex-1">
<MemberCombobox
v-model="newMember"
></MemberCombobox>
<MemberCombobox v-model="newMember"></MemberCombobox>
</div>
</div>
</template>

View File

@@ -1,7 +1,17 @@
<script setup lang="ts">
import { TrashIcon, UserCircleIcon, PencilSquareIcon, ArrowDownOnSquareStackIcon } from '@heroicons/vue/20/solid';
import {
TrashIcon,
UserCircleIcon,
PencilSquareIcon,
ArrowDownOnSquareStackIcon,
} from '@heroicons/vue/20/solid';
import type { Member } from '@/packages/api/src';
import {canDeleteMembers, canMakeMembersPlaceholders, canMergeMembers, canUpdateMembers} from '@/utils/permissions';
import {
canDeleteMembers,
canMakeMembersPlaceholders,
canMergeMembers,
canUpdateMembers,
} from '@/utils/permissions';
import {
DropdownMenu,
DropdownMenuContent,

View File

@@ -26,8 +26,8 @@ const emit = defineEmits<{
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<p class="py-1 text-center">
You are about to transfer the ownership of this
organization to {{ memberName }}.
You are about to transfer the ownership of this organization to
{{ memberName }}.
</p>
</div>
</div>

View File

@@ -22,9 +22,7 @@ function getNameFromItem(item: Role) {
}
function getNameForKey(key: string | undefined) {
const item = page.props.availableRoles.find(
(item) => getKeyFromItem(item) === key
);
const item = page.props.availableRoles.find((item) => getKeyFromItem(item) === key);
if (item) {
return getNameFromItem(item);
}

View File

@@ -10,12 +10,9 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Email</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Role</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Billable Rate
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Billable Rate</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Status</div>
<div
class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12 bg-row-heading-background">
<span class="sr-only">Edit</span>
</div>
</TableHeading>

View File

@@ -86,13 +86,9 @@ const userHasValidMailAddress = computed(() => {
</div>
<div
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1 items-center font-medium">
<CheckCircleIcon
v-if="member.is_placeholder === false"
class="w-5"></CheckCircleIcon>
<CheckCircleIcon v-if="member.is_placeholder === false" class="w-5"></CheckCircleIcon>
<span v-if="member.is_placeholder === false">Active</span>
<UserCircleIcon
v-if="member.is_placeholder === true"
class="w-5"></UserCircleIcon>
<UserCircleIcon v-if="member.is_placeholder === true" class="w-5"></UserCircleIcon>
<span v-if="member.is_placeholder === true">Inactive</span>
</div>
<div
@@ -116,12 +112,8 @@ const userHasValidMailAddress = computed(() => {
showMakeMemberPlaceholderModal = true
"></MemberMoreOptionsDropdown>
</div>
<MemberEditModal
v-model:show="showEditMemberModal"
:member="member"></MemberEditModal>
<MemberMergeModal
v-model:show="showMergeMemberModal"
:member="member"></MemberMergeModal>
<MemberEditModal v-model:show="showEditMemberModal" :member="member"></MemberEditModal>
<MemberMergeModal v-model:show="showMergeMemberModal" :member="member"></MemberMergeModal>
<MemberMakePlaceholderModal
v-model:show="showMakeMemberPlaceholderModal"
:member="member"></MemberMakePlaceholderModal>

View File

@@ -41,8 +41,8 @@ defineEmits<{
>.
</p>
<p class="py-0.5 text-center font-semibold">
Do you want to update all existing time entries, where the
organization billable rate applies as well?
Do you want to update all existing time entries, where the organization billable rate
applies as well?
</p>
</BillableRateModal>
</template>

View File

@@ -1,41 +1,39 @@
<script setup lang="ts">
import ProjectBadge from "@/packages/ui/src/Project/ProjectBadge.vue";
import { computed, nextTick, ref, watch } from "vue";
import { useProjectsStore } from "@/utils/useProjects";
import Dropdown from "@/packages/ui/src/Input/Dropdown.vue";
import ProjectBadge from '@/packages/ui/src/Project/ProjectBadge.vue';
import { computed, nextTick, ref, watch } from 'vue';
import { useProjectsStore } from '@/utils/useProjects';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import {
ComboboxAnchor,
ComboboxContent,
ComboboxInput,
ComboboxItem,
ComboboxRoot,
ComboboxViewport
} from "radix-vue";
import { PlusCircleIcon } from "@heroicons/vue/20/solid";
import { storeToRefs } from "pinia";
import { api } from "@/packages/api/src";
import { usePage } from "@inertiajs/vue3";
import { getRandomColor } from "@/packages/ui/src/utils/color";
import type { Project } from "@/packages/api/src";
import ProjectDropdownItem from "@/packages/ui/src/Project/ProjectDropdownItem.vue";
import { UseFocusTrap } from "@vueuse/integrations/useFocusTrap/component";
ComboboxViewport,
} from 'radix-vue';
import { PlusCircleIcon } from '@heroicons/vue/20/solid';
import { storeToRefs } from 'pinia';
import { api } from '@/packages/api/src';
import { usePage } from '@inertiajs/vue3';
import { getRandomColor } from '@/packages/ui/src/utils/color';
import type { Project } from '@/packages/api/src';
import ProjectDropdownItem from '@/packages/ui/src/Project/ProjectDropdownItem.vue';
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component';
const searchValue = ref("");
const searchValue = ref('');
const searchInput = ref<HTMLElement | null>(null);
const model = defineModel<string | null>({
default: null
default: null,
});
const open = ref(false);
const projectsStore = useProjectsStore();
const emit = defineEmits(["update:modelValue", "changed"]);
const emit = defineEmits(['update:modelValue', 'changed']);
const { projects } = storeToRefs(projectsStore);
const projectDropdownTrigger = ref<HTMLElement | null>(null);
const shownProjects = computed(() => {
return projects.value.filter((project) => {
return project.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || "");
return project.name.toLowerCase().includes(searchValue.value?.toLowerCase()?.trim() || '');
});
});
@@ -44,7 +42,7 @@ withDefaults(
border?: boolean;
}>(),
{
border: true
border: true,
}
);
@@ -62,13 +60,13 @@ async function addProjectIfNoneExists() {
{
name: searchValue.value,
color: getRandomColor(),
is_billable: false
is_billable: false,
},
{ params: { organization: page.props.auth.user.current_team_id } }
);
projects.value.unshift(response.data);
model.value = response.data.id;
searchValue.value = "";
searchValue.value = '';
open.value = false;
}
}
@@ -95,16 +93,16 @@ function isProjectSelected(project: Project) {
}
const selectedProjectName = computed(() => {
return currentProject.value?.name || "No Project";
return currentProject.value?.name || 'No Project';
});
const selectedProjectColor = computed(() => {
return currentProject.value?.color || "var(--theme-color-icon-default)";
return currentProject.value?.color || 'var(--theme-color-icon-default)';
});
function updateValue(project: Project) {
model.value = project.id;
emit("changed");
emit('changed');
}
</script>
@@ -122,16 +120,13 @@ function updateValue(project: Project) {
</template>
<template #content>
<UseFocusTrap
v-if="open"
:options="{ immediate: true, allowOutsideClick: true }">
<UseFocusTrap v-if="open" :options="{ immediate: true, allowOutsideClick: true }">
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
:model-value="currentProject"
class="relative"
@update:model-value="updateValue"
>
@update:model-value="updateValue">
<ComboboxAnchor>
<ComboboxInput
ref="searchInput"
@@ -155,19 +150,12 @@ function updateValue(project: Project) {
:name="project.name"></ProjectDropdownItem>
</ComboboxItem>
<div
v-if="
searchValue.length > 0 &&
shownProjects.length === 0
"
v-if="searchValue.length > 0 && shownProjects.length === 0"
class="bg-card-background-active">
<div
class="flex space-x-3 items-center px-4 py-3 text-xs font-medium border-t rounded-b-lg border-card-background-separator">
<PlusCircleIcon
class="w-5 flex-shrink-0"></PlusCircleIcon>
<span
>Add "{{ searchValue }}" as a new
Project</span
>
<PlusCircleIcon class="w-5 flex-shrink-0"></PlusCircleIcon>
<span>Add "{{ searchValue }}" as a new Project</span>
</div>
</div>
</ComboboxViewport>

View File

@@ -3,11 +3,7 @@ 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 { computed, ref } from 'vue';
import type {
CreateClientBody,
CreateProjectBody,
Project,
} from '@/packages/api/src';
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { useFocus } from '@vueuse/core';
@@ -64,9 +60,7 @@ useFocus(projectNameInput, { initialValue: true });
const currentClientName = computed(() => {
if (project.value.client_id) {
return clients.value.find(
(client) => client.id === project.value.client_id
)?.name;
return clients.value.find((client) => client.id === project.value.client_id)?.name;
}
return 'No Client';
});
@@ -87,8 +81,7 @@ async function submitBillableRate() {
</template>
<template #content>
<div
class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-5">
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-5">
<div class="flex-1 flex items-center">
<div class="text-center">
<InputLabel for="color" value="Color" />
@@ -122,8 +115,7 @@ async function submitBillableRate() {
class="bg-input-background cursor-pointer hover:bg-tertiary"
size="xlarge">
<div class="flex items-center space-x-2">
<UserCircleIcon
class="w-5 text-icon-default"></UserCircleIcon>
<UserCircleIcon class="w-5 text-icon-default"></UserCircleIcon>
<span class="whitespace-nowrap">
{{ currentClientName }}
</span>
@@ -137,9 +129,7 @@ async function submitBillableRate() {
<div>
<ProjectEditBillableSection
v-model:is-billable="project.is_billable"
v-model:billable-rate="
project.billable_rate
"
v-model:billable-rate="project.billable_rate"
:currency="getOrganizationCurrencyString()"
@submit="submit"></ProjectEditBillableSection>
</div>

View File

@@ -1,9 +1,5 @@
<script setup lang="ts">
import {
TrashIcon,
PencilSquareIcon,
ArchiveBoxIcon,
} from '@heroicons/vue/20/solid';
import { TrashIcon, PencilSquareIcon, ArchiveBoxIcon } from '@heroicons/vue/20/solid';
import type { Project } from '@/packages/api/src';
import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions';
import {

View File

@@ -7,12 +7,7 @@ import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
import { canCreateProjects } from '@/utils/permissions';
import type {
CreateProjectBody,
Project,
Client,
CreateClientBody,
} from '@/packages/api/src';
import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src';
import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
@@ -24,15 +19,11 @@ const props = defineProps<{
}>();
const showCreateProjectModal = ref(false);
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 { clients } = storeToRefs(useClientsStore());
@@ -52,19 +43,11 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
:enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">
<div
data-testid="project_table"
class="grid min-w-full"
:style="gridTemplate">
<div data-testid="project_table" class="grid min-w-full" :style="gridTemplate">
<ProjectTableHeading
:show-billable-rate="
props.showBillableRate
"></ProjectTableHeading>
<div
v-if="projects.length === 0"
class="col-span-5 py-24 text-center">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
:show-billable-rate="props.showBillableRate"></ProjectTableHeading>
<div v-if="projects.length === 0" class="col-span-5 py-24 text-center">
<FolderPlusIcon class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-text-primary font-semibold">
{{
canCreateProjects()

View File

@@ -12,15 +12,9 @@ defineProps<{
Name
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Client</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Total Time
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Progress
</div>
<div
v-if="showBillableRate"
class="px-3 py-1.5 text-left font-semibold text-text-primary">
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Total Time</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Progress</div>
<div v-if="showBillableRate" class="px-3 py-1.5 text-left font-semibold text-text-primary">
Billable Rate
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Status</div>

View File

@@ -26,14 +26,11 @@ const props = defineProps<{
}>();
const client = computed(() => {
return clients.value.find(
(client) => client.id === props.project.client_id
);
return clients.value.find((client) => client.id === props.project.client_id);
});
const projectTasksCount = computed(() => {
return tasks.value.filter((task) => task.project_id === props.project.id)
.length;
return tasks.value.filter((task) => task.project_id === props.project.id).length;
});
function deleteProject() {
@@ -67,7 +64,6 @@ const billableRateInfo = computed(() => {
});
const showEditProjectModal = ref(false);
</script>
<template>
@@ -86,15 +82,10 @@ const showEditProjectModal = ref(false);
<span class="overflow-ellipsis overflow-hidden">
{{ project.name }}
</span>
<span class="text-text-secondary">
{{ projectTasksCount }} Tasks
</span>
<span class="text-text-secondary"> {{ projectTasksCount }} Tasks </span>
</div>
<div
class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-secondary">
<div
v-if="project.client_id"
class="overflow-ellipsis overflow-hidden">
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-secondary">
<div v-if="project.client_id" class="overflow-ellipsis overflow-hidden">
{{ client?.name }}
</div>
<div v-else>No client</div>
@@ -111,10 +102,8 @@ const showEditProjectModal = ref(false);
</div>
<div v-else>--</div>
</div>
<div
class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<UpgradeBadge
v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
<div class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<UpgradeBadge v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
<EstimatedTimeProgress
v-else-if="project.estimated_time"
:estimated="project.estimated_time"

View File

@@ -42,8 +42,8 @@ defineEmits<{
>.
</p>
<p class="py-1 text-center font-semibold max-w-md mx-auto">
Do you want to update all existing time entries, where the project
member billable rate applies as well?
Do you want to update all existing time entries, where the project member billable rate
applies as well?
</p>
</BillableRateModal>
</template>

View File

@@ -2,10 +2,7 @@
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import type {
CreateProjectMemberBody,
ProjectMember,
} from '@/packages/api/src';
import type { CreateProjectMemberBody, ProjectMember } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
@@ -57,9 +54,7 @@ useFocus(projectNameInput, { initialValue: true });
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<BillableRateInput
v-model="
projectMember.billable_rate
"
v-model="projectMember.billable_rate"
name="billable_rate"
:currency="getOrganizationCurrencyString()"></BillableRateInput>
</div>

View File

@@ -2,10 +2,7 @@
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref, watch } from 'vue';
import type {
ProjectMember,
UpdateProjectMemberBody,
} from '@/packages/api/src';
import type { ProjectMember, UpdateProjectMemberBody } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
@@ -29,10 +26,7 @@ const projectMemberBody = ref<UpdateProjectMemberBody>({
});
const showBillableRateModal = ref(false);
async function submit() {
if (
props.projectMember.billable_rate !==
projectMemberBody.value.billable_rate
) {
if (props.projectMember.billable_rate !== projectMemberBody.value.billable_rate) {
// make sure that the alert modal is not immediately submitted when user presses enter
setTimeout(() => {
showBillableRateModal.value = true;
@@ -84,20 +78,14 @@ useFocus(projectNameInput, { initialValue: true });
@close="showBillableRateModal = false"
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
<div class="grid grid-cols-3 items-center space-x-4">
<div
class="col-span-3 sm:col-span-2 space-x-2 flex items-center">
<div class="col-span-3 sm:col-span-2 space-x-2 flex items-center">
<UserIcon class="w-4 text-text-secondary"></UserIcon>
<span>{{ props.name }}</span>
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<InputLabel
for="billable_rate"
class="mb-2"
value="Billable Rate"></InputLabel>
<InputLabel for="billable_rate" class="mb-2" value="Billable Rate"></InputLabel>
<BillableRateInput
v-model="
projectMemberBody.billable_rate
"
v-model="projectMemberBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="billable_rate"
@keydown.enter="submit"></BillableRateInput>

View File

@@ -22,9 +22,7 @@ const props = defineProps<{
const { members } = storeToRefs(useMembersStore());
const currentMember = computed(() => {
return members.value.find(
(member) => member.id === props.projectMember.user_id
);
return members.value.find((member) => member.id === props.projectMember.user_id);
});
</script>

View File

@@ -28,24 +28,16 @@ const createProjectMember = ref(false);
class="grid min-w-full"
style="grid-template-columns: 1fr 150px 150px 80px">
<ProjectMemberTableHeading></ProjectMemberTableHeading>
<div
v-if="projectMembers.length === 0"
class="col-span-5 py-24 text-center">
<UserGroupIcon
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<div v-if="projectMembers.length === 0" class="col-span-5 py-24 text-center">
<UserGroupIcon class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-text-primary font-semibold">No project members</h3>
<p class="pb-5">Add the first project member!</p>
<SecondaryButton
:icon="PlusIcon"
@click="createProjectMember = true"
<SecondaryButton :icon="PlusIcon" @click="createProjectMember = true"
>Add a new Project Member
</SecondaryButton>
</div>
<template
v-for="projectMember in projectMembers"
:key="projectMember.id">
<ProjectMemberTableRow
:project-member="projectMember"></ProjectMemberTableRow>
<template v-for="projectMember in projectMembers" :key="projectMember.id">
<ProjectMemberTableRow :project-member="projectMember"></ProjectMemberTableRow>
</template>
</div>
</div>

View File

@@ -8,9 +8,7 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
class="py-1.5 pr-3 text-left font-semibold text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
Name
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Billable Rate
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Billable Rate</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Role</div>
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<span class="sr-only">Edit</span>

View File

@@ -31,9 +31,7 @@ function editProjectMember() {
const { members } = storeToRefs(useMembersStore());
const member = computed(() => {
return members.value.find(
(member) => member.id === props.projectMember.member_id
);
return members.value.find((member) => member.id === props.projectMember.member_id);
});
const showEditModal = ref(false);
</script>

View File

@@ -5,10 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.vue';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import type {
CreateReportBody,
CreateReportBodyProperties,
} from '@/packages/api/src';
import type { CreateReportBody, CreateReportBodyProperties } from '@/packages/api/src';
import { useMutation } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { api } from '@/packages/api/src';
@@ -80,10 +77,7 @@ async function submit() {
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="report.name"
class="mt-1.5 w-full"></TextInput>
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
@@ -95,19 +89,13 @@ async function submit() {
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-3 px-2 py-3">
<Checkbox
id="is_public"
v-model:checked="report.is_public"></Checkbox>
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
</div>
<div
v-if="report.is_public"
class="flex items-center space-x-4">
<div v-if="report.is_public" class="flex items-center space-x-4">
<div>
<InputLabel for="public_until" value="Expires at" />
<div class="text-text-tertiary font-medium">
(optional)
</div>
<div class="text-text-tertiary font-medium">(optional)</div>
</div>
<DatePicker id="public_until"></DatePicker>
</div>

View File

@@ -94,10 +94,7 @@ async function submit() {
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="report.name"
class="mt-1.5 w-full"></TextInput>
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
@@ -109,14 +106,10 @@ async function submit() {
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-2 px-2 py-3">
<Checkbox
id="is_public"
v-model:checked="report.is_public"></Checkbox>
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
</div>
<div
v-if="report.is_public"
class="flex items-center space-x-4">
<div v-if="report.is_public" class="flex items-center space-x-4">
<InputLabel for="public_until" value="Expires at" />
<DatePicker id="public_until"></DatePicker>
</div>

View File

@@ -31,13 +31,9 @@ function onSaveReportClick() {
v-model:show="showCreateReportModal"
:properties="reportProperties"></ReportCreateModal>
<UpgradeModal v-model:show="showPremiumModal">
<strong>Sharable Reports</strong> is only available in solidtime
Professional.
<strong>Sharable Reports</strong> is only available in solidtime Professional.
</UpgradeModal>
<SecondaryButton
v-if="canCreateReports()"
:icon="SaveIcon"
@click="onSaveReportClick"
<SecondaryButton v-if="canCreateReports()" :icon="SaveIcon" @click="onSaveReportClick"
>Save Report</SecondaryButton
>
</template>

View File

@@ -21,25 +21,15 @@ const gridTemplate = computed(() => {
<template>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">
<div
data-testid="report_table"
class="grid min-w-full"
:style="gridTemplate">
<div data-testid="report_table" class="grid min-w-full" :style="gridTemplate">
<ReportTableHeading></ReportTableHeading>
<div
v-if="reports.length === 0"
class="col-span-5 py-24 text-center">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-text-primary font-semibold">
No shared reports found
</h3>
<div v-if="reports.length === 0" class="col-span-5 py-24 text-center">
<FolderPlusIcon class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-text-primary font-semibold">No shared reports found</h3>
<p v-if="canCreateProjects()" class="pb-5">
Go to the overview to create a report
</p>
<SecondaryButton
:icon="PlusIcon"
@click="router.visit(route('reporting'))"
<SecondaryButton :icon="PlusIcon" @click="router.visit(route('reporting'))"
>Go to overview
</SecondaryButton>
</div>

View File

@@ -8,15 +8,9 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
class="py-1.5 pr-3 text-left font-semibold text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
Name
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Description
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Visibility
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Public URL
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Description</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Visibility</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Public URL</div>
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<span class="sr-only">Edit</span>
</div>

View File

@@ -57,9 +57,7 @@ async function deleteReport() {
</script>
<template>
<ReportEditModal
v-model:show="showEditReportModal"
:original-report="report"></ReportEditModal>
<ReportEditModal v-model:show="showEditReportModal" :original-report="report"></ReportEditModal>
<TableRow>
<div
class="whitespace-nowrap min-w-0 flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
@@ -75,14 +73,9 @@ async function deleteReport() {
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
{{ report.is_public ? 'Public' : 'Private' }}
</div>
<div
class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<div
v-if="report.shareable_link"
class="space-x-2 flex items-center">
<SecondaryButton
v-if="isSupported"
@click="copy(report.shareable_link)">
<div class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<div v-if="report.shareable_link" class="space-x-2 flex items-center">
<SecondaryButton v-if="isSupported" @click="copy(report.shareable_link)">
<span v-if="!copied">Copy URL</span>
<span v-else>Copied!</span>
</SecondaryButton>

View File

@@ -2,11 +2,7 @@
import VChart, { THEME_KEY } from 'vue-echarts';
import { computed, provide, inject, shallowRef, type ComputedRef } from 'vue';
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
import {
formatDate,
formatHumanReadableDuration,
formatWeek,
} from '@/packages/ui/src/utils/time';
import { formatDate, formatHumanReadableDuration, formatWeek } from '@/packages/ui/src/utils/time';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { BarChart } from 'echarts/charts';
@@ -19,14 +15,7 @@ import {
import type { AggregatedTimeEntries, Organization } from '@/packages/api/src';
import { useCssVariable } from '@/utils/useCssVariable';
use([
CanvasRenderer,
BarChart,
TitleComponent,
GridComponent,
TooltipComponent,
LegendComponent,
]);
use([CanvasRenderer, BarChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
provide(THEME_KEY, 'dark');
@@ -174,9 +163,7 @@ const option = computed(() => ({
class="chart"
:option="option" />
<div v-else class="chart flex flex-col items-center justify-center">
<p class="text-lg text-text-primary font-semibold">
No time entries found
</p>
<p class="text-lg text-text-primary font-semibold">No time entries found</p>
<p>Try to change the filters and time range</p>
</div>
</div>

View File

@@ -27,15 +27,11 @@ function triggerDownload(format: ExportFormat) {
<template>
<Dropdown align="end">
<template #trigger>
<SecondaryButton :icon="ArrowDownTrayIcon" :loading>
Export
</SecondaryButton>
<SecondaryButton :icon="ArrowDownTrayIcon" :loading> Export </SecondaryButton>
</template>
<template #content>
<div class="flex flex-col space-y-1 p-1.5">
<SecondaryButton
class="border-0 px-2"
@click="triggerDownload('pdf')">
<SecondaryButton class="border-0 px-2" @click="triggerDownload('pdf')">
<div class="flex items-center space-x-2">
<span> Export as PDF </span>
<LockClosedIcon
@@ -43,27 +39,20 @@ function triggerDownload(format: ExportFormat) {
class="w-3.5 text-text-tertiary"></LockClosedIcon>
</div>
</SecondaryButton>
<SecondaryButton
class="border-0 px-2"
@click="triggerDownload('xlsx')"
<SecondaryButton class="border-0 px-2" @click="triggerDownload('xlsx')"
>Export as Excel</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="triggerDownload('csv')"
<SecondaryButton class="border-0 px-2" @click="triggerDownload('csv')"
>Export as CSV</SecondaryButton
>
<SecondaryButton
class="border-0 px-2"
@click="triggerDownload('ods')"
<SecondaryButton class="border-0 px-2" @click="triggerDownload('ods')"
>Export as ODS
</SecondaryButton>
</div>
</template>
</Dropdown>
<UpgradeModal v-model:show="showPremiumModal">
<strong>PDF Reports</strong> are only available in solidtime
Professional.
<strong>PDF Reports</strong> are only available in solidtime Professional.
</UpgradeModal>
</template>

View File

@@ -1,9 +1,5 @@
<script setup lang="ts">
import {
ArrowDownTrayIcon,
CheckCircleIcon,
XMarkIcon,
} from '@heroicons/vue/20/solid';
import { ArrowDownTrayIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/vue/20/solid';
import { Modal, PrimaryButton } from '@/packages/ui/src';
const props = defineProps<{
exportUrl: string | null;
@@ -19,30 +15,19 @@ function downloadCurrentExport() {
</script>
<template>
<Modal
closeable
max-width="lg"
:show="showExportModal"
@close="showExportModal = false">
<Modal closeable max-width="lg" :show="showExportModal" @close="showExportModal = false">
<button
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
</button>
<div class="text-center text-text-primary py-6">
<div
class="flex items-center font-semibold text-lg justify-center space-x-2 pb-2">
<CheckCircleIcon
class="text-text-tertiary w-6"></CheckCircleIcon>
<div class="flex items-center font-semibold text-lg justify-center space-x-2 pb-2">
<CheckCircleIcon class="text-text-tertiary w-6"></CheckCircleIcon>
<span> Export Successful! </span>
</div>
<div class="text-center text-sm max-w-64 mx-auto">
<p class="pb-5">
Your export is ready, you can download it with the button
below.
</p>
<PrimaryButton
:icon="ArrowDownTrayIcon"
@click="downloadCurrentExport"
<p class="pb-5">Your export is ready, you can download it with the button below.</p>
<PrimaryButton :icon="ArrowDownTrayIcon" @click="downloadCurrentExport"
>Download</PrimaryButton
>
</div>

View File

@@ -26,18 +26,8 @@ const iconClass = computed(() => {
</script>
<template>
<Button
variant="outline"
size="sm"
:class="
twMerge(
activeClass
)
">
<component
:is="icon"
:class="iconClass"
></component>
<Button variant="outline" size="sm" :class="twMerge(activeClass)">
<component :is="icon" :class="iconClass"></component>
<span class="text-nowrap"> {{ title }} </span>
<div
v-if="count"

View File

@@ -8,12 +8,10 @@ const props = defineProps<{
groupByOptions: { value: string; label: string; icon: Component }[];
}>();
const icon = computed(() => {
return props.groupByOptions.find((option) => option.value === model.value)
?.icon;
return props.groupByOptions.find((option) => option.value === model.value)?.icon;
});
const title = computed(() => {
return props.groupByOptions.find((option) => option.value === model.value)
?.label;
return props.groupByOptions.find((option) => option.value === model.value)?.label;
});
</script>

View File

@@ -1,10 +1,5 @@
<script setup lang="ts">
import {
ChartBarIcon,
CheckCircleIcon,
TagIcon,
UserGroupIcon,
} from '@heroicons/vue/20/solid';
import { ChartBarIcon, CheckCircleIcon, TagIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
import { FolderIcon } from '@heroicons/vue/16/solid';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import { getOrganizationCurrencyString } from '@/utils/money';
@@ -43,11 +38,7 @@ import {
type CreateReportBodyProperties,
type Organization,
} from '@/packages/api/src';
import {
getCurrentMembershipId,
getCurrentOrganizationId,
getCurrentRole,
} from '@/utils/useUser';
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
import { useTagsStore } from '@/utils/useTags';
import { useSessionStorage, useStorage } from '@vueuse/core';
import { useNotificationsStore } from '@/utils/notification';
@@ -84,8 +75,7 @@ const subGroup = useStorage<GroupingOption>('reporting-sub-group', 'task');
const reportingStore = useReportingStore();
const { aggregatedGraphTimeEntries, aggregatedTableTimeEntries } =
storeToRefs(reportingStore);
const { aggregatedGraphTimeEntries, aggregatedTableTimeEntries } = storeToRefs(reportingStore);
const { groupByOptions } = reportingStore;
@@ -103,26 +93,13 @@ function getFilterAttributes(): AggregatedTimeEntriesQueryParams {
};
params = {
...params,
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,
member_id:
getCurrentRole() === 'employee'
? getCurrentMembershipId()
: undefined,
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
};
@@ -142,9 +119,7 @@ function updateGraphReporting() {
function updateTableReporting() {
const params = getFilterAttributes();
if (group.value === subGroup.value) {
const fallbackOption = groupByOptions.find(
(el) => el.value !== group.value
);
const fallbackOption = groupByOptions.find((el) => el.value !== group.value);
if (fallbackOption?.value) {
subGroup.value = fallbackOption.value;
}
@@ -162,14 +137,8 @@ function updateReporting() {
updateTableReporting();
}
function getOptimalGroupingOption(
startDate: string,
endDate: string
): 'day' | 'week' | 'month' {
const diffInDays = getDayJsInstance()(endDate).diff(
getDayJsInstance()(startDate),
'd'
);
function getOptimalGroupingOption(startDate: string, endDate: string): 'day' | 'week' | 'month' {
const diffInDays = getDayJsInstance()(endDate).diff(getDayJsInstance()(startDate), 'd');
if (diffInDays <= 31) {
return 'day';
@@ -213,10 +182,7 @@ async function downloadExport(format: ExportFormat) {
...getFilterAttributes(),
group: group.value,
sub_group: subGroup.value,
history_group: getOptimalGroupingOption(
startDate.value,
endDate.value
),
history_group: getOptimalGroupingOption(startDate.value, endDate.value),
format: format,
},
}),
@@ -249,17 +215,12 @@ const groupedPieChartData = computed(() => {
if (
name &&
aggregatedTableTimeEntries.value?.grouped_type &&
emptyPlaceholder[
aggregatedTableTimeEntries.value?.grouped_type
] === name
emptyPlaceholder[aggregatedTableTimeEntries.value?.grouped_type] === name
) {
color = '#CCCCCC';
} else if (
aggregatedTableTimeEntries.value?.grouped_type === 'project'
) {
} else if (aggregatedTableTimeEntries.value?.grouped_type === 'project') {
color =
projects.value?.find((project) => project.id === entry.key)
?.color ?? '#CCCCCC';
projects.value?.find((project) => project.id === entry.key)?.color ?? '#CCCCCC';
}
return {
value: entry.seconds,
@@ -288,10 +249,7 @@ const tableData = computed(() => {
return {
seconds: el.seconds,
cost: el.cost,
description: getNameForReportingRowEntry(
el.key,
entry.grouped_type
),
description: getNameForReportingRowEntry(el.key, entry.grouped_type),
};
}) ?? [],
};
@@ -310,20 +268,15 @@ const tableData = computed(() => {
<ReportingTabNavbar active="reporting"></ReportingTabNavbar>
</div>
<div class="flex space-x-2">
<ReportingExportButton
:download="downloadExport"></ReportingExportButton>
<ReportSaveButton
:report-properties="reportProperties"></ReportSaveButton>
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
<ReportSaveButton :report-properties="reportProperties"></ReportSaveButton>
</div>
</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">
<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"
@submit="updateReporting">
<MemberMultiselectDropdown v-model="selectedMembers" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedMembers.length"
@@ -332,9 +285,7 @@ const tableData = computed(() => {
:icon="UserGroupIcon"></ReportingFilterBadge>
</template>
</MemberMultiselectDropdown>
<ProjectMultiselectDropdown
v-model="selectedProjects"
@submit="updateReporting">
<ProjectMultiselectDropdown v-model="selectedProjects" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedProjects.length"
@@ -343,9 +294,7 @@ const tableData = computed(() => {
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ProjectMultiselectDropdown>
<TaskMultiselectDropdown
v-model="selectedTasks"
@submit="updateReporting">
<TaskMultiselectDropdown v-model="selectedTasks" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedTasks.length"
@@ -354,9 +303,7 @@ const tableData = computed(() => {
:icon="CheckCircleIcon"></ReportingFilterBadge>
</template>
</TaskMultiselectDropdown>
<ClientMultiselectDropdown
v-model="selectedClients"
@submit="updateReporting">
<ClientMultiselectDropdown v-model="selectedClients" @submit="updateReporting">
<template #trigger>
<ReportingFilterBadge
:count="selectedClients.length"
@@ -401,11 +348,7 @@ const tableData = computed(() => {
<template #trigger>
<ReportingFilterBadge
:active="billable !== null"
:title="
billable === 'false'
? 'Non Billable'
: 'Billable'
"
:title="billable === 'false' ? 'Non Billable' : 'Billable'"
:icon="BillableIcon"></ReportingFilterBadge>
</template>
</SelectDropdown>
@@ -427,37 +370,26 @@ const tableData = computed(() => {
<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>
<div class="sm:grid grid-cols-4 pt-6 items-start">
<div
class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
<div class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
<div
class="text-sm flex text-text-primary items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
<span>Group by</span>
<ReportingGroupBySelect
v-model="group"
:group-by-options="groupByOptions"
@changed="
updateTableReporting
"></ReportingGroupBySelect>
@changed="updateTableReporting"></ReportingGroupBySelect>
<span>and</span>
<ReportingGroupBySelect
v-model="subGroup"
:group-by-options="
groupByOptions.filter((el) => el.value !== group)
"
@changed="
updateTableReporting
"></ReportingGroupBySelect>
:group-by-options="groupByOptions.filter((el) => el.value !== group)"
@changed="updateTableReporting"></ReportingGroupBySelect>
</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>
@@ -475,13 +407,11 @@ const tableData = computed(() => {
:currency="getOrganizationCurrencyString()"
:type="aggregatedTableTimeEntries.grouped_type"
:entry="entry"></ReportingRow>
<div
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
<div class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
<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,
@@ -490,8 +420,7 @@ const tableData = computed(() => {
)
}}
</div>
<div
class="justify-end pr-6 flex items-center font-medium">
<div class="justify-end pr-6 flex items-center font-medium">
{{
aggregatedTableTimeEntries.cost
? formatCents(
@@ -509,16 +438,13 @@ const tableData = computed(() => {
<div
v-else
class="chart flex flex-col items-center justify-center py-12 col-span-3">
<p class="text-lg text-text-primary font-medium">
No time entries found
</p>
<p class="text-lg text-text-primary font-medium">No time entries found</p>
<p>Try to change the filters and time range</p>
</div>
</div>
</div>
<div class="px-2 lg:px-4">
<ReportingPieChart
:data="groupedPieChartData"></ReportingPieChart>
<ReportingPieChart :data="groupedPieChartData"></ReportingPieChart>
</div>
</div>
</MainContainer>

View File

@@ -14,14 +14,7 @@ import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
import { useCssVariable } from '@/utils/useCssVariable';
import type { Organization } from '@/packages/api/src';
use([
CanvasRenderer,
PieChart,
TitleComponent,
GridComponent,
TooltipComponent,
LegendComponent,
]);
use([CanvasRenderer, PieChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
provide(THEME_KEY, 'dark');

View File

@@ -2,20 +2,20 @@
import { Switch } from '@/Components/ui/switch';
import { Popover, PopoverContent, PopoverTrigger } from '@/Components/ui/popover';
import { Button } from '@/Components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import {
NumberField,
NumberFieldInput,
NumberFieldContent,
NumberFieldIncrement,
NumberFieldDecrement
import {
NumberField,
NumberFieldInput,
NumberFieldContent,
NumberFieldIncrement,
NumberFieldDecrement,
} from '@/Components/ui/number-field';
import { ArrowsUpDownIcon } from '@heroicons/vue/20/solid';
import { computed, ref, watch } from 'vue';
@@ -30,7 +30,7 @@ const TimeEntryRoundingType = {
Nearest: 'nearest' as const,
} as const;
type TimeEntryRoundingType = typeof TimeEntryRoundingType[keyof typeof TimeEntryRoundingType];
type TimeEntryRoundingType = (typeof TimeEntryRoundingType)[keyof typeof TimeEntryRoundingType];
interface Props {
enabled: boolean;
@@ -79,8 +79,8 @@ const selectedInterval = ref('');
// Compute the current interval value based on props
const currentInterval = computed(() => {
const predefined = predefinedIntervals.find(interval =>
interval.value !== 'custom' && parseInt(interval.value) === props.minutes
const predefined = predefinedIntervals.find(
(interval) => interval.value !== 'custom' && parseInt(interval.value) === props.minutes
);
return predefined ? predefined.value : 'custom';
});
@@ -116,10 +116,14 @@ function handleCustomMinutesChange(value: string | number) {
}
// Watch for changes in props.minutes
watch(() => props.minutes, (newMinutes) => {
customMinutes.value = newMinutes;
initializeSelectedInterval();
}, { immediate: true });
watch(
() => props.minutes,
(newMinutes) => {
customMinutes.value = newMinutes;
initializeSelectedInterval();
},
{ immediate: true }
);
watch(currentInterval, () => {
initializeSelectedInterval();
@@ -136,7 +140,9 @@ const activeClass = computed(() => {
const iconClass = computed(() => {
return twMerge(
'w-4 h-4',
props.enabled ? 'dark:text-accent-300/80 text-accent-400/80' : 'text-muted-foreground opacity-50'
props.enabled
? 'dark:text-accent-300/80 text-accent-400/80'
: 'text-muted-foreground opacity-50'
);
});
</script>
@@ -144,10 +150,7 @@ const iconClass = computed(() => {
<template>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
size="sm"
:class="twMerge(activeClass)">
<Button variant="outline" size="sm" :class="twMerge(activeClass)">
<ArrowsUpDownIcon :class="iconClass" />
Rounding {{ enabled ? 'on' : 'off' }}
</Button>
@@ -155,7 +158,9 @@ const iconClass = computed(() => {
<PopoverContent class="w-72 p-4">
<div v-if="!isAllowedToPerformPremiumAction()" class="flex flex-col space-y-2">
<span class="font-semibold text-xs">Premium</span>
<span class="text-xs text-text-secondary flex-1">Rounding is a premium feature. Upgrade to unlock this feature.</span>
<span class="text-xs text-text-secondary flex-1"
>Rounding is a premium feature. Upgrade to unlock this feature.</span
>
<Link href="/billing">
<Button size="sm" variant="input" class="items-center space-x-1">
<CreditCardIcon class="w-3.5 h-3.5 text-text-tertiary mr-1" />
@@ -166,26 +171,31 @@ const iconClass = computed(() => {
<div v-else class="space-y-4">
<div>
<div class="flex items-center justify-between">
<InputLabel for="enable-rounding" value="Enable Rounding" />
<Switch
id="enable-rounding"
:model-value="enabled"
class="data-[state=checked]:bg-accent-500"
@update:model-value="updateEnabled" />
</div>
<div class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
Rounding is applied to each individual time entry, not to the accumulated total.
</div>
<InputLabel for="enable-rounding" value="Enable Rounding" />
<Switch
id="enable-rounding"
:model-value="enabled"
class="data-[state=checked]:bg-accent-500"
@update:model-value="updateEnabled" />
</div>
<div
class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
Rounding is applied to each individual time entry, not to the accumulated
total.
</div>
</div>
<div>
<InputLabel for="rounding-type" value="Rounding Type" class="mb-2" />
<Select
<Select
:model-value="type"
:disabled="!enabled"
@update:model-value="(value) => updateType(value as TimeEntryRoundingType)">
<SelectTrigger id="rounding-type" size="small" class="w-full" :disabled="!enabled">
<SelectTrigger
id="rounding-type"
size="small"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select rounding type" />
</SelectTrigger>
<SelectContent>
@@ -197,23 +207,27 @@ const iconClass = computed(() => {
</div>
<div>
<InputLabel for="minutes-interval" value="Minutes Interval" class="mb-2" />
<Select
<Select
:model-value="selectedInterval"
:disabled="!enabled"
@update:model-value="(value) => handleIntervalChange(value as string)">
<SelectTrigger id="minutes-interval" size="small" class="w-full" :disabled="!enabled">
<SelectValue placeholder="Select interval" />
<SelectTrigger
id="minutes-interval"
size="small"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="interval in predefinedIntervals"
<SelectItem
v-for="interval in predefinedIntervals"
:key="interval.value"
:value="interval.value">
{{ interval.label }}
</SelectItem>
</SelectContent>
</Select>
<div v-if="showCustomInput" class="mt-2">
<NumberField
id="custom-minutes"
@@ -226,7 +240,9 @@ const iconClass = computed(() => {
@update:model-value="handleCustomMinutesChange">
<NumberFieldContent>
<NumberFieldDecrement :disabled="!enabled" />
<NumberFieldInput placeholder="Enter custom minutes" :disabled="!enabled" />
<NumberFieldInput
placeholder="Enter custom minutes"
:disabled="!enabled" />
<NumberFieldIncrement :disabled="!enabled" />
</NumberFieldContent>
</NumberField>
@@ -235,4 +251,4 @@ const iconClass = computed(() => {
</div>
</PopoverContent>
</Popover>
</template>
</template>

View File

@@ -32,10 +32,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
class="contents text-text-primary [&>*]:transition [&>*]:border-card-background-separator [&>*]:border-b [&>*]:h-[50px]">
<div
:class="
twMerge(
'pl-6 font-medium flex items-center space-x-3',
props.indent ? 'pl-16' : ''
)
twMerge('pl-6 font-medium flex items-center space-x-3', props.indent ? 'pl-16' : '')
">
<GroupedItemsCountButton
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
@@ -57,13 +54,17 @@ const organization = inject<ComputedRef<Organization>>('organization');
}}
</div>
<div class="justify-end pr-6 flex items-center">
{{ entry.cost ? formatCents(
entry.cost,
props.currency,
organization?.currency_format,
organization?.currency_symbol,
organization?.number_format
) : '--' }}
{{
entry.cost
? formatCents(
entry.cost,
props.currency,
organization?.currency_format,
organization?.currency_symbol,
organization?.number_format
)
: '--'
}}
</div>
</div>
<div

View File

@@ -2,8 +2,8 @@
import { router } from '@inertiajs/vue3';
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
import {canViewReport} from "@/utils/permissions";
import {computed} from "vue";
import { canViewReport } from '@/utils/permissions';
import { computed } from 'vue';
defineProps<{
active: 'reporting' | 'detailed' | 'shared';
}>();
@@ -12,17 +12,11 @@ const showSharedReports = computed(() => canViewReport());
</script>
<template>
<TabBar
:model-value="active"
>
<TabBarItem
value="reporting"
@click="router.visit(route('reporting'))"
<TabBar :model-value="active">
<TabBarItem value="reporting" @click="router.visit(route('reporting'))"
>Overview</TabBarItem
>
<TabBarItem
value="detailed"
@click="router.visit(route('reporting.detailed'))"
<TabBarItem value="detailed" @click="router.visit(route('reporting.detailed'))"
>Detailed</TabBarItem
>
<TabBarItem

View File

@@ -6,8 +6,7 @@ defineProps<{
</script>
<template>
<div
class="rounded-lg bg-card-background border-card-border shadow-card border px-3.5 py-2.5">
<div class="rounded-lg bg-card-background border-card-border shadow-card border px-3.5 py-2.5">
<dt class="font-semibold text-sm text-text-secondary">{{ title }}</dt>
<dd class="text-2xl text-text-primary pt-1 font-semibold">
{{ value ?? '--' }}

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { Tabs, TabsList } from '@/Components/ui/tabs'
import { Tabs, TabsList } from '@/Components/ui/tabs';
defineProps<{
defaultValue?: string
}>()
defaultValue?: string;
}>();
</script>
<template>

View File

@@ -1,20 +1,25 @@
<script setup lang="ts">
import { TabsTrigger } from '@/Components/ui/tabs'
import { twMerge } from 'tailwind-merge'
import type { Component } from 'vue'
import { TabsTrigger } from '@/Components/ui/tabs';
import { twMerge } from 'tailwind-merge';
import type { Component } from 'vue';
const props = defineProps<{
value: string
class?: string
icon?: Component
}>()
value: string;
class?: string;
icon?: Component;
}>();
</script>
<template>
<TabsTrigger
:value="value"
:icon="icon"
:class="twMerge('rounded-md px-2 sm:px-3 py-1 border sm:py-1.5 text-xs sm:text-sm font-medium text-text-tertiary hover:text-text-primary focus-visible:outline-none data-[state=active]:bg-tab-background data-[state=active]:border-input-border data-[state=active]:text-text-primary border-tab-border', props.class)">
:class="
twMerge(
'rounded-md px-2 sm:px-3 py-1 border sm:py-1.5 text-xs sm:text-sm font-medium text-text-tertiary hover:text-text-primary focus-visible:outline-none data-[state=active]:bg-tab-background data-[state=active]:border-input-border data-[state=active]:text-text-primary border-tab-border',
props.class
)
">
<slot></slot>
</TabsTrigger>
</template>

View File

@@ -18,9 +18,7 @@ const showCreateTagModal = ref(false);
</script>
<template>
<TagCreateModal
v-model:show="showCreateTagModal"
:create-tag></TagCreateModal>
<TagCreateModal v-model:show="showCreateTagModal" :create-tag></TagCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
@@ -28,15 +26,10 @@ const showCreateTagModal = ref(false);
class="grid min-w-full"
style="grid-template-columns: 1fr 80px">
<TagTableHeading></TagTableHeading>
<div
v-if="tags.length === 0"
class="col-span-5 py-24 text-center">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<div v-if="tags.length === 0" class="col-span-5 py-24 text-center">
<FolderPlusIcon class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-text-primary font-semibold">No tags found</h3>
<p v-if="canCreateTags()" class="pb-5">
Create your first tag now!
</p>
<p v-if="canCreateTags()" class="pb-5">Create your first tag now!</p>
<SecondaryButton
v-if="canCreateTags()"
:icon="PlusIcon"

View File

@@ -2,7 +2,7 @@
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 { ref, watch } from "vue";
import { ref, watch } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useTasksStore } from '@/utils/useTasks';
@@ -23,9 +23,12 @@ const props = defineProps<{
const taskProjectId = ref<string>(props.projectId);
watch(() => props.projectId, (value) => {
taskProjectId.value = value;
});
watch(
() => props.projectId,
(value) => {
taskProjectId.value = value;
}
);
async function submit() {
await createTask({

View File

@@ -1,9 +1,5 @@
<script setup lang="ts">
import {
TrashIcon,
PencilSquareIcon,
CheckCircleIcon,
} from '@heroicons/vue/20/solid';
import { TrashIcon, PencilSquareIcon, CheckCircleIcon } from '@heroicons/vue/20/solid';
import type { Task } from '@/packages/api/src';
import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions';
import {

View File

@@ -18,9 +18,7 @@ const createTask = ref(false);
</script>
<template>
<TaskCreateModal
v-model:show="createTask"
:project-id="props.projectId"></TaskCreateModal>
<TaskCreateModal v-model:show="createTask" :project-id="props.projectId"></TaskCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
@@ -29,22 +27,14 @@ const createTask = ref(false);
class="grid min-w-full"
style="
grid-template-columns:
1fr minmax(80px, auto) minmax(120px, auto) minmax(
50px,
auto
)
1fr minmax(80px, auto) minmax(120px, auto) minmax(50px, auto)
80px;
">
<TaskTableHeading></TaskTableHeading>
<div
v-if="tasks.length === 0"
class="col-span-5 py-24 text-center">
<PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<div v-if="tasks.length === 0" class="col-span-5 py-24 text-center">
<PlusCircleIcon class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-text-primary font-semibold">No tasks found</h3>
<p v-if="canCreateTasks()" class="pb-5">
Create your first task now!
</p>
<p v-if="canCreateTasks()" class="pb-5">Create your first task now!</p>
<SecondaryButton
v-if="canCreateTasks()"
:icon="PlusIcon"

View File

@@ -8,12 +8,8 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
class="py-1.5 pr-3 text-left font-semibold text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
Task Name
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Total Time
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">
Progress
</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Total Time</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Progress</div>
<div class="px-3 py-1.5 text-left font-semibold text-text-primary">Status</div>
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<span class="sr-only">Edit</span>

View File

@@ -54,10 +54,8 @@ const showTaskEditModal = ref(false);
</span>
<span v-else> -- </span>
</div>
<div
class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<UpgradeBadge
v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
<div class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
<UpgradeBadge v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
<EstimatedTimeProgress
v-else-if="task.estimated_time"
:estimated="task.estimated_time"
@@ -83,9 +81,7 @@ const showTaskEditModal = ref(false);
@edit="showTaskEditModal = true"
@delete="deleteTask"></TaskMoreOptionsDropdown>
</div>
<TaskEditModal
v-model:show="showTaskEditModal"
:task="task"></TaskEditModal>
<TaskEditModal v-model:show="showTaskEditModal" :task="task"></TaskEditModal>
</TableRow>
</template>

View File

@@ -7,8 +7,7 @@ const showUpgradeModal = ref(false);
<template>
<UpgradeModal v-model:show="showUpgradeModal">
<strong>Project and Task Estimates</strong> is only available in
solidtime Professional.
<strong>Project and Task Estimates</strong> is only available in solidtime Professional.
</UpgradeModal>
<button
class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1"

View File

@@ -46,13 +46,9 @@ const show = defineModel('show', { default: false });
to try out this feature.
</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">

View File

@@ -24,11 +24,7 @@ const close = () => {
</script>
<template>
<Modal
:show="show"
:max-width="maxWidth"
:closeable="closeable"
@close="close">
<Modal :show="show" :max-width="maxWidth" :closeable="closeable" @close="close">
<div class="bg-card-background px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
@@ -59,8 +55,7 @@ const close = () => {
</div>
</div>
<div
class="flex flex-row justify-end px-6 py-4 bg-card-background text-end">
<div class="flex flex-row justify-end px-6 py-4 bg-card-background text-end">
<slot name="footer" />
</div>
</Modal>

View File

@@ -43,9 +43,7 @@ const isRunningInDifferentOrganization = computed(() => {
</div>
</div>
<div>
<div class="text-text-secondary font-medium text-xs">
Current Timer
</div>
<div class="text-text-secondary font-medium text-xs">Current Timer</div>
<div class="text-text-primary font-medium text-lg">
{{ currentTime }}
</div>

View File

@@ -55,10 +55,7 @@ provide(THEME_KEY, 'dark');
const max = computed(() => {
if (!isLoading.value && dailyHoursTracked.value) {
return Math.max(
Math.max(...dailyHoursTracked.value.map((el) => el.duration)),
1
);
return Math.max(Math.max(...dailyHoursTracked.value.map((el) => el.duration)), 1);
} else {
return 1;
}
@@ -97,10 +94,7 @@ const option = computed(() => {
},
range: [
dayjs().format('YYYY-MM-DD'),
getDayJsInstance()()
.subtract(50, 'day')
.startOf('week')
.format('YYYY-MM-DD'),
getDayJsInstance()().subtract(50, 'day').startOf('week').format('YYYY-MM-DD'),
],
itemStyle: {
color: 'transparent',
@@ -112,9 +106,7 @@ const option = computed(() => {
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
data:
dailyHoursTracked?.value?.map((el) => [el.date, el.duration]) ??
[],
data: dailyHoursTracked?.value?.map((el) => [el.date, el.duration]) ?? [],
itemStyle: {
borderRadius: 5,
borderColor: borderColor.value,
@@ -159,9 +151,7 @@ const option = computed(() => {
:option="option"
style="height: 260px; background-color: transparent" />
</div>
<div v-else class="text-center text-gray-500 py-8">
No activity data available
</div>
<div v-else class="text-center text-gray-500 py-8">No activity data available</div>
</div>
</DashboardCard>
</template>

View File

@@ -10,19 +10,21 @@ const props = defineProps<{
const accentColor = useCssVariable('--theme-color-chart');
const markLineColor = useCssVariable('--color-border-secondary');
const seriesData = computed(() => props.history.map((el) => {
return {
value: el,
...{
itemStyle: {
borderWidth: 1,
borderColor: 'rgba(' + accentColor.value + ',0.8)',
borderRadius: [2, 2, 0, 0],
color: 'rgba(' + accentColor.value + ',0.8)',
const seriesData = computed(() =>
props.history.map((el) => {
return {
value: el,
...{
itemStyle: {
borderWidth: 1,
borderColor: 'rgba(' + accentColor.value + ',0.8)',
borderRadius: [2, 2, 0, 0],
color: 'rgba(' + accentColor.value + ',0.8)',
},
},
},
};
}));
};
})
);
const option = computed(() => ({
grid: {
top: 0,

View File

@@ -1,9 +1,6 @@
<script setup lang="ts">
import DayOverviewCardChart from '@/Components/Dashboard/DayOverviewCardChart.vue';
import {
formatHumanReadableDate,
formatHumanReadableDuration,
} from '@/packages/ui/src/utils/time';
import { formatHumanReadableDate, formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
import { inject, type ComputedRef } from 'vue';
import type { Organization } from '@/packages/api/src';
@@ -17,8 +14,7 @@ defineProps<{
</script>
<template>
<div
class="px-3.5 py-2 flex justify-between @container border-b border-b-background-separator">
<div class="px-3.5 py-2 flex justify-between @container border-b border-b-background-separator">
<div class="flex items-center min-w-[70px]">
<p class="font-medium text-sm text-text-primary">
{{ formatHumanReadableDate(date) }}

View File

@@ -1,35 +1,33 @@
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query";
import { computed } from "vue";
import DashboardCard from "@/Components/Dashboard/DashboardCard.vue";
import DayOverviewCardEntry from "@/Components/Dashboard/DayOverviewCardEntry.vue";
import { CalendarIcon } from "@heroicons/vue/20/solid";
import { getCurrentOrganizationId } from "@/utils/useUser";
import { api } from "@/packages/api/src";
import { LoadingSpinner } from "@/packages/ui/src";
import { useQuery } from '@tanstack/vue-query';
import { computed } from 'vue';
import DashboardCard from '@/Components/Dashboard/DashboardCard.vue';
import DayOverviewCardEntry from '@/Components/Dashboard/DayOverviewCardEntry.vue';
import { CalendarIcon } from '@heroicons/vue/20/solid';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { api } from '@/packages/api/src';
import { LoadingSpinner } from '@/packages/ui/src';
// Get the organization ID using the utility function
const organizationId = computed(() => getCurrentOrganizationId());
// Set up the query
const { data: last7Days, isLoading } = useQuery({
queryKey: ["lastSevenDays", organizationId],
queryKey: ['lastSevenDays', organizationId],
queryFn: () => {
return api.lastSevenDays({
params: {
organization: organizationId.value!
}
organization: organizationId.value!,
},
});
},
enabled: computed(() => !!organizationId.value),
placeholderData: Array.from({ length: 7 }, (_, i) => ({
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
duration: 0,
history: Array(8).fill(0)
}))
history: Array(8).fill(0),
})),
});
</script>
<template>
@@ -46,8 +44,6 @@ const { data: last7Days, isLoading } = useQuery({
:history="day.history"
:duration="day.duration"></DayOverviewCardEntry>
</div>
<div v-else class="text-center text-gray-500 py-8">
No data available
</div>
<div v-else class="text-center text-gray-500 py-8">No data available</div>
</DashboardCard>
</template>

View File

@@ -12,16 +12,9 @@ import {
} from 'echarts/components';
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
import { useCssVariable } from '@/utils/useCssVariable';
import type { Organization } from "@/packages/api/src";
import type { Organization } from '@/packages/api/src';
use([
CanvasRenderer,
PieChart,
TitleComponent,
GridComponent,
TooltipComponent,
LegendComponent,
]);
use([CanvasRenderer, PieChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
provide(THEME_KEY, 'dark');
const labelColor = useCssVariable('--color-text-secondary');
@@ -72,7 +65,11 @@ const option = computed(() => ({
},
tooltip: {
valueFormatter: (value: number) => {
return formatHumanReadableDuration(value, organization?.value?.interval_format, organization?.value?.number_format);
return formatHumanReadableDuration(
value,
organization?.value?.interval_format,
organization?.value?.number_format
);
},
},
data: seriesData,

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query";
import { computed } from "vue";
import RecentlyTrackedTasksCardEntry from "@/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue";
import DashboardCard from "@/Components/Dashboard/DashboardCard.vue";
import { CheckCircleIcon } from "@heroicons/vue/20/solid";
import SecondaryButton from "@/packages/ui/src/Buttons/SecondaryButton.vue";
import { PlusCircleIcon } from "@heroicons/vue/24/solid";
import { router } from "@inertiajs/vue3";
import { getCurrentMembershipId, getCurrentOrganizationId } from "@/utils/useUser";
import { api } from "@/packages/api/src";
import { LoadingSpinner } from "@/packages/ui/src";
import { useQuery } from '@tanstack/vue-query';
import { computed } from 'vue';
import RecentlyTrackedTasksCardEntry from '@/Components/Dashboard/RecentlyTrackedTasksCardEntry.vue';
import DashboardCard from '@/Components/Dashboard/DashboardCard.vue';
import { CheckCircleIcon } from '@heroicons/vue/20/solid';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import { PlusCircleIcon } from '@heroicons/vue/24/solid';
import { router } from '@inertiajs/vue3';
import { getCurrentMembershipId, getCurrentOrganizationId } from '@/utils/useUser';
import { api } from '@/packages/api/src';
import { LoadingSpinner } from '@/packages/ui/src';
// Get the organization ID using the utility function
const organizationId = computed(() => getCurrentOrganizationId());
@@ -17,19 +17,23 @@ const organizationId = computed(() => getCurrentOrganizationId());
// Function to fetch latest tasks using the API client
// Set up the query
const { data: timeEntriesResponse, isLoading, refetch } = useQuery({
queryKey: ["timeEntries", organizationId],
const {
data: timeEntriesResponse,
isLoading,
refetch,
} = useQuery({
queryKey: ['timeEntries', organizationId],
queryFn: () => {
return api.getTimeEntries({
params: {
organization: organizationId.value!
organization: organizationId.value!,
},
queries: {
member_id: getCurrentMembershipId()
}
member_id: getCurrentMembershipId(),
},
});
},
enabled: computed(() => !!organizationId.value)
enabled: computed(() => !!organizationId.value),
});
const latestTasks = computed(() => {
@@ -45,21 +49,26 @@ const filteredLatestTasks = computed(() => {
const finishedTimeEntries = latestTasks.value.filter((item) => item.end !== null);
// filter out duplicates based on description, task, project, tags and billable
return finishedTimeEntries.filter((item, index, self) => {
return index === self.findIndex((t) => (
t.description === item.description &&
t.task_id === item.task_id &&
t.project_id === item.project_id &&
t.tags.length === item.tags.length &&
t.tags.every((tag) => item.tags.includes(tag)) &&
t.billable === item.billable
));
}).slice(0, 4);
return finishedTimeEntries
.filter((item, index, self) => {
return (
index ===
self.findIndex(
(t) =>
t.description === item.description &&
t.task_id === item.task_id &&
t.project_id === item.project_id &&
t.tags.length === item.tags.length &&
t.tags.every((tag) => item.tags.includes(tag)) &&
t.billable === item.billable
)
);
})
.slice(0, 4);
});
// Listen for dashboard refresh events
window.addEventListener("dashboard:refresh", () => {
window.addEventListener('dashboard:refresh', () => {
refetch();
});
</script>
@@ -74,20 +83,17 @@ window.addEventListener("dashboard:refresh", () => {
v-for="lastTask in filteredLatestTasks"
:key="lastTask.id"
:time-entry="lastTask"
:class="filteredLatestTasks.length === 4 ? 'last:border-0' : ''"></RecentlyTrackedTasksCardEntry>
:class="
filteredLatestTasks.length === 4 ? 'last:border-0' : ''
"></RecentlyTrackedTasksCardEntry>
</div>
<div
v-else
class="text-center flex flex-1 justify-center items-center">
<div v-else class="text-center flex flex-1 justify-center items-center">
<div>
<PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-text-primary font-semibold text-sm">
No recent tasks found
</h3>
<PlusCircleIcon class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-text-primary font-semibold text-sm">No recent tasks found</h3>
<p class="pb-5 text-sm">Create tasks inside of a project!</p>
<SecondaryButton @click="router.visit(route('projects'))"
>Go to Projects
>Go to Projects
</SecondaryButton>
</div>
</div>
@@ -95,12 +101,11 @@ window.addEventListener("dashboard:refresh", () => {
v-if="latestTasks && latestTasks.length === 1"
class="text-center flex flex-1 justify-center items-center text-sm">
<div>
<PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<PlusCircleIcon class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-text-primary font-semibold">Add more tasks</h3>
<p class="pb-5">Create tasks inside of a project!</p>
<SecondaryButton @click="router.visit(route('projects'))"
>Go to Projects
>Go to Projects
</SecondaryButton>
</div>
</div>

View File

@@ -6,12 +6,12 @@ import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { getDayJsInstance } from '@/packages/ui/src/utils/time';
import type { TimeEntry } from "@/packages/api/src";
import { useTasksStore } from "@/utils/useTasks";
import { ChevronRightIcon } from "@heroicons/vue/16/solid";
import type { TimeEntry } from '@/packages/api/src';
import { useTasksStore } from '@/utils/useTasks';
import { ChevronRightIcon } from '@heroicons/vue/16/solid';
const props = defineProps<{
timeEntry: TimeEntry
timeEntry: TimeEntry;
}>();
const { projects } = storeToRefs(useProjectsStore());
@@ -20,7 +20,7 @@ const project = computed(() => {
return projects.value.find((project) => project.id === props.timeEntry.project_id);
});
const {tasks} = storeToRefs(useTasksStore());
const { tasks } = storeToRefs(useTasksStore());
const task = computed(() => {
return tasks.value.find((task) => task.id === props.timeEntry.task_id);
@@ -42,39 +42,31 @@ async function startTaskTimer() {
await setActiveState(true);
useCurrentTimeEntryStore().fetchCurrentTimeEntry();
}
</script>
<template>
<div
class="px-3.5 py-2 grid grid-cols-5 border-b border-b-background-separator">
<div class="px-3.5 py-2 grid grid-cols-5 border-b border-b-background-separator">
<div class="col-span-4">
<p class="font-medium text-text-primary text-sm pb-1 truncate">
<span v-if="timeEntry.description"> {{ timeEntry.description }}</span>
<span v-else class="text-text-tertiary">No description</span>
</p>
<ProjectBadge
size="base"
class="min-w-0 max-w-full"
:color="project?.color">
<ProjectBadge size="base" class="min-w-0 max-w-full" :color="project?.color">
<div class="flex items-center lg:space-x-0.5 min-w-0">
<span class="whitespace-nowrap ">
<span class="whitespace-nowrap">
{{ project?.name ?? 'No Project' }}
</span>
<ChevronRightIcon
v-if="task"
class="w-4 text-text-secondary shrink-0"></ChevronRightIcon>
<div
v-if="task"
class="min-w-0 shrink truncate">
<div v-if="task" class="min-w-0 shrink truncate">
{{ task.name }}
</div>
</div>
</ProjectBadge>
</div>
<div class="flex items-center justify-center">
<TimeTrackerStartStop
@changed="startTaskTimer"></TimeTrackerStartStop>
<TimeTrackerStartStop @changed="startTaskTimer"></TimeTrackerStartStop>
</div>
</div>
</template>

View File

@@ -7,7 +7,7 @@ import { UserGroupIcon } from '@heroicons/vue/20/solid';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { api } from '@/packages/api/src';
import { LoadingSpinner } from "@/packages/ui/src";
import { LoadingSpinner } from '@/packages/ui/src';
import { router } from '@inertiajs/vue3';
// Get the organization ID using the utility function
@@ -15,17 +15,16 @@ const organizationId = computed(() => getCurrentOrganizationId());
// Set up the query
const { data: latestTeamActivity, isLoading } = useQuery({
queryKey: ['latestTeamActivity', organizationId],
queryFn: () => {
return api.latestTeamActivity({
params: {
organization: organizationId.value!
}
})
},
enabled: computed(() => !!organizationId.value),
queryKey: ['latestTeamActivity', organizationId],
queryFn: () => {
return api.latestTeamActivity({
params: {
organization: organizationId.value!,
},
});
},
enabled: computed(() => !!organizationId.value),
});
</script>
<template>
@@ -42,18 +41,13 @@ const { data: latestTeamActivity, isLoading } = useQuery({
:description="activity.description"
:working="activity.status"></TeamActivityCardEntry>
</div>
<div v-else class="text-center text-gray-500 py-8">
No team activity found
</div>
<div v-else class="text-center text-gray-500 py-8">No team activity found</div>
<div
v-if="latestTeamActivity && latestTeamActivity.length <= 1"
class="text-center flex flex-1 justify-center items-center">
<div>
<UserGroupIcon
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-text-primary font-semibold text-sm">
Invite your co-workers
</h3>
<UserGroupIcon class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-text-primary font-semibold text-sm">Invite your co-workers</h3>
<p class="pb-5 text-sm">You can invite your entire team.</p>
<SecondaryButton @click="router.visit(route('members'))"
>Go to Members

View File

@@ -13,20 +13,13 @@ defineProps<{
<p class="font-semibold text-sm text-text-primary">
{{ name }}
</p>
<div
v-if="working"
class="flex space-x-1.5 items-center justify-end">
<span
class="relative flex h-3 w-3 justify-center items-center">
<div v-if="working" class="flex space-x-1.5 items-center justify-end">
<span class="relative flex h-3 w-3 justify-center items-center">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span
class="text-green-500 font-medium text-sm block pb-0.5">
working
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span class="text-green-500 font-medium text-sm block pb-0.5"> working </span>
</div>
</div>
<div

View File

@@ -24,14 +24,7 @@ import { useQuery } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { api, type Organization } from '@/packages/api/src';
use([
CanvasRenderer,
BarChart,
TitleComponent,
GridComponent,
TooltipComponent,
LegendComponent,
]);
use([CanvasRenderer, BarChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
provide(THEME_KEY, 'dark');
@@ -246,15 +239,8 @@ const option = computed(() => {
<div
class="grid space-y-5 sm:space-y-0 sm:gap-x-6 xl:gap-x-6 grid-cols-1 lg:grid-cols-3 xl:grid-cols-4">
<div class="col-span-2 xl:col-span-3">
<CardTitle
title="This Week"
class="pb-8"
:icon="ClockIcon"></CardTitle>
<v-chart
v-if="weeklyHistory"
:autoresize="true"
class="chart"
:option="option" />
<CardTitle title="This Week" class="pb-8" :icon="ClockIcon"></CardTitle>
<v-chart v-if="weeklyHistory" :autoresize="true" class="chart" :option="option" />
</div>
<div class="space-y-6">
<StatCard
@@ -294,9 +280,7 @@ const option = computed(() => {
" />
<ProjectsChartCard
v-if="weeklyProjectOverview"
:weekly-project-overview="
weeklyProjectOverview
"></ProjectsChartCard>
:weekly-project-overview="weeklyProjectOverview"></ProjectsChartCard>
</div>
</div>
</template>

View File

@@ -22,11 +22,7 @@ const hasActions = computed(() => !!useSlots().actions);
<form @submit.prevent="$emit('submitted')">
<div
class="px-4 py-5 bg-card-background sm:p-6 shadow"
:class="
hasActions
? 'sm:rounded-tl-md sm:rounded-tr-md'
: 'sm:rounded-md'
">
:class="hasActions ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md'">
<div class="grid grid-cols-6 gap-6">
<slot name="form" />
</div>

View File

@@ -1,11 +1,7 @@
<script setup lang="ts">
import { type Component } from 'vue';
import NavigationSidebarLink from '@/Components/NavigationSidebarLink.vue';
import {
CollapsibleContent,
CollapsibleRoot,
CollapsibleTrigger,
} from 'radix-vue';
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from 'radix-vue';
import { useSessionStorage } from '@vueuse/core';
import { ChevronRightIcon } from '@heroicons/vue/20/solid';
@@ -14,7 +10,7 @@ const props = defineProps<{
icon?: Component;
current?: boolean;
href: string;
subItems?: { title: string; route: string, show: boolean }[];
subItems?: { title: string; route: string; show: boolean }[];
}>();
const open = useSessionStorage('nav-collapse-state-' + props.title, true);
@@ -69,9 +65,7 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
v-if="subItem.show"
:title="subItem.title"
:current="route().current(subItem.route)"
:href="
route(subItem.route)
"></NavigationSidebarLink>
:href="route(subItem.route)"></NavigationSidebarLink>
</li>
</ul>
</div>

View File

@@ -22,9 +22,7 @@ defineProps<{
:is="icon"
v-if="icon"
:class="[
current
? 'text-icon-active'
: 'text-icon-default group-hover:text-icon-active',
current ? 'text-icon-active' : 'text-icon-default group-hover:text-icon-active',
'transition h-4 w-4 shrink-0',
]"
aria-hidden="true" />

View File

@@ -26,34 +26,23 @@
<div class="max-w-sm text-center mx-auto py-4 text-base">
<p class="py-1">
Your organization is currently
<strong class="font-semibold"
>blocked from performing this action</strong
>
<strong class="font-semibold">blocked from performing this action</strong>
</p>
<p class="py-1">
To unblock your organization, please
<strong class="font-semibold">
upgrade to a premium plan</strong
>
<strong class="font-semibold"> upgrade to a premium plan</strong>
or remove all users except the owner.
</p>
<Link
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<PrimaryButton
:icon="CreditCardIcon"
type="button"
class="mt-6">
<Link v-if="isBillingActivated() && canManageBilling()" href="/billing">
<PrimaryButton :icon="CreditCardIcon" type="button" class="mt-6">
Go to Billing
</PrimaryButton>
</Link>
</div>
</template>
<template #footer>
<SecondaryButton @click="showActionBlockedModal = false">
Cancel</SecondaryButton
>
<SecondaryButton @click="showActionBlockedModal = false"> Cancel</SecondaryButton>
</template>
</DialogModal>
</template>
@@ -70,7 +59,5 @@ import { Link } from '@inertiajs/vue3';
import PrimaryButton from '../packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '../packages/ui/src/Buttons/SecondaryButton.vue';
const { notifications, showActionBlockedModal } = storeToRefs(
useNotificationsStore()
);
const { notifications, showActionBlockedModal } = storeToRefs(useNotificationsStore());
</script>

View File

@@ -28,23 +28,15 @@ const switchToTeam = (organization: Organization) => {
</script>
<template>
<Dropdown
v-if="page.props.jetstream.hasTeamFeatures"
align="center"
width="60">
<Dropdown v-if="page.props.jetstream.hasTeamFeatures" align="center" width="60">
<template #trigger>
<div
data-testid="organization_switcher"
class="flex hover:bg-white/10 cursor-pointer transition px-2 py-1 rounded-lg w-full items-center justify-between font-medium">
<div
class="flex flex-1 space-x-2 items-center w-[calc(100%-30px)]">
<div class="flex flex-1 space-x-2 items-center w-[calc(100%-30px)]">
<div
class="rounded sm:rounded-lg bg-blue-900 font-semibold text-xs sm:text-sm flex-shrink-0 text-white w-5 sm:w-6 h-5 sm:h-6 flex items-center justify-center">
{{
page.props.auth.user.current_team.name
.slice(0, 1)
.toUpperCase()
}}
{{ page.props.auth.user.current_team.name.slice(0, 1).toUpperCase() }}
</div>
<span class="text-sm flex-1 truncate font-semibold">
{{ page.props.auth.user.current_team.name }}
@@ -53,8 +45,7 @@ const switchToTeam = (organization: Organization) => {
<div class="w-[30px]">
<button
class="p-1 transition hover:bg-white/10 rounded-full flex items-center w-8 h-8">
<ChevronDownIcon
class="w-5 sm:w-full mt-[1px]"></ChevronDownIcon>
<ChevronDownIcon class="w-5 sm:w-full mt-[1px]"></ChevronDownIcon>
</button>
</div>
</div>
@@ -63,24 +54,14 @@ const switchToTeam = (organization: Organization) => {
<template #content>
<div class="w-60">
<!-- Organization Management -->
<div class="block px-4 py-2 text-xs text-text-secondary">
Manage Organization
</div>
<div class="block px-4 py-2 text-xs text-text-secondary">Manage Organization</div>
<!-- Organization Settings -->
<DropdownLink
:href="
route(
'teams.show',
page.props.auth.user.current_team.id
)
">
<DropdownLink :href="route('teams.show', page.props.auth.user.current_team.id)">
Organization Settings
</DropdownLink>
<DropdownLink
v-if="canManageBilling() && isBillingActivated()"
href="/billing">
<DropdownLink v-if="canManageBilling() && isBillingActivated()" href="/billing">
Billing
</DropdownLink>
@@ -98,17 +79,12 @@ const switchToTeam = (organization: Organization) => {
Switch Organizations
</div>
<template
v-for="team in page.props.auth.user.all_teams"
:key="team.id">
<template v-for="team in page.props.auth.user.all_teams" :key="team.id">
<form @submit.prevent="switchToTeam(team)">
<DropdownLink as="button">
<div class="flex items-center">
<svg
v-if="
team.id ==
page.props.auth.user.current_team_id
"
v-if="team.id == page.props.auth.user.current_team_id"
class="me-2 h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"

View File

@@ -17,10 +17,7 @@ const classes = computed(() => {
<template>
<div>
<button
v-if="as == 'button'"
:class="classes"
class="w-full text-start">
<button v-if="as == 'button'" :class="classes" class="w-full text-start">
<slot />
</button>

View File

@@ -16,11 +16,7 @@ import { useProjectsStore } from '@/utils/useProjects';
import { useTasksStore } from '@/utils/useTasks';
import { useTagsStore } from '@/utils/useTags';
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
import type {
CreateClientBody,
CreateProjectBody,
Project,
} from '@/packages/api/src';
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
import { useClientsStore } from '@/utils/useClients';
import { getOrganizationCurrencyString } from '@/utils/money';
@@ -81,9 +77,7 @@ const isRunningInDifferentOrganization = computed(() => {
);
});
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
const newProject = await useProjectsStore().createProject(project);
if (newProject) {
currentTimeEntry.value.project_id = newProject.id;
@@ -110,10 +104,10 @@ const { tags } = storeToRefs(useTagsStore());
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
<div class="relative">
<TimeTrackerRunningInDifferentOrganizationOverlay
v-if="
isRunningInDifferentOrganization
"
@switch-organization="switchToTimeEntryOrganization"></TimeTrackerRunningInDifferentOrganizationOverlay>
v-if="isRunningInDifferentOrganization"
@switch-organization="
switchToTimeEntryOrganization
"></TimeTrackerRunningInDifferentOrganizationOverlay>
<TimeTrackerControls
v-model:current-time-entry="currentTimeEntry"

View File

@@ -5,16 +5,13 @@ import { useStorage } from '@vueuse/core';
const showReleaseInfo = useStorage('showReleaseInfo-desktop', true);
function openDesktopGithubRepo() {
window
.open('https://github.com/solidtime-io/solidtime-desktop', '_blank')
?.focus();
window.open('https://github.com/solidtime-io/solidtime-desktop', '_blank')?.focus();
}
</script>
<template>
<div v-if="showReleaseInfo" class="py-4 hidden lg:block">
<div
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
<div class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
<div class="flex items-start justify-between">
<div
class="text-xs pb-1.5 font-semibold text-text-tertiary flex items-center space-x-1">
@@ -29,9 +26,8 @@ function openDesktopGithubRepo() {
</div>
<p class="text-xs">
<span class="font-semibold">Solidtime Desktop Beta</span> is
here! Test our brand new clients for Windows, macOS and Linux
now.
<span class="font-semibold">Solidtime Desktop Beta</span> is here! Test our brand
new clients for Windows, macOS and Linux now.
</p>
<SecondaryButton
size="small"

View File

@@ -60,13 +60,9 @@ const logout = () => {
<template #content>
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
Manage Account
</div>
<div class="block px-4 py-2 text-xs text-gray-400">Manage Account</div>
<DropdownLink :href="route('profile.show')">
Profile
</DropdownLink>
<DropdownLink :href="route('profile.show')"> Profile </DropdownLink>
<DropdownLink
v-if="page.props.jetstream.hasApiFeatures"
@@ -78,9 +74,7 @@ const logout = () => {
<!-- Authentication -->
<form @submit.prevent="logout">
<DropdownLink as="button" data-testid="logout_button">
Log Out
</DropdownLink>
<DropdownLink as="button" data-testid="logout_button"> Log Out </DropdownLink>
</form>
</template>
</Dropdown>

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'reka-ui'
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const props = defineProps<AccordionRootProps>();
const emits = defineEmits<AccordionRootEmits>();
const forwarded = useForwardPropsEmits(props, emits)
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View File

@@ -1,24 +1,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AccordionContent, type AccordionContentProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils';
import { AccordionContent, type AccordionContentProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down px-0.5"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down px-0.5">
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View File

@@ -1,24 +1,21 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils';
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps)
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AccordionItem
v-bind="forwardedProps"
:class="cn('border-b', props.class)"
>
<slot />
</AccordionItem>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot />
</AccordionItem>
</template>

View File

@@ -1,39 +1,33 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { ChevronDown } from 'lucide-vue-next'
import {
AccordionHeader,
AccordionTrigger,
type AccordionTriggerProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils';
import { ChevronDown } from 'lucide-vue-next';
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class
)
">
<slot />
<slot name="icon">
<ChevronDown
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@@ -1,4 +1,4 @@
export { default as Accordion } from './Accordion.vue'
export { default as AccordionContent } from './AccordionContent.vue'
export { default as AccordionItem } from './AccordionItem.vue'
export { default as AccordionTrigger } from './AccordionTrigger.vue'
export { default as Accordion } from './Accordion.vue';
export { default as AccordionContent } from './AccordionContent.vue';
export { default as AccordionItem } from './AccordionItem.vue';
export { default as AccordionTrigger } from './AccordionTrigger.vue';

View File

@@ -1,14 +1,19 @@
<script setup lang="ts">
import { type AlertDialogEmits, type AlertDialogProps, AlertDialogRoot, useForwardPropsEmits } from 'reka-ui'
import {
type AlertDialogEmits,
type AlertDialogProps,
AlertDialogRoot,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>()
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();
const forwarded = useForwardPropsEmits(props, emits)
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import { buttonVariants } from '@/Components/ui/button'
import { AlertDialogAction, type AlertDialogActionProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { twMerge } from 'tailwind-merge'
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
import { buttonVariants } from '@/Components/ui/button';
import { AlertDialogAction, type AlertDialogActionProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import { twMerge } from 'tailwind-merge';
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="twMerge(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
<AlertDialogAction v-bind="delegatedProps" :class="twMerge(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View File

@@ -1,27 +1,22 @@
<script setup lang="ts">
import { buttonVariants } from '@/Components/ui/button'
import { AlertDialogCancel, type AlertDialogCancelProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { twMerge } from "tailwind-merge";
import { buttonVariants } from '@/Components/ui/button';
import { AlertDialogCancel, type AlertDialogCancelProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import { twMerge } from 'tailwind-merge';
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="twMerge(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
props.class,
)"
>
<slot />
</AlertDialogCancel>
<AlertDialogCancel
v-bind="delegatedProps"
:class="twMerge(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)">
<slot />
</AlertDialogCancel>
</template>

View File

@@ -1,42 +1,40 @@
<script setup lang="ts">
import {
AlertDialogContent,
type AlertDialogContentEmits,
type AlertDialogContentProps,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from "@/lib/utils";
AlertDialogContent,
type AlertDialogContentEmits,
type AlertDialogContentProps,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<AlertDialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class
)
">
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@@ -1,25 +1,21 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
AlertDialogDescription,
type AlertDialogDescriptionProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils';
import { AlertDialogDescription, type AlertDialogDescriptionProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</AlertDialogDescription>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</AlertDialogDescription>
</template>

View File

@@ -1,21 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
<slot />
</div>
</template>

View File

@@ -1,16 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
<div :class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@@ -1,22 +1,19 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AlertDialogTitle, type AlertDialogTitleProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils';
import { AlertDialogTitle, type AlertDialogTitleProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>
<AlertDialogTitle
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
<AlertDialogTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold', props.class)">
<slot />
</AlertDialogTitle>
</template>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { AlertDialogTrigger, type AlertDialogTriggerProps } from 'reka-ui'
import { AlertDialogTrigger, type AlertDialogTriggerProps } from 'reka-ui';
const props = defineProps<AlertDialogTriggerProps>()
const props = defineProps<AlertDialogTriggerProps>();
</script>
<template>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
</template>

View File

@@ -1,9 +1,9 @@
export { default as AlertDialog } from './AlertDialog.vue'
export { default as AlertDialogAction } from './AlertDialogAction.vue'
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
export { default as AlertDialogContent } from './AlertDialogContent.vue'
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'
export { default as AlertDialog } from './AlertDialog.vue';
export { default as AlertDialogAction } from './AlertDialogAction.vue';
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
export { default as AlertDialogContent } from './AlertDialogContent.vue';
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
export { default as AlertDialogFooter } from './AlertDialogFooter.vue';
export { default as AlertDialogHeader } from './AlertDialogHeader.vue';
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue';

Some files were not shown because too many files have changed in this diff Show More