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 manual time entries
// TODO: Test Grouped time entries by description/project // TODO: Test Grouped time entries by description/project
// TODO: Add Test for Date Update

View File

@@ -6,15 +6,20 @@ import {
getOrganizationCurrencySymbol, getOrganizationCurrencySymbol,
} from '../../utils/money'; } from '../../utils/money';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useFocus } from '@vueuse/core';
defineProps<{ const props = defineProps<{
name: string; name: string;
focus?: boolean;
}>(); }>();
const model = defineModel<number | null>({ const model = defineModel<number | null>({
default: null, default: null,
}); });
const billableRateInput = ref<HTMLInputElement | null>(null);
useFocus(billableRateInput, { initialValue: props.focus });
function cleanUpDecimalValue(value: string) { function cleanUpDecimalValue(value: string) {
value = value.replace(/,/g, ''); value = value.replace(/,/g, '');
value = value.replace(getOrganizationCurrencySymbol(), ''); value = value.replace(getOrganizationCurrencySymbol(), '');
@@ -67,7 +72,7 @@ const inputValue = ref(formatValue(model.value));
<div class="relative"> <div class="relative">
<TextInput <TextInput
:id="name" :id="name"
ref="projectMemberRateInput" ref="billableRateInput"
v-model="inputValue" v-model="inputValue"
@blur="updateRate($event.target.value)" @blur="updateRate($event.target.value)"
type="text" 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"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue'; 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 type { Member } from '@/utils/api';
import { canDeleteMembers } from '@/utils/permissions'; import { canDeleteMembers, canUpdateMembers } from '@/utils/permissions';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
edit: [];
}>(); }>();
const props = defineProps<{ const props = defineProps<{
member: Member; member: Member;
@@ -13,7 +14,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown> <Dropdown align="bottom-end">
<template #trigger> <template #trigger>
<svg <svg
data-testid="client_actions" data-testid="client_actions"
@@ -31,15 +32,26 @@ const props = defineProps<{
</svg> </svg>
</template> </template>
<template #content> <template #content>
<button <div class="min-w-[150px]">
v-if="canDeleteMembers()" <button
@click="emit('delete')" v-if="canUpdateMembers()"
:aria-label="'Delete Member ' + props.member.name" @click="emit('edit')"
data-testid="client_delete" :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"> 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> <PencilSquareIcon
<span>Delete</span> class="w-5 text-icon-active"></PencilSquareIcon>
</button> <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> </template>
</Dropdown> </Dropdown>
</template> </template>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from '../../../openapi.json.client'; import { api } from '../../../openapi.json.client';
import { computed, ref } from 'vue'; 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 { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification'; 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[]>(() => { const members = computed<Member[]>(() => {
return membersResponse.value?.data || []; return membersResponse.value?.data || [];
}); });
return { members, fetchMembers, removeMember }; return { members, fetchMembers, removeMember, updateMember };
}); });