add project members to project show page

This commit is contained in:
Gregor Vostrak
2024-04-10 03:06:48 +02:00
parent 5293bc7a05
commit b94c159af0
19 changed files with 731 additions and 46 deletions

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import { ref } from 'vue';
import type { CreateProjectMemberBody, ProjectMember } from '@/utils/api';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
import MemberCombobox from '@/Components/Common/Member/MemberCombobox.vue';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
const { createProjectMember } = useProjectMembersStore();
const show = defineModel('show', { default: false });
const saving = ref(false);
const props = defineProps<{
projectId: string;
existingMembers: ProjectMember[];
}>();
const projectMember = ref<CreateProjectMemberBody>({
user_id: '',
billable_rate: null,
});
async function submit() {
await createProjectMember(props.projectId, projectMember.value);
show.value = false;
}
const projectNameInput = ref<HTMLInputElement | null>(null);
useFocus(projectNameInput, { initialValue: true });
</script>
<template>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
<div class="flex space-x-2">
<span>Add Project Member</span>
</div>
</template>
<template #content>
<div class="grid grid-cols-3 items-center space-x-4">
<div class="col-span-3 sm:col-span-2">
<MemberCombobox
:hidden-members="props.existingMembers"
v-model="projectMember.user_id"></MemberCombobox>
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<BillableRateInput
v-model="
projectMember.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">
Add Project Member
</PrimaryButton>
</template>
</DialogModal>
</template>
<style scoped></style>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon } from '@heroicons/vue/20/solid';
import type { ProjectMember } from '@/utils/api';
import { useMembersStore } from '@/utils/useMembers';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
const emit = defineEmits<{
delete: [];
}>();
const props = defineProps<{
projectMember: ProjectMember;
}>();
const { members } = storeToRefs(useMembersStore());
const currentMember = computed(() => {
return members.value.find(
(member) => member.id === props.projectMember.user_id
);
});
</script>
<template>
<Dropdown>
<template #trigger>
<svg
data-testid="project_actions"
:aria-label="
'Actions for Project Member ' + currentMember?.name
"
class="h-10 w-10 p-2 rounded-full hover:bg-card-background opacity-20 group-hover:opacity-100 transition"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</template>
<template #content>
<button
@click.prevent="emit('delete')"
:aria-label="'Delete Project Member ' + currentMember?.name"
data-testid="project_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>Remove from Team</span>
</button>
</template>
</Dropdown>
</template>
<style scoped></style>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import SecondaryButton from '@/Components/SecondaryButton.vue';
import { PlusIcon } from '@heroicons/vue/16/solid';
import { ref } from 'vue';
import ProjectMemberTableRow from '@/Components/Common/ProjectMember/ProjectMemberTableRow.vue';
import { UserGroupIcon } from '@heroicons/vue/24/solid';
import ProjectMemberTableHeading from '@/Components/Common/ProjectMember/ProjectMemberTableHeading.vue';
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
import type { ProjectMember } from '@/utils/api';
defineProps<{
projectId: string;
projectMembers: ProjectMember[];
}>();
const createProjectMember = ref(false);
</script>
<template>
<ProjectMemberCreateModal
:existing-members="projectMembers"
:project-id="projectId"
v-model:show="createProjectMember"></ProjectMemberCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
data-testid="project_table"
class="grid min-w-full"
style="grid-template-columns: 1fr 150px 150px 80px">
<ProjectMemberTableHeading></ProjectMemberTableHeading>
<div
class="col-span-5 py-24 text-center"
v-if="projectMembers.length === 0">
<UserGroupIcon
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-white font-semibold">No project members</h3>
<p class="pb-5">Add the first project member!</p>
<SecondaryButton
@click="createProjectMember = true"
:icon="PlusIcon"
>Add a new Project Member
</SecondaryButton>
</div>
<template
v-for="projectMember in projectMembers"
:key="projectMember.id">
<ProjectMemberTableRow
:project-member="projectMember"></ProjectMemberTableRow>
</template>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import TableHeading from '@/Components/Common/TableHeading.vue';
</script>
<template>
<TableHeading>
<div
class="py-1.5 pr-3 text-left text-sm font-semibold text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
Name
</div>
<div class="px-3 py-1.5 text-left text-sm font-semibold text-white">
Billable Rate
</div>
<div class="px-3 py-1.5 text-left text-sm font-semibold text-white">
Role
</div>
<div class="relative py-1.5 pl-3 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<span class="sr-only">Edit</span>
</div>
</TableHeading>
</template>
<style scoped></style>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import type { ProjectMember } from '@/utils/api';
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import TableRow from '@/Components/TableRow.vue';
import { useMembersStore } from '@/utils/useMembers';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
import ProjectMemberMoreOptionsDropdown from '@/Components/Common/ProjectMember/ProjectMemberMoreOptionsDropdown.vue';
import { formatCents } from '@/utils/money';
import { capitalizeFirstLetter } from '@/utils/format';
const props = defineProps<{
projectMember: ProjectMember;
}>();
function deleteProjectMember() {
useProjectMembersStore().deleteProjectMember(
props.projectMember.project_id,
props.projectMember.id
);
}
const { members } = storeToRefs(useMembersStore());
const member = computed(() => {
return members.value.find(
(member) => member.id === props.projectMember.user_id
);
});
</script>
<template>
<TableRow>
<div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
<span>
{{ member?.name }}
</span>
</div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
{{
projectMember.billable_rate
? formatCents(projectMember.billable_rate)
: '--'
}}
</div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
{{ capitalizeFirstLetter(member?.role ?? '') }}
</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">
<ProjectMemberMoreOptionsDropdown
:project-member="projectMember"
@delete="
deleteProjectMember
"></ProjectMemberMoreOptionsDropdown>
</div>
</TableRow>
</template>
<style scoped></style>