mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 04:32:15 +01:00
add project member role selection to create and update modals, fixes ST-446
This commit is contained in:
@@ -29,6 +29,8 @@ class ProjectMemberResource extends BaseResource
|
|||||||
'member_id' => $this->resource->member_id,
|
'member_id' => $this->resource->member_id,
|
||||||
/** @var string $project_id ID of the project */
|
/** @var string $project_id ID of the project */
|
||||||
'project_id' => $this->resource->project_id,
|
'project_id' => $this->resource->project_id,
|
||||||
|
/** @var string $role Role of the project member */
|
||||||
|
'role' => $this->resource->role->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
|||||||
import MemberCombobox from '@/Components/Common/Member/MemberCombobox.vue';
|
import MemberCombobox from '@/Components/Common/Member/MemberCombobox.vue';
|
||||||
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
|
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
|
import { InputLabel } from '@/packages/ui/src';
|
||||||
|
import ProjectMemberRoleSelect from '@/Components/Common/ProjectMember/ProjectMemberRoleSelect.vue';
|
||||||
const { createProjectMember } = useProjectMembersStore();
|
const { createProjectMember } = useProjectMembersStore();
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -24,6 +26,7 @@ const props = defineProps<{
|
|||||||
const projectMember = ref<CreateProjectMemberBody>({
|
const projectMember = ref<CreateProjectMemberBody>({
|
||||||
member_id: '',
|
member_id: '',
|
||||||
billable_rate: null,
|
billable_rate: null,
|
||||||
|
role: 'normal',
|
||||||
});
|
});
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -32,6 +35,7 @@ async function submit() {
|
|||||||
projectMember.value = {
|
projectMember.value = {
|
||||||
member_id: '',
|
member_id: '',
|
||||||
billable_rate: null,
|
billable_rate: null,
|
||||||
|
role: 'normal',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,13 +53,17 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="grid grid-cols-3 items-center space-x-4">
|
<div class="items-center space-y-4">
|
||||||
<div class="col-span-3 sm:col-span-2">
|
<div>
|
||||||
|
<InputLabel value="Member" class="mb-2"></InputLabel>
|
||||||
<MemberCombobox
|
<MemberCombobox
|
||||||
:hidden-members="props.existingMembers"
|
:hidden-members="props.existingMembers"
|
||||||
v-model="projectMember.member_id"></MemberCombobox>
|
v-model="projectMember.member_id"></MemberCombobox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
<div>
|
||||||
|
<InputLabel
|
||||||
|
value="Billable Rate"
|
||||||
|
for="billable_rate"></InputLabel>
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
name="billable_rate"
|
name="billable_rate"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
@@ -63,6 +71,11 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
projectMember.billable_rate
|
projectMember.billable_rate
|
||||||
"></BillableRateInput>
|
"></BillableRateInput>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<InputLabel value="Role" class="mb-2"></InputLabel>
|
||||||
|
<ProjectMemberRoleSelect
|
||||||
|
v-model="projectMember.role"></ProjectMemberRoleSelect>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import type {
|
|||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
import {
|
||||||
|
type ProjectMemberRole,
|
||||||
|
useProjectMembersStore,
|
||||||
|
} from '@/utils/useProjectMembers';
|
||||||
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
|
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
|
||||||
import { UserIcon } from '@heroicons/vue/24/solid';
|
|
||||||
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue';
|
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue';
|
||||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
|
import ProjectMemberRoleSelect from '@/Components/Common/ProjectMember/ProjectMemberRoleSelect.vue';
|
||||||
const { updateProjectMember } = useProjectMembersStore();
|
const { updateProjectMember } = useProjectMembersStore();
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
@@ -26,6 +29,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const projectMemberBody = ref<UpdateProjectMemberBody>({
|
const projectMemberBody = ref<UpdateProjectMemberBody>({
|
||||||
billable_rate: props.projectMember.billable_rate,
|
billable_rate: props.projectMember.billable_rate,
|
||||||
|
role: props.projectMember.role as ProjectMemberRole,
|
||||||
});
|
});
|
||||||
const showBillableRateModal = ref(false);
|
const showBillableRateModal = ref(false);
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -40,6 +44,7 @@ async function submit() {
|
|||||||
show.value = false;
|
show.value = false;
|
||||||
projectMemberBody.value = {
|
projectMemberBody.value = {
|
||||||
billable_rate: null,
|
billable_rate: null,
|
||||||
|
role: 'normal',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ watch(
|
|||||||
if (value) {
|
if (value) {
|
||||||
projectMemberBody.value = {
|
projectMemberBody.value = {
|
||||||
billable_rate: props.projectMember.billable_rate,
|
billable_rate: props.projectMember.billable_rate,
|
||||||
|
role: props.projectMember.role as ProjectMemberRole,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +75,7 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
<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">
|
||||||
<span>Edit Project Member</span>
|
<span>Edit Project Member "{{ props.name }}"</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,23 +86,26 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
:new-billable-rate="projectMemberBody.billable_rate"
|
:new-billable-rate="projectMemberBody.billable_rate"
|
||||||
@close="showBillableRateModal = false"
|
@close="showBillableRateModal = false"
|
||||||
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
||||||
<div class="grid grid-cols-3 items-center space-x-4">
|
<div>
|
||||||
<div
|
<div class="items-center space-y-4">
|
||||||
class="col-span-3 sm:col-span-2 space-x-2 flex items-center">
|
<div>
|
||||||
<UserIcon class="w-4 text-muted"></UserIcon>
|
<InputLabel
|
||||||
<span>{{ props.name }}</span>
|
value="Billable Rate"
|
||||||
</div>
|
for="billable_rate"></InputLabel>
|
||||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
<BillableRateInput
|
||||||
<InputLabel
|
name="billable_rate"
|
||||||
for="billable_rate"
|
:currency="getOrganizationCurrencyString()"
|
||||||
value="Billable Rate"></InputLabel>
|
v-model="
|
||||||
<BillableRateInput
|
projectMemberBody.billable_rate
|
||||||
@keydown.enter="submit"
|
"></BillableRateInput>
|
||||||
:currency="getOrganizationCurrencyString()"
|
</div>
|
||||||
name="billable_rate"
|
<div>
|
||||||
v-model="
|
<InputLabel value="Role" class="mb-2"></InputLabel>
|
||||||
projectMemberBody.billable_rate
|
<ProjectMemberRoleSelect
|
||||||
"></BillableRateInput>
|
v-model="
|
||||||
|
projectMemberBody.role
|
||||||
|
"></ProjectMemberRoleSelect>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
|
||||||
|
import Badge from '@/packages/ui/src/Badge.vue';
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import type { ProjectMemberRole } from '@/utils/useProjectMembers';
|
||||||
|
|
||||||
|
type ProjectMemberRoleItem = { key: ProjectMemberRole; name: string };
|
||||||
|
|
||||||
|
const projectMemberRoles: ProjectMemberRoleItem[] = [
|
||||||
|
{
|
||||||
|
key: 'normal',
|
||||||
|
name: 'Normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'manager',
|
||||||
|
name: 'Manager',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const model = defineModel<string>({
|
||||||
|
default: 'normal',
|
||||||
|
});
|
||||||
|
|
||||||
|
function getKeyFromItem(item: ProjectMemberRoleItem) {
|
||||||
|
return item.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameFromItem(item: ProjectMemberRoleItem) {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameForKey(key: string | undefined) {
|
||||||
|
return projectMemberRoles.find((item) => item.key === key)?.name ?? '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectDropdown
|
||||||
|
v-model="model"
|
||||||
|
:get-key-from-item="getKeyFromItem"
|
||||||
|
:get-name-for-item="getNameFromItem"
|
||||||
|
:items="projectMemberRoles">
|
||||||
|
<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>
|
||||||
@@ -57,7 +57,7 @@ const showEditModal = ref(false);
|
|||||||
}}
|
}}
|
||||||
</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">
|
||||||
{{ capitalizeFirstLetter(member?.role ?? '') }}
|
{{ capitalizeFirstLetter(projectMember?.role ?? '') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium sm:pr-0 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium sm:pr-0 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const ProjectResource = z
|
|||||||
is_billable: z.boolean(),
|
is_billable: z.boolean(),
|
||||||
estimated_time: z.union([z.number(), z.null()]),
|
estimated_time: z.union([z.number(), z.null()]),
|
||||||
spent_time: z.number().int(),
|
spent_time: z.number().int(),
|
||||||
|
limited_visibility: z.boolean(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const ProjectStoreRequest = z
|
const ProjectStoreRequest = z
|
||||||
@@ -99,16 +100,22 @@ const ProjectMemberResource = z
|
|||||||
billable_rate: z.union([z.number(), z.null()]),
|
billable_rate: z.union([z.number(), z.null()]),
|
||||||
member_id: z.string(),
|
member_id: z.string(),
|
||||||
project_id: z.string(),
|
project_id: z.string(),
|
||||||
|
role: z.string(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
const ProjectMemberRole = z.enum(['manager', 'normal']);
|
||||||
const ProjectMemberStoreRequest = z
|
const ProjectMemberStoreRequest = z
|
||||||
.object({
|
.object({
|
||||||
member_id: z.string().uuid(),
|
member_id: z.string().uuid(),
|
||||||
billable_rate: z.union([z.number(), z.null()]).optional(),
|
billable_rate: z.union([z.number(), z.null()]).optional(),
|
||||||
|
role: ProjectMemberRole,
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const ProjectMemberUpdateRequest = z
|
const ProjectMemberUpdateRequest = z
|
||||||
.object({ billable_rate: z.union([z.number(), z.null()]) })
|
.object({
|
||||||
|
billable_rate: z.union([z.number(), z.null()]),
|
||||||
|
role: ProjectMemberRole,
|
||||||
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const TagResource = z
|
const TagResource = z
|
||||||
@@ -257,6 +264,7 @@ export const schemas = {
|
|||||||
ProjectStoreRequest,
|
ProjectStoreRequest,
|
||||||
ProjectUpdateRequest,
|
ProjectUpdateRequest,
|
||||||
ProjectMemberResource,
|
ProjectMemberResource,
|
||||||
|
ProjectMemberRole,
|
||||||
ProjectMemberStoreRequest,
|
ProjectMemberStoreRequest,
|
||||||
ProjectMemberUpdateRequest,
|
ProjectMemberUpdateRequest,
|
||||||
TagResource,
|
TagResource,
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ watchEffect(() => {
|
|||||||
tasks: [],
|
tasks: [],
|
||||||
estimated_time: null,
|
estimated_time: null,
|
||||||
spent_time: 0,
|
spent_time: 0,
|
||||||
|
limited_visibility: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
export type ProjectMemberRole = 'normal' | 'manager';
|
||||||
|
|
||||||
export const useProjectMembersStore = defineStore('project-members', () => {
|
export const useProjectMembersStore = defineStore('project-members', () => {
|
||||||
const projectMemberResponse = ref<ProjectMemberResponse | null>(null);
|
const projectMemberResponse = ref<ProjectMemberResponse | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user