add prevent_overlapping_time_entries setting to organization

when enabled users are blocked from creating or editing new time entries that are overlapping with other time entries
This commit is contained in:
Gregor Vostrak
2025-10-03 14:58:21 +02:00
parent b373427dc7
commit 231d22a5ce
14 changed files with 450 additions and 17 deletions

View File

@@ -27,7 +27,7 @@ interface FormValues {
}
const store = useOrganizationStore();
const { fetchOrganization, updateOrganization } = store;
const { updateOrganization } = store;
const { organization } = storeToRefs(store);
const queryClient = useQueryClient();
@@ -47,7 +47,6 @@ const mutation = useMutation({
});
onMounted(async () => {
await fetchOrganization();
if (organization.value) {
form.value = {
number_format: organization.value.number_format as NumberFormat,

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { onMounted, ref } from 'vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Checkbox } from '@/packages/ui/src';
import type { UpdateOrganizationBody } from '@/packages/api/src';
import { useOrganizationStore } from '@/utils/useOrganization';
import { storeToRefs } from 'pinia';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
const store = useOrganizationStore();
const { updateOrganization } = store;
const { organization } = storeToRefs(store);
const queryClient = useQueryClient();
const form = ref<{ prevent_overlapping_time_entries: boolean }>({
prevent_overlapping_time_entries: false,
});
onMounted(async () => {
form.value.prevent_overlapping_time_entries =
organization.value?.prevent_overlapping_time_entries ?? false;
});
const mutation = useMutation({
mutationFn: (values: Partial<UpdateOrganizationBody>) => updateOrganization(values),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['organization'] });
},
});
async function submit() {
await mutation.mutateAsync({
prevent_overlapping_time_entries: form.value.prevent_overlapping_time_entries,
});
}
</script>
<template>
<FormSection>
<template #title>Time Entry Settings</template>
<template #description>
Disallow overlapping time entries for members of this organization. When enabled, users
cannot create new time entries that overlap with their existing ones. This only affects
newly created entries.
</template>
<template #form>
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<div class="flex items-center space-x-2">
<Checkbox
id="preventOverlappingTimeEntries"
v-model:checked="form.prevent_overlapping_time_entries" />
<InputLabel
for="preventOverlappingTimeEntries"
value="Prevent overlapping time entries (new entries only)" />
</div>
</div>
</div>
</template>
<template #actions>
<PrimaryButton :disabled="mutation.isPending.value" @click="submit">Save</PrimaryButton>
</template>
</FormSection>
</template>

View File

@@ -8,12 +8,25 @@ import type { Permissions, Role } from '@/types/jetstream';
import { canUpdateOrganization } from '@/utils/permissions';
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
import OrganizationFormatSettings from '@/Pages/Teams/Partials/OrganizationFormatSettings.vue';
import OrganizationTimeEntrySettings from '@/Pages/Teams/Partials/OrganizationTimeEntrySettings.vue';
import { onMounted, ref } from 'vue';
import { useOrganizationStore } from '@/utils/useOrganization';
import { storeToRefs } from 'pinia';
defineProps<{
team: Organization;
availableRoles: Role[];
permissions: Permissions;
}>();
const loading = ref(true);
const orgStore = useOrganizationStore();
const { organization } = storeToRefs(orgStore);
onMounted(async () => {
await orgStore.fetchOrganization();
loading.value = false;
});
</script>
<template>
@@ -26,17 +39,25 @@ defineProps<{
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<UpdateTeamNameForm :team="team" :permissions="permissions" />
<div v-if="loading || !organization" class="py-16 text-center text-text-secondary">
Loading organization settings...
</div>
<template v-else>
<UpdateTeamNameForm :team="team" :permissions="permissions" />
<SectionBorder />
<OrganizationBillableRate v-if="canUpdateOrganization()" :team="team" />
<SectionBorder />
<SectionBorder />
<OrganizationBillableRate v-if="canUpdateOrganization()" :team="team" />
<SectionBorder />
<OrganizationFormatSettings v-if="canUpdateOrganization()" :team="team" />
<SectionBorder />
<OrganizationFormatSettings v-if="canUpdateOrganization()" :team="team" />
<SectionBorder />
<template v-if="permissions.canDeleteTeam && !team.personal_team">
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
<OrganizationTimeEntrySettings v-if="canUpdateOrganization()" />
<SectionBorder />
<template v-if="permissions.canDeleteTeam && !team.personal_team">
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
</template>
</template>
</div>
</div>

View File

@@ -320,6 +320,7 @@ const OrganizationResource = z
is_personal: z.boolean(),
billable_rate: z.union([z.number(), z.null()]),
employees_can_see_billable_rates: z.boolean(),
prevent_overlapping_time_entries: z.boolean(),
currency: z.string(),
currency_symbol: z.string(),
number_format: NumberFormat,
@@ -334,6 +335,7 @@ const OrganizationUpdateRequest = z
name: z.string().max(255),
billable_rate: z.union([z.number(), z.null()]),
employees_can_see_billable_rates: z.boolean(),
prevent_overlapping_time_entries: z.boolean(),
number_format: NumberFormat,
currency_format: CurrencyFormat,
date_format: DateFormat,