mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 04:02:15 +01:00
add project members to project show page
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user