mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
add ability to change role of a user
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
import DialogModal from '@/Components/DialogModal.vue';
|
import DialogModal from '@/Components/DialogModal.vue';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { Member, UpdateMemberBody } from '@/utils/api';
|
import type { Member, UpdateMemberBody } from '@/utils/api';
|
||||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||||
import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers';
|
import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers';
|
||||||
@@ -10,6 +10,8 @@ import InputLabel from '@/Components/InputLabel.vue';
|
|||||||
import MemberBillableRateModal from '@/Components/Common/Member/MemberBillableRateModal.vue';
|
import MemberBillableRateModal from '@/Components/Common/Member/MemberBillableRateModal.vue';
|
||||||
import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue';
|
import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue';
|
||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
|
import MemberRoleSelect from '@/Components/Common/Member/MemberRoleSelect.vue';
|
||||||
|
import MemberOwnershipTransferConfirmModal from '@/Components/Common/Member/MemberOwnershipTransferConfirmModal.vue';
|
||||||
|
|
||||||
const { updateMember } = useMembersStore();
|
const { updateMember } = useMembersStore();
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
@@ -25,21 +27,39 @@ const memberBody = ref<UpdateMemberBody>({
|
|||||||
billable_rate: props.member.billable_rate,
|
billable_rate: props.member.billable_rate,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function submit(billableRateUpdateTimeEntries: boolean) {
|
async function submitBillableRate(billableRateUpdateTimeEntries: boolean) {
|
||||||
memberBody.value.billable_rate_update_time_entries =
|
memberBody.value.billable_rate_update_time_entries =
|
||||||
billableRateUpdateTimeEntries;
|
billableRateUpdateTimeEntries;
|
||||||
|
if (memberBody.value.role === 'owner' && props.member.role !== 'owner') {
|
||||||
|
show.value = false;
|
||||||
|
showOwnershipTransferConfirmModal.value = true;
|
||||||
|
} else {
|
||||||
|
await submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
await updateMember(props.member.id, memberBody.value);
|
await updateMember(props.member.id, memberBody.value);
|
||||||
show.value = false;
|
show.value = false;
|
||||||
showBillableRateModal.value = false;
|
showBillableRateModal.value = false;
|
||||||
|
showOwnershipTransferConfirmModal.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showBillableRateModal = ref(false);
|
const showBillableRateModal = ref(false);
|
||||||
function openBillableRateModalIfNeeded() {
|
const showOwnershipTransferConfirmModal = ref(false);
|
||||||
|
|
||||||
|
function saveWithChecks() {
|
||||||
if (memberBody.value.billable_rate !== props.member.billable_rate) {
|
if (memberBody.value.billable_rate !== props.member.billable_rate) {
|
||||||
showBillableRateModal.value = true;
|
showBillableRateModal.value = true;
|
||||||
show.value = false;
|
show.value = false;
|
||||||
|
} else if (
|
||||||
|
memberBody.value.role === 'owner' &&
|
||||||
|
props.member.role !== 'owner'
|
||||||
|
) {
|
||||||
|
show.value = false;
|
||||||
|
showOwnershipTransferConfirmModal.value = true;
|
||||||
} else {
|
} else {
|
||||||
submit(false);
|
submitBillableRate(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +79,29 @@ watch(billableRateSelect, () => {
|
|||||||
memberBody.value.billable_rate = props.member.billable_rate ?? 0;
|
memberBody.value.billable_rate = props.member.billable_rate ?? 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const roleDescriptionTexts = {
|
||||||
|
'owner':
|
||||||
|
'The owner has full access of the organization. The owner is the only role that can: delete the organization, transfer the ownership to another user and access to the billing settings',
|
||||||
|
'admin':
|
||||||
|
'The admin has full access to the organization, except for the stuff that only the owner can do.',
|
||||||
|
'manager':
|
||||||
|
'The manager has full access to projects, clients, tags, time entries, and reports, but can not manage the organization or the users.',
|
||||||
|
'employee':
|
||||||
|
'An employee is a user that is only using the application to track time, but has no administrative rights.',
|
||||||
|
'placeholder':
|
||||||
|
'Placeholder users can not do anything in the organization. They are not billed and can be used to remove users from the organization without deleting their time entries.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleDescription = computed(() => {
|
||||||
|
if (
|
||||||
|
memberBody.value.role &&
|
||||||
|
memberBody.value.role in roleDescriptionTexts
|
||||||
|
) {
|
||||||
|
return roleDescriptionTexts[memberBody.value.role];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -67,7 +110,11 @@ watch(billableRateSelect, () => {
|
|||||||
v-model:show="showBillableRateModal"
|
v-model:show="showBillableRateModal"
|
||||||
:member-name="member.name"
|
:member-name="member.name"
|
||||||
:newBillableRate="memberBody.billable_rate"
|
:newBillableRate="memberBody.billable_rate"
|
||||||
@submit="submit"></MemberBillableRateModal>
|
@submit="submitBillableRate"></MemberBillableRateModal>
|
||||||
|
<MemberOwnershipTransferConfirmModal
|
||||||
|
:member-name="member.name"
|
||||||
|
v-model:show="showOwnershipTransferConfirmModal"
|
||||||
|
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
||||||
<DialogModal closeable :show="show" @close="show = false">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
@@ -76,41 +123,57 @@ watch(billableRateSelect, () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="pb-5 pt-2 divide-y divide-border-secondary">
|
||||||
<div class="col-span-6 sm:col-span-4 flex-1 flex space-x-5">
|
<div class="pb-5 flex space-x-6">
|
||||||
<div>
|
<div>
|
||||||
<InputLabel for="billableType" value="Billable" />
|
<InputLabel for="role" value="Role" />
|
||||||
<MemberBillableSelect
|
<MemberRoleSelect
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
name="billableType"
|
name="role"
|
||||||
v-model="billableRateSelect"></MemberBillableSelect>
|
v-model="memberBody.role"></MemberRoleSelect>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex-1 text-xs flex items-center pt-6">
|
||||||
class="flex-1"
|
<p>{{ roleDescription }}</p>
|
||||||
v-if="billableRateSelect === 'custom-rate'">
|
</div>
|
||||||
<InputLabel
|
</div>
|
||||||
for="memberBillableRate"
|
<div class="flex items-center space-x-4 pt-5">
|
||||||
value="Billable Rate" />
|
<div class="col-span-6 sm:col-span-4 flex-1 flex space-x-5">
|
||||||
<BillableRateInput
|
<div>
|
||||||
focus
|
<InputLabel for="billableType" value="Billable" />
|
||||||
class="w-full"
|
<MemberBillableSelect
|
||||||
@keydown.enter="openBillableRateModalIfNeeded()"
|
class="mt-2"
|
||||||
name="memberBillableRate"
|
name="billableType"
|
||||||
v-model="
|
v-model="
|
||||||
memberBody.billable_rate
|
billableRateSelect
|
||||||
"></BillableRateInput>
|
"></MemberBillableSelect>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex-1"
|
||||||
|
v-if="billableRateSelect === 'custom-rate'">
|
||||||
|
<InputLabel
|
||||||
|
for="memberBillableRate"
|
||||||
|
value="Billable Rate" />
|
||||||
|
<BillableRateInput
|
||||||
|
focus
|
||||||
|
class="w-full"
|
||||||
|
@keydown.enter="saveWithChecks()"
|
||||||
|
name="memberBillableRate"
|
||||||
|
v-model="
|
||||||
|
memberBody.billable_rate
|
||||||
|
"></BillableRateInput>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
class="ms-3"
|
class="ms-3"
|
||||||
:class="{ 'opacity-25': saving }"
|
:class="{ 'opacity-25': saving }"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
@click="openBillableRateModalIfNeeded()">
|
@click="saveWithChecks()">
|
||||||
Update Member
|
Update Member
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
|
import DialogModal from '@/Components/DialogModal.vue';
|
||||||
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||||
|
|
||||||
|
const show = defineModel('show', { default: false });
|
||||||
|
const saving = defineModel('saving', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
memberName: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<span> Confirm Ownership Transfer </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="col-span-6 sm:col-span-4 flex-1">
|
||||||
|
<p class="py-1 text-center">
|
||||||
|
You are about to transfer the ownership of this
|
||||||
|
organization to {{ memberName }}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
class="ms-3"
|
||||||
|
@click="emit('submit')"
|
||||||
|
:class="{ 'opacity-25': saving }"
|
||||||
|
:disabled="saving">
|
||||||
|
Confirm Transfer
|
||||||
|
</PrimaryButton>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
52
resources/js/Components/Common/Member/MemberRoleSelect.vue
Normal file
52
resources/js/Components/Common/Member/MemberRoleSelect.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SelectDropdown from '@/Components/Common/SelectDropdown.vue';
|
||||||
|
import Badge from '@/Components/Common/Badge.vue';
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import type { Role } from '@/types/jetstream';
|
||||||
|
import { usePage } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
const model = defineModel<string>({
|
||||||
|
default: 'employee',
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = usePage<{
|
||||||
|
availableRoles: Role[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getKeyFromItem(item: Role) {
|
||||||
|
return item.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameFromItem(item: Role) {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameForKey(key: string | undefined) {
|
||||||
|
const item = page.props.availableRoles.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="page.props.availableRoles">
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user