diff --git a/e2e/members.spec.ts b/e2e/members.spec.ts new file mode 100644 index 00000000..79a8c473 --- /dev/null +++ b/e2e/members.spec.ts @@ -0,0 +1 @@ +// TODO: Edit Billable Rate diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index 5549f1c8..648cfd25 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -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 diff --git a/resources/js/Components/Common/BillableRateInput.vue b/resources/js/Components/Common/BillableRateInput.vue index ca34c905..dbf5371c 100644 --- a/resources/js/Components/Common/BillableRateInput.vue +++ b/resources/js/Components/Common/BillableRateInput.vue @@ -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({ default: null, }); +const billableRateInput = ref(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));
+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({ + // @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; +} + + + + + diff --git a/resources/js/Components/Common/Member/MemberMoreOptionsDropdown.vue b/resources/js/Components/Common/Member/MemberMoreOptionsDropdown.vue index e86a15a4..eaa53d21 100644 --- a/resources/js/Components/Common/Member/MemberMoreOptionsDropdown.vue +++ b/resources/js/Components/Common/Member/MemberMoreOptionsDropdown.vue @@ -1,11 +1,12 @@ diff --git a/resources/js/Components/Common/Member/MemberTableRow.vue b/resources/js/Components/Common/Member/MemberTableRow.vue index 1adf00c9..8626a481 100644 --- a/resources/js/Components/Common/Member/MemberTableRow.vue +++ b/resources/js/Components/Common/Member/MemberTableRow.vue @@ -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) }}
- {{ member.billable_rate ?? '--' }} + {{ + member.billable_rate ? formatCents(member.billable_rate) : '--' + }}
@@ -82,8 +89,12 @@ async function invitePlaceholder(id: string) { >
+ diff --git a/resources/js/utils/api.ts b/resources/js/utils/api.ts index 7fd8122c..e6c4e3b0 100644 --- a/resources/js/utils/api.ts +++ b/resources/js/utils/api.ts @@ -86,6 +86,8 @@ export type MemberIndexResponse = ZodiosResponseByAlias< >; export type Member = MemberIndexResponse['data'][0]; +export type UpdateMemberBody = ZodiosBodyByAlias; + export type CreateTagBody = ZodiosBodyByAlias; export type ImportType = ZodiosResponseByAlias< diff --git a/resources/js/utils/permissions.ts b/resources/js/utils/permissions.ts index 35016581..2d81eff6 100644 --- a/resources/js/utils/permissions.ts +++ b/resources/js/utils/permissions.ts @@ -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'); } diff --git a/resources/js/utils/useMembers.ts b/resources/js/utils/useMembers.ts index 1c5cd0f3..da81e8f4 100644 --- a/resources/js/utils/useMembers.ts +++ b/resources/js/utils/useMembers.ts @@ -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(() => { return membersResponse.value?.data || []; }); - return { members, fetchMembers, removeMember }; + return { members, fetchMembers, removeMember, updateMember }; });