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

@@ -1487,6 +1487,21 @@ const endpoints = makeApi([
},
],
},
{
method: 'get',
path: '/v1/users/me/time-entries/active',
alias: 'getMyActiveTimeEntry',
description: `This endpoint is independent of organization.`,
requestFormat: 'json',
response: z.object({ data: TimeEntryResource }).passthrough(),
errors: [
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
],
},
]);
export const api = new Zodios('/api', endpoints);

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import TextInput from '@/Components/TextInput.vue';
import {
formatMoney,
getOrganizationCurrencyString,
getOrganizationCurrencySymbol,
} from '../../utils/money';
const model = defineModel({
default: null,
type: Number,
});
function cleanUpDecimalValue(value: string) {
value = value.replace(/,/g, '');
value = value.replace(getOrganizationCurrencySymbol(), '');
return value.replace(/\./g, '');
}
function updateRate(value: string) {
if (value.includes(',')) {
const parts = value.split(',');
const lastPart = (parts[parts.length - 1] = parts[parts.length - 1]);
if (lastPart.length === 2) {
// we detected a decimal number with 2 digits after the comma
value = cleanUpDecimalValue(value);
model.value = parseInt(value);
}
} else if (value.includes('.')) {
const parts = value.split('.');
const lastPart = (parts[parts.length - 1] = parts[parts.length - 1]);
if (lastPart.length === 2) {
value = cleanUpDecimalValue(value);
model.value = parseInt(value);
}
} else {
// if it doesn't contain a comma or a dot, it's probably a whole number so let's convert it to cents
model.value = parseInt(cleanUpDecimalValue(value)) * 100;
}
}
function formatCents(modelValue: number) {
const formattedValue = formatMoney(
modelValue / 100,
getOrganizationCurrencyString()
);
return formattedValue.replace(getOrganizationCurrencySymbol(), '');
}
</script>
<template>
<div class="relative">
<TextInput
id="projectMemberRate"
ref="projectMemberRateInput"
:modelValue="formatCents(model)"
@blur="updateRate($event.target.value)"
type="text"
placeholder="Billable Rate"
class="mt-1 block w-full"
autocomplete="teamMemberRate" />
<span>
<div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium">
<span>
{{ getOrganizationCurrencyString() }}
</span>
</div>
</span>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div class="rounded-lg border border-card-border">
<slot></slot>
</div>
</template>

View File

@@ -8,16 +8,21 @@ defineProps<{
</script>
<template>
<h3
class="text-white font-bold pb-4 text-base flex items-center space-x-2.5">
<component
v-if="icon"
:is="icon"
class="w-6 text-icon-default"></component>
<span>
{{ title }}
</span>
</h3>
<div class="flex w-full items-center justify-between pb-4">
<h3
class="text-white font-bold text-base flex items-center space-x-2.5">
<component
v-if="icon"
:is="icon"
class="w-6 text-icon-default"></component>
<span>
{{ title }}
</span>
</h3>
<div>
<slot name="actions"></slot>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,190 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { type Component, computed, nextTick, onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import ClientDropdownItem from '@/Components/Common/Client/ClientDropdownItem.vue';
import { useMembersStore } from '@/utils/useMembers';
import { UserIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import TextInput from '@/Components/TextInput.vue';
import { useFocus } from '@vueuse/core';
import type { ProjectMember } from '@/utils/api';
const membersStore = useMembersStore();
const { members } = storeToRefs(membersStore);
const model = defineModel<string>({
default: '',
});
const props = defineProps<{
hiddenMembers: ProjectMember[];
}>();
const searchInput = ref<HTMLInputElement | null>(null);
const dropdownViewport = ref<Component | null>(null);
const searchValue = ref('');
function isMemberSelected(id: string) {
return model.value === id;
}
const { focused } = useFocus(searchInput, { initialValue: true });
const filteredMembers = computed(() => {
return members.value.filter((member) => {
return (
member.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '') &&
!props.hiddenMembers.some(
(hiddenMember) => hiddenMember.user_id === member.id
) &&
member.is_placeholder === false
);
});
});
watch(filteredMembers, () => {
resetHighlightedItem();
});
onMounted(() => {
resetHighlightedItem();
});
function resetHighlightedItem() {
if (filteredMembers.value.length > 0) {
highlightedItemId.value = filteredMembers.value[0].id;
}
}
function updateSearchValue(event: Event) {
const newInput = (event.target as HTMLInputElement).value;
if (newInput === ' ') {
searchValue.value = '';
const highlightedClientId = highlightedItemId.value;
if (highlightedClientId) {
const highlightedClient = members.value.find(
(member) => member.id === highlightedClientId
);
if (highlightedClient) {
model.value = highlightedClient.id;
}
}
} else {
searchValue.value = newInput;
}
}
const emit = defineEmits(['update:modelValue', 'changed']);
function updateMember(newValue: string | null) {
if (newValue) {
model.value = newValue;
nextTick(() => {
emit('changed');
});
}
}
function moveHighlightUp() {
if (highlightedItem.value) {
const currentHightlightedIndex = filteredMembers.value.indexOf(
highlightedItem.value
);
if (currentHightlightedIndex === 0) {
highlightedItemId.value =
filteredMembers.value[filteredMembers.value.length - 1].id;
} else {
highlightedItemId.value =
filteredMembers.value[currentHightlightedIndex - 1].id;
}
}
}
function moveHighlightDown() {
if (highlightedItem.value) {
const currentHightlightedIndex = filteredMembers.value.indexOf(
highlightedItem.value
);
if (currentHightlightedIndex === filteredMembers.value.length - 1) {
highlightedItemId.value = filteredMembers.value[0].id;
} else {
highlightedItemId.value =
filteredMembers.value[currentHightlightedIndex + 1].id;
}
}
}
const highlightedItemId = ref<string | null>(null);
const highlightedItem = computed(() => {
return members.value.find(
(member) => member.id === highlightedItemId.value
);
});
const currentValue = computed(() => {
if (model.value) {
return members.value.find((member) => member.id === model.value)?.name;
}
return searchValue.value;
});
const hasMemberSelected = computed(() => {
return model.value !== '';
});
</script>
<template>
<div class="flex relative">
<div
class="absolute h-full items-center px-3 w-full flex justify-between">
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
<button
v-if="hasMemberSelected"
@click="model = ''"
class="focus:text-accent-200 focus:bg-card-background text-muted">
<XMarkIcon class="relative z-10 w-4"></XMarkIcon>
</button>
</div>
<TextInput
:value="currentValue"
@input="updateSearchValue"
data-testid="member_dropdown_search"
@keydown.enter.prevent="updateMember(highlightedItemId)"
@keydown.up.prevent="moveHighlightUp"
class="relative w-full pl-10"
@keydown.down.prevent="moveHighlightDown"
placeholder="Search for a member..."
ref="searchInput" />
</div>
<Dropdown
align="left"
width="300"
v-model="focused"
:closeOnContentClick="true">
<template #content>
<div ref="dropdownViewport" class="w-60">
<div
v-for="member in filteredMembers"
:key="member.id"
role="option"
:value="member.id"
:class="{
'bg-card-background-active':
member.id === highlightedItemId,
}"
@click="updateMember(member.id)"
data-testid="client_dropdown_entries"
:data-client-id="member.id">
<ClientDropdownItem
:selected="isMemberSelected(member.id)"
:name="member.name"></ClientDropdownItem>
</div>
</div>
</template>
</Dropdown>
</template>
<style scoped></style>

View File

@@ -4,6 +4,7 @@ import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/20/solid';
import { useClientsStore } from '@/utils/useClients';
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
import TableRow from '@/Components/TableRow.vue';
import { capitalizeFirstLetter } from '../../../utils/format';
const props = defineProps<{
member: Member;
@@ -12,10 +13,6 @@ const props = defineProps<{
function removeMember() {
useClientsStore().deleteClient(props.member.id);
}
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
</script>
<template>

View File

@@ -12,7 +12,7 @@ import TableHeading from '@/Components/Common/TableHeading.vue';
Client
</div>
<div class="px-3 py-1.5 text-left text-sm font-semibold text-white">
Team
Billable Rate
</div>
<div class="px-3 py-1.5 text-left text-sm font-semibold text-white">
Status

View File

@@ -54,24 +54,7 @@ function deleteProject() {
<div v-else>No client</div>
</div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
<div class="isolate flex -space-x-1 opacity-50">
<img
class="relative z-30 inline-block h-6 w-6 rounded-full ring-4 ring-card-background"
src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt="" />
<img
class="relative z-20 inline-block h-6 w-6 rounded-full ring-4 ring-card-background"
src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt="" />
<img
class="relative z-10 inline-block h-6 w-6 rounded-full ring-4 ring-card-background"
src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80"
alt="" />
<img
class="relative z-0 inline-block h-6 w-6 rounded-full ring-4 ring-card-background"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt="" />
</div>
{{ project.billable_rate ?? '--' }}
</div>
<div
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">

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>

View File

@@ -102,7 +102,7 @@ function onBackgroundClick() {
class="absolute z-50 mt-2 rounded-md shadow-lg"
:class="[widthClass, alignmentClasses]"
style="display: none"
@click="onContentClick">
@click.prevent="onContentClick">
<div
class="rounded-lg ring-1 relative ring-black ring-opacity-5"
:class="contentClasses">

View File

@@ -3,13 +3,23 @@ import MainContainer from '@/Pages/MainContainer.vue';
import AppLayout from '@/Layouts/AppLayout.vue';
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useProjectsStore } from '@/utils/useProjects';
import { storeToRefs } from 'pinia';
import { ChevronRightIcon } from '@heroicons/vue/20/solid';
import {
ChevronRightIcon,
CheckCircleIcon,
UserGroupIcon,
} from '@heroicons/vue/20/solid';
import { Link } from '@inertiajs/vue3';
import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue';
import TaskTable from '@/Components/Common/Task/TaskTable.vue';
import CardTitle from '@/Components/Common/CardTitle.vue';
import Card from '@/Components/Common/Card.vue';
import ProjectMemberTable from '@/Components/Common/ProjectMember/ProjectMemberTable.vue';
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
const { projects } = storeToRefs(useProjectsStore());
const project = computed(() => {
@@ -20,8 +30,14 @@ const project = computed(() => {
);
});
const createTask = ref(false);
const createProjectMember = ref(false);
const projectId = route()?.params?.project as string;
const projectId: string = route().params.project;
const { projectMembers } = storeToRefs(useProjectMembersStore());
onMounted(() => {
useProjectMembersStore().fetchProjectMembers(projectId);
});
</script>
<template>
@@ -37,7 +53,7 @@ const projectId: string = route().params.project;
class="flex items-center space-x-2.5">
<FolderIcon
class="w-6 text-icon-default"></FolderIcon>
<span> Projects </span>
<span class="font-medium">Projects</span>
</Link>
</div>
</li>
@@ -60,13 +76,49 @@ const projectId: string = route().params.project;
</li>
</ol>
</nav>
<SecondaryButton :icon="PlusIcon" @click="createTask = true"
>Create Task
</SecondaryButton>
<TaskCreateModal
:project-id="projectId"
v-model:show="createTask"></TaskCreateModal>
</MainContainer>
<TaskTable :project-id="projectId"></TaskTable>
<MainContainer>
<div class="grid grid-cols-2 gap-x-6 pt-6">
<div>
<CardTitle title="Tasks" :icon="CheckCircleIcon">
<template #actions>
<SecondaryButton
:icon="PlusIcon"
@click="createTask = true"
>Create Task
</SecondaryButton>
<TaskCreateModal
:project-id="projectId"
v-model:show="createTask"></TaskCreateModal>
</template>
</CardTitle>
<Card>
<TaskTable :project-id="projectId"></TaskTable>
</Card>
</div>
<div>
<CardTitle title="Project Members" :icon="UserGroupIcon">
<template #actions>
<SecondaryButton
:icon="PlusIcon"
@click="createProjectMember = true">
Add Member
</SecondaryButton>
<ProjectMemberCreateModal
:project-id="projectId"
:existing-members="projectMembers"
v-model:show="
createProjectMember
"></ProjectMemberCreateModal>
</template>
</CardTitle>
<Card>
<ProjectMemberTable
:project-members="projectMembers"
:project-id="projectId"></ProjectMemberTable>
</Card>
</div>
</div>
</MainContainer>
</AppLayout>
</template>

View File

@@ -24,6 +24,18 @@ export type CreateProjectBody = ZodiosBodyByAlias<
'createProject'
>;
export type ProjectMemberResponse = ZodiosResponseByAlias<
SolidTimeApi,
'getProjectMembers'
>;
export type CreateProjectMemberBody = ZodiosBodyByAlias<
SolidTimeApi,
'createProjectMember'
>;
export type ProjectMember = ProjectMemberResponse['data'][0];
export type CreateTaskBody = ZodiosBodyByAlias<SolidTimeApi, 'createTask'>;
export type CreateClientBody = ZodiosBodyByAlias<SolidTimeApi, 'createClient'>;

View File

@@ -0,0 +1,3 @@
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

@@ -1,6 +1,21 @@
export function formatMoney(amount: number, currency: string) {
export function formatMoney(
amount: number,
currency: string = getOrganizationCurrencyString()
) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount);
}
export function formatCents(amount: number) {
return formatMoney(amount / 100);
}
export function getOrganizationCurrencyString() {
return 'EUR';
}
export function getOrganizationCurrencySymbol() {
return '€';
}

View File

@@ -0,0 +1,71 @@
import { defineStore } from 'pinia';
import { api } from '../../../openapi.json.client';
import { computed, ref } from 'vue';
import type {
CreateProjectMemberBody,
ProjectMember,
ProjectMemberResponse,
} from '@/utils/api';
import { getCurrentOrganizationId } from '@/utils/useUser';
export const useProjectMembersStore = defineStore('project-members', () => {
const projectMemberResponse = ref<ProjectMemberResponse | null>(null);
async function fetchProjectMembers(projectId: string) {
const organization = getCurrentOrganizationId();
if (organization) {
projectMemberResponse.value = await api.getProjectMembers({
params: {
organization: organization,
project: projectId,
},
});
}
}
async function createProjectMember(
projectId: string,
projectMemberBody: CreateProjectMemberBody
) {
const organization = getCurrentOrganizationId();
if (organization) {
await api.createProjectMember(projectMemberBody, {
params: {
organization: organization,
project: projectId,
},
});
await fetchProjectMembers(projectId);
}
}
async function deleteProjectMember(
projectId: string,
projectMemberId: string
) {
const organizationId = getCurrentOrganizationId();
if (organizationId) {
await api.deleteProjectMember(
{},
{
params: {
organization: organizationId,
projectMember: projectMemberId,
},
}
);
await fetchProjectMembers(projectId);
}
}
const projectMembers = computed<ProjectMember[]>(
() => projectMemberResponse.value?.data || []
);
return {
projectMembers,
fetchProjectMembers,
createProjectMember,
deleteProjectMember,
};
});