add is_billable support for create / update project and in timetracker, fixes ST-217

This commit is contained in:
Gregor Vostrak
2024-06-06 16:04:37 +02:00
committed by Constantin Graf
parent d9244d1ab4
commit 7fb58ea341
22 changed files with 343 additions and 32 deletions

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import SelectDropdown from '@/Components/Common/SelectDropdown.vue';
import type { BillableKey } from '@/utils/useProjects';
import Badge from '@/Components/Common/Badge.vue';
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
const model = defineModel<BillableKey>({
default: 'non-billable',
});
type Option = { key: BillableKey; name: string };
const options: Option[] = [
{
key: 'non-billable',
name: 'Non-billable',
},
{
key: 'default-rate',
name: 'Default Rate',
},
{
key: 'custom-rate',
name: 'Custom Rate',
},
];
function getKeyFromItem(item: Option) {
return item.key;
}
function getNameFromItem(item: Option) {
return item.name;
}
function getNameForKey(key: BillableKey | undefined) {
const item = options.find((item) => getKeyFromItem(item) === key);
if (item) {
return getNameFromItem(item);
}
return '';
}
</script>
<template>
<SelectDropdown
v-model="model"
:get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameFromItem"
:items="options">
<template #trigger>
<Badge size="xlarge" class="bg-input-background cursor-pointer">
<span>
{{ getNameForKey(model) }}
</span>
<ChevronDownIcon class="text-muted w-5"></ChevronDownIcon>
</Badge>
</template>
</SelectDropdown>
</template>
<style scoped></style>

View File

@@ -13,9 +13,9 @@ import Badge from '@/Components/Common/Badge.vue';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import InputLabel from '@/Components/InputLabel.vue';
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
const { createProject } = useProjectsStore();
const { clients } = storeToRefs(useClientsStore());
@@ -27,6 +27,7 @@ const project = ref<CreateProjectBody>({
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
});
async function submit() {
@@ -37,6 +38,7 @@ async function submit() {
color: getRandomColor(),
client_id: null,
billable_rate: null,
is_billable: false,
};
}
@@ -87,12 +89,6 @@ const currentClientName = computed(() => {
autocomplete="projectName" />
</div>
</div>
<div class="sm:max-w-[120px]">
<InputLabel for="billableRate" value="Billable Rate" />
<BillableRateInput
v-model="project.billable_rate"
name="billableRate" />
</div>
<div>
<InputLabel for="client" value="Client" />
<ClientDropdown class="mt-2" v-model="project.client_id">
@@ -112,10 +108,14 @@ const currentClientName = computed(() => {
</ClientDropdown>
</div>
</div>
<ProjectEditBillableSection
v-model:isBillable="project.is_billable"
v-model:billableRate="
project.billable_rate
"></ProjectEditBillableSection>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': saving }"

View File

@@ -61,6 +61,7 @@ async function addProjectIfNoneExists() {
{
name: searchValue.value,
color: getRandomColor(),
is_billable: false,
},
{ params: { organization: page.props.auth.user.current_team_id } }
);

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import InputLabel from '@/Components/InputLabel.vue';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
import ProjectBillableSelect from '@/Components/Common/Project/ProjectBillableSelect.vue';
import { computed, onMounted, ref, watch } from 'vue';
import type { BillableKey } from '@/utils/useProjects';
const billableRateSelect = ref<BillableKey>('non-billable');
const billableRate = defineModel<number | null>('billableRate');
const isBillable = defineModel<boolean>('isBillable');
onMounted(() => {
if (isBillable.value === true) {
if (billableRate.value) {
billableRateSelect.value = 'custom-rate';
} else {
billableRateSelect.value = 'default-rate';
}
}
});
watch(billableRateSelect, () => {
if (billableRateSelect.value === 'non-billable') {
isBillable.value = false;
billableRate.value = null;
} else if (billableRateSelect.value === 'default-rate') {
isBillable.value = true;
billableRate.value = null;
} else {
isBillable.value = true;
}
});
billableRateSelect.value = 'non-billable';
const billableOptionInfoTexts: { [key in BillableKey]: string } = {
'non-billable':
'New time entries for this project not be marked billable by default.',
'default-rate':
'New time entries for this project will be billable at the default rate by default.',
'custom-rate':
'New time entries for this project will be billable at a custom rate by default.',
};
const billableOptionInfoText = computed(() => {
return billableOptionInfoTexts[billableRateSelect.value];
});
</script>
<template>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4 pt-6">
<div>
<InputLabel for="billable" value="Billable Default" />
<ProjectBillableSelect
v-model="billableRateSelect"
class="mt-2"></ProjectBillableSelect>
</div>
<div
class="sm:max-w-[120px]"
v-if="billableRateSelect === 'custom-rate'">
<InputLabel for="billableRate" value="Billable Rate" />
<BillableRateInput v-model="billableRate" name="billableRate" />
</div>
</div>
<div class="flex items-center text-muted pt-2 pl-1">
<span>
<span class="font-semibold"> Info: </span>
{{ billableOptionInfoText }}
</span>
</div>
</template>
<style scoped></style>

View File

@@ -12,8 +12,8 @@ import { twMerge } from 'tailwind-merge';
import Badge from '@/Components/Common/Badge.vue';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
const { updateProject } = useProjectsStore();
const { clients } = storeToRefs(useClientsStore());
@@ -29,6 +29,7 @@ const project = ref<CreateProjectBody>({
color: props.originalProject.color,
client_id: props.originalProject.client_id,
billable_rate: props.originalProject.billable_rate,
is_billable: props.originalProject.is_billable,
});
async function submit() {
@@ -77,11 +78,6 @@ const currentClientName = computed(() => {
required
autocomplete="projectName" />
</div>
<div class="sm:max-w-[120px]">
<BillableRateInput
v-model="project.billable_rate"
name="billable_rate" />
</div>
<div class="">
<ClientDropdown v-model="project.client_id">
<template #trigger>
@@ -98,6 +94,11 @@ const currentClientName = computed(() => {
</ClientDropdown>
</div>
</div>
<ProjectEditBillableSection
v-model:isBillable="project.is_billable"
v-model:billableRate="
project.billable_rate
"></ProjectEditBillableSection>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>

View File

@@ -33,6 +33,17 @@ function deleteProject() {
useProjectsStore().deleteProject(props.project.id);
}
const billableRateInfo = computed(() => {
if (props.project.is_billable) {
if (props.project.billable_rate) {
return formatCents(props.project.billable_rate);
} else {
return 'Default Rate';
}
}
return '--';
});
const showEditProjectModal = ref(false);
</script>
@@ -61,11 +72,7 @@ const showEditProjectModal = ref(false);
<div v-else>No client</div>
</div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
{{
project.billable_rate
? formatCents(project.billable_rate)
: '--'
}}
{{ billableRateInfo }}
</div>
<div
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">