mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +01:00
add invitations tab to members page
This commit is contained in:
@@ -24,9 +24,9 @@ class InvitationResource extends BaseResource
|
|||||||
/** @var string $id ID of the invitation */
|
/** @var string $id ID of the invitation */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** @var string $email Email */
|
/** @var string $email Email */
|
||||||
'user_id' => $this->resource->email,
|
'email' => $this->resource->email,
|
||||||
/** @var string $role Role */
|
/** @var string $role Role */
|
||||||
'name' => $this->resource->role,
|
'role' => $this->resource->role,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const importData_Body = z
|
|||||||
.object({ type: z.string(), data: z.string() })
|
.object({ type: z.string(), data: z.string() })
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const InvitationResource = z
|
const InvitationResource = z
|
||||||
.object({ id: z.string(), user_id: z.string(), name: z.string() })
|
.object({ id: z.string(), email: z.string(), role: z.string() })
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
||||||
const invite_Body = z
|
const invite_Body = z
|
||||||
@@ -449,7 +449,10 @@ const endpoints = makeApi([
|
|||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
schema: z.object({ message: z.string() }).passthrough(),
|
schema: z.union([
|
||||||
|
z.object({ message: z.string() }).passthrough(),
|
||||||
|
z.object({ message: z.string() }).passthrough(),
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useInvitationsStore } from '@/utils/useInvitations';
|
||||||
|
import InvitationTableRow from '@/Components/Common/Invitation/InvitationTableRow.vue';
|
||||||
|
import InvitationTableHeading from '@/Components/Common/Invitation/InvitationTableHeading.vue';
|
||||||
|
|
||||||
|
const { invitations } = storeToRefs(useInvitationsStore());
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await useInvitationsStore().fetchInvitations();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||||
|
<div class="inline-block min-w-full align-middle">
|
||||||
|
<div
|
||||||
|
data-testid="client_table"
|
||||||
|
class="grid min-w-full"
|
||||||
|
style="grid-template-columns: 1fr 1fr">
|
||||||
|
<InvitationTableHeading></InvitationTableHeading>
|
||||||
|
<template
|
||||||
|
v-for="invitation in invitations"
|
||||||
|
:key="invitation.id">
|
||||||
|
<InvitationTableRow
|
||||||
|
:invitation="invitation"></InvitationTableRow>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import TableHeading from '@/Components/Common/TableHeading.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableHeading>
|
||||||
|
<div class="px-3 py-1.5 text-left font-semibold text-white">Email</div>
|
||||||
|
<div class="px-3 py-1.5 text-left font-semibold text-white">Role</div>
|
||||||
|
</TableHeading>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Invitation } from '@/utils/api';
|
||||||
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
|
import { capitalizeFirstLetter } from '../../../utils/format';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
invitation: Invitation;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow>
|
||||||
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
|
||||||
|
{{ invitation.email }}
|
||||||
|
</div>
|
||||||
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
|
||||||
|
{{ capitalizeFirstLetter(invitation.role) }}
|
||||||
|
</div>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -23,6 +23,8 @@ const addTeamMemberForm = useForm({
|
|||||||
role: null as string | null,
|
role: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
@@ -31,6 +33,7 @@ async function submit() {
|
|||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
addTeamMemberForm.reset();
|
addTeamMemberForm.reset();
|
||||||
|
emit('close');
|
||||||
show.value = false;
|
show.value = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue';
|
|
||||||
import MemberTableHeading from '@/Components/Common/Member/MemberTableHeading.vue';
|
import MemberTableHeading from '@/Components/Common/Member/MemberTableHeading.vue';
|
||||||
import MemberTableRow from '@/Components/Common/Member/MemberTableRow.vue';
|
import MemberTableRow from '@/Components/Common/Member/MemberTableRow.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersStore } from '@/utils/useMembers';
|
||||||
|
|
||||||
const { members } = storeToRefs(useMembersStore());
|
const { members } = storeToRefs(useMembersStore());
|
||||||
const createClient = ref(false);
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await useMembersStore().fetchMembers();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientCreateModal v-model:show="createClient"></ClientCreateModal>
|
|
||||||
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ import MemberTable from '@/Components/Common/Member/MemberTable.vue';
|
|||||||
import MemberInviteModal from '@/Components/Common/Member/MemberInviteModal.vue';
|
import MemberInviteModal from '@/Components/Common/Member/MemberInviteModal.vue';
|
||||||
import type { Role } from '@/types/jetstream';
|
import type { Role } from '@/types/jetstream';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
|
import InvitationTable from '@/Components/Common/Invitation/InvitationTable.vue';
|
||||||
|
|
||||||
const inviteMember = ref(false);
|
const inviteMember = ref(false);
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
availableRoles: Role[];
|
availableRoles: Role[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const activeTab = ref<'all' | 'invitations'>('all');
|
||||||
|
function isActiveTab(tab: string) {
|
||||||
|
return activeTab.value === tab;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -26,9 +32,16 @@ defineProps<{
|
|||||||
<div class="flex items-center space-x-4 sm:space-x-6">
|
<div class="flex items-center space-x-4 sm:space-x-6">
|
||||||
<PageTitle :icon="UserGroupIcon" title="Members"> </PageTitle>
|
<PageTitle :icon="UserGroupIcon" title="Members"> </PageTitle>
|
||||||
<TabBar>
|
<TabBar>
|
||||||
<TabBarItem active>All</TabBarItem>
|
<TabBarItem
|
||||||
<TabBarItem>Active</TabBarItem>
|
:active="isActiveTab('all')"
|
||||||
<TabBarItem>Inactive</TabBarItem>
|
@click="activeTab = 'all'"
|
||||||
|
>All</TabBarItem
|
||||||
|
>
|
||||||
|
<TabBarItem
|
||||||
|
:active="isActiveTab('invitations')"
|
||||||
|
@click="activeTab = 'invitations'"
|
||||||
|
>Invitations</TabBarItem
|
||||||
|
>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton :icon="PlusIcon" @click="inviteMember = true"
|
<SecondaryButton :icon="PlusIcon" @click="inviteMember = true"
|
||||||
@@ -36,8 +49,10 @@ defineProps<{
|
|||||||
>
|
>
|
||||||
<MemberInviteModal
|
<MemberInviteModal
|
||||||
:available-roles="availableRoles"
|
:available-roles="availableRoles"
|
||||||
v-model:show="inviteMember"></MemberInviteModal>
|
v-model:show="inviteMember"
|
||||||
|
@close="activeTab = 'invitations'"></MemberInviteModal>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MemberTable></MemberTable>
|
<MemberTable v-if="activeTab === 'all'"></MemberTable>
|
||||||
|
<InvitationTable v-if="activeTab === 'invitations'"></InvitationTable>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import { api } from '../../../openapi.json.client';
|
|||||||
|
|
||||||
export type SolidTimeApi = ApiOf<typeof api>;
|
export type SolidTimeApi = ApiOf<typeof api>;
|
||||||
|
|
||||||
|
export type InvitationsIndexResponse = ZodiosResponseByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'getInvitations'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type CreateInvitationBody = ZodiosBodyByAlias<SolidTimeApi, 'invite'>;
|
||||||
|
|
||||||
|
export type Invitation = InvitationsIndexResponse['data'][0];
|
||||||
|
|
||||||
export type TimeEntryResponse = ZodiosResponseByAlias<
|
export type TimeEntryResponse = ZodiosResponseByAlias<
|
||||||
SolidTimeApi,
|
SolidTimeApi,
|
||||||
'getTimeEntries'
|
'getTimeEntries'
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function capitalizeFirstLetter(string: string) {
|
export function capitalizeFirstLetter(string: string) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
return string?.charAt(0)?.toUpperCase() + string?.slice(1);
|
||||||
}
|
}
|
||||||
|
|||||||
54
resources/js/utils/useInvitations.ts
Normal file
54
resources/js/utils/useInvitations.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from '../../../openapi.json.client';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import type {
|
||||||
|
InvitationsIndexResponse,
|
||||||
|
CreateInvitationBody,
|
||||||
|
Invitation,
|
||||||
|
} from '@/utils/api';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
|
|
||||||
|
export const useInvitationsStore = defineStore('invitations', () => {
|
||||||
|
const invitationsResponse = ref<InvitationsIndexResponse | null>(null);
|
||||||
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
|
async function fetchInvitations() {
|
||||||
|
const organization = getCurrentOrganizationId();
|
||||||
|
if (organization) {
|
||||||
|
invitationsResponse.value = await handleApiRequestNotifications(
|
||||||
|
api.getInvitations({
|
||||||
|
params: {
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
'Failed to fetch invitations'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInvitation(
|
||||||
|
inviteBody: CreateInvitationBody
|
||||||
|
): Promise<undefined> {
|
||||||
|
const organization = getCurrentOrganizationId();
|
||||||
|
if (organization) {
|
||||||
|
await handleApiRequestNotifications(
|
||||||
|
api.invite(inviteBody, {
|
||||||
|
params: {
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'User successfully invited',
|
||||||
|
'Failed to invite user'
|
||||||
|
);
|
||||||
|
await fetchInvitations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const invitations = computed<Invitation[]>(() => {
|
||||||
|
return invitationsResponse.value?.data || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { invitations, fetchInvitations, createInvitation };
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user