add update member billable rate , fixes ST-241

This commit is contained in:
Gregor Vostrak
2024-06-05 18:28:47 +02:00
parent b1795392ad
commit 474c0de3ac
9 changed files with 141 additions and 17 deletions

1
e2e/members.spec.ts Normal file
View File

@@ -0,0 +1 @@
// TODO: Edit Billable Rate

View File

@@ -466,3 +466,5 @@ test.skip('test that load more works when the end of page is reached', async ({
// TODO: Test manual time entries
// TODO: Test Grouped time entries by description/project
// TODO: Add Test for Date Update

View File

@@ -6,15 +6,20 @@ import {
getOrganizationCurrencySymbol,
} from '../../utils/money';
import { ref, watch } from 'vue';
import { useFocus } from '@vueuse/core';
defineProps<{
const props = defineProps<{
name: string;
focus?: boolean;
}>();
const model = defineModel<number | null>({
default: null,
});
const billableRateInput = ref<HTMLInputElement | null>(null);
useFocus(billableRateInput, { initialValue: props.focus });
function cleanUpDecimalValue(value: string) {
value = value.replace(/,/g, '');
value = value.replace(getOrganizationCurrencySymbol(), '');
@@ -67,7 +72,7 @@ const inputValue = ref(formatValue(model.value));
<div class="relative">
<TextInput
:id="name"
ref="projectMemberRateInput"
ref="billableRateInput"
v-model="inputValue"
@blur="updateRate($event.target.value)"
type="text"

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import { ref } from 'vue';
import type { Member, UpdateMemberBody } from '@/utils/api';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useMembersStore } from '@/utils/useMembers';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
const { updateMember } = useMembersStore();
const show = defineModel('show', { default: false });
const saving = ref(false);
const props = defineProps<{
member: Member;
}>();
const memberBody = ref<UpdateMemberBody>({
// @ts-expect-error - The role value is always valid
role: props.member.role,
billable_rate: props.member.billable_rate,
});
async function submit() {
await updateMember(props.member.id, memberBody.value);
show.value = false;
}
</script>
<template>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
<div class="flex space-x-2">
<span> Update Member </span>
</div>
</template>
<template #content>
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<BillableRateInput
focus
name="billable_rate"
v-model="memberBody.billable_rate"></BillableRateInput>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="submit">
Update Client
</PrimaryButton>
</template>
</DialogModal>
</template>
<style scoped></style>

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon } from '@heroicons/vue/20/solid';
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import type { Member } from '@/utils/api';
import { canDeleteMembers } from '@/utils/permissions';
import { canDeleteMembers, canUpdateMembers } from '@/utils/permissions';
const emit = defineEmits<{
delete: [];
edit: [];
}>();
const props = defineProps<{
member: Member;
@@ -13,7 +14,7 @@ const props = defineProps<{
</script>
<template>
<Dropdown>
<Dropdown align="bottom-end">
<template #trigger>
<svg
data-testid="client_actions"
@@ -31,15 +32,26 @@ const props = defineProps<{
</svg>
</template>
<template #content>
<button
v-if="canDeleteMembers()"
@click="emit('delete')"
:aria-label="'Delete Member ' + props.member.name"
data-testid="client_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>
<div class="min-w-[150px]">
<button
v-if="canUpdateMembers()"
@click="emit('edit')"
:aria-label="'Edit Member ' + props.member.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
v-if="canDeleteMembers()"
@click="emit('delete')"
:aria-label="'Delete Member ' + props.member.name"
data-testid="member_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>
</div>
</template>
</Dropdown>
</template>

View File

@@ -10,11 +10,16 @@ import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import { canInvitePlaceholderMembers } from '@/utils/permissions';
import { useMembersStore } from '@/utils/useMembers';
import { ref } from 'vue';
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
import { formatCents } from '../../../utils/money';
const props = defineProps<{
member: Member;
}>();
const showEditMemberModal = ref(false);
function removeMember() {
useMembersStore().removeMember(props.member.id);
}
@@ -56,7 +61,9 @@ async function invitePlaceholder(id: string) {
{{ capitalizeFirstLetter(member.role) }}
</div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
{{ member.billable_rate ?? '--' }}
{{
member.billable_rate ? formatCents(member.billable_rate) : '--'
}}
</div>
<div
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">
@@ -82,8 +89,12 @@ async function invitePlaceholder(id: string) {
>
<MemberMoreOptionsDropdown
:member="member"
@edit="showEditMemberModal = true"
@delete="removeMember"></MemberMoreOptionsDropdown>
</div>
<MemberEditModal
:member="member"
v-model:show="showEditMemberModal"></MemberEditModal>
</TableRow>
</template>

View File

@@ -86,6 +86,8 @@ export type MemberIndexResponse = ZodiosResponseByAlias<
>;
export type Member = MemberIndexResponse['data'][0];
export type UpdateMemberBody = ZodiosBodyByAlias<SolidTimeApi, 'updateMember'>;
export type CreateTagBody = ZodiosBodyByAlias<SolidTimeApi, 'createTag'>;
export type ImportType = ZodiosResponseByAlias<

View File

@@ -69,6 +69,10 @@ export function canViewMembers() {
return currentUserHasPermission('members:view');
}
export function canUpdateMembers() {
return currentUserHasPermission('members:update');
}
export function canDeleteMembers() {
return currentUserHasPermission('members:delete');
}

View File

@@ -1,7 +1,11 @@
import { defineStore } from 'pinia';
import { api } from '../../../openapi.json.client';
import { computed, ref } from 'vue';
import type { Member, MemberIndexResponse } from '@/utils/api';
import type {
Member,
MemberIndexResponse,
UpdateMemberBody,
} from '@/utils/api';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
@@ -46,9 +50,30 @@ export const useMembersStore = defineStore('members', () => {
}
}
async function updateMember(
memberId: string,
memberBody: UpdateMemberBody
) {
const organization = getCurrentOrganizationId();
if (organization) {
await handleApiRequestNotifications(
() =>
api.updateMember(memberBody, {
params: {
organization: organization,
member: memberId,
},
}),
'Member updated successfully',
'Failed to update member'
);
await fetchMembers();
}
}
const members = computed<Member[]>(() => {
return membersResponse.value?.data || [];
});
return { members, fetchMembers, removeMember };
return { members, fetchMembers, removeMember, updateMember };
});