Customised training file filters, role and member search and fixed invite redirect bug

This commit is contained in:
Viswamedha Nalabotu 2026-02-27 13:58:00 +00:00
parent e9f329be54
commit c40c7b5e3a
5 changed files with 144 additions and 25 deletions

View file

@ -19,11 +19,17 @@ class TrainingFileViewSet(ModelViewSet):
def get_queryset(self):
user = self.request.user
return TrainingFile.objects.filter(
queryset = TrainingFile.objects.filter(
Q(role__organization__owner=user) |
Q(role__organization__members=user)
).distinct()
organization_uuid = self.request.query_params.get('organization_uuid')
if organization_uuid:
queryset = queryset.filter(role__organization__uuid=organization_uuid)
return queryset
def perform_create(self, serializer):
role_uuid = self.request.data.get('role')
@ -33,7 +39,7 @@ class TrainingFileViewSet(ModelViewSet):
return Response({'error': 'Role not found'}, status=status.HTTP_404_NOT_FOUND)
is_owner = role.organization.owner == self.request.user
is_member = role.organization.members.filter(id=self.request.user.id).exists()
is_member = role.organization.members.filter(uuid=self.request.user.uuid).exists()
if not (is_owner or is_member):
return Response({'error': 'Permission denied'}, status=status.HTTP_403_FORBIDDEN)

View file

@ -12,18 +12,22 @@ const loading = ref(false)
const accepting = ref(false)
const accepted = ref(false)
const error = ref<string | null>(null)
const joinedOrganizationUuid = ref<string | null>(null)
const acceptInvite = async () => {
accepting.value = true
error.value = null
try {
const response = await apiClient.post<{ message: string; success: boolean; uuid: string }>(
const response = await apiClient.post<{ message?: string; organization?: { uuid?: string } }>(
API.organization.invites.join(inviteUuid),
)
joinedOrganizationUuid.value = response.data?.organization?.uuid || null
message.success(response.data?.message || 'Successfully joined organization')
accepted.value = true
setTimeout(() => {
if (response.data?.uuid) router.push(`/organization/${response.data.uuid}`)
if (joinedOrganizationUuid.value) {
router.push(`/organization/${joinedOrganizationUuid.value}`)
}
else router.push('/')
}, 1500)
} catch (err) {

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Card,
@ -32,6 +32,8 @@ const members = ref<User[]>([])
const invites = ref<InviteToken[]>([])
const newInviteMaxUses = ref<number>(1)
const Roles = ref<Role[]>([])
const memberSearch = ref('')
const roleSearch = ref('')
const loading = ref(false)
const creatingRole = ref(false)
const deletingRoleUuid = ref<string | null>(null)
@ -45,6 +47,37 @@ const newInviteUrl = ref('')
const editingDescription = ref(false)
const newDescription = ref('')
const filteredMembers = computed(() => {
const query = memberSearch.value.trim().toLowerCase()
if (!query) return members.value
return members.value.filter((member) => {
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase()
return fullName.includes(query)
})
})
const filteredRoles = computed(() => {
const query = roleSearch.value.trim().toLowerCase()
if (!query) return Roles.value
return Roles.value.filter((role) => {
const name = role.name?.toLowerCase() || ''
const description = role.description?.toLowerCase() || ''
return name.includes(query) || description.includes(query)
})
})
const memberEmptyMessage = computed(() => {
if (members.value.length === 0) return 'No members in this organization yet.'
if (memberSearch.value.trim()) return 'No members match your search.'
return 'No members in this organization yet.'
})
const roleEmptyMessage = computed(() => {
if (Roles.value.length === 0) return 'No Roles in this organization yet.'
if (roleSearch.value.trim()) return 'No roles match your search.'
return 'No Roles in this organization yet.'
})
const fetchOrganization = async () => {
loading.value = true
try {
@ -276,18 +309,35 @@ onMounted(async () => {
<div class="section">
<div class="section-header">
<Typography.Title :level="4" style="color: #ffffff !important">
Members ({{ members.length }})
Members ({{ filteredMembers.length }})
</Typography.Title>
<Input
v-model:value="memberSearch"
allow-clear
class="search-input"
placeholder="Search members by name"
style="max-width: 280px"
/>
</div>
<List :data-source="members" :bordered="false">
<List
v-if="filteredMembers.length > 0"
:data-source="filteredMembers"
:bordered="false"
>
<template #renderItem="{ item }">
<List.Item class="member-item">
<List.Item.Meta
:title="`${item.first_name} ${item.last_name}`"
:description="item.bio || 'No bio provided'"
:description="item.email_address"
/>
<Space>
<Tag v-if="item.uuid === organization.owner.uuid" color="blue">
Owner
</Tag>
<Tag v-else :color="item.is_manager ? 'purple' : 'default'">
{{ item.is_manager ? 'Manager' : 'Member' }}
</Tag>
<Button
v-if="item.uuid !== organization.owner.uuid"
danger
@ -296,11 +346,13 @@ onMounted(async () => {
>
Remove
</Button>
<Tag v-else color="blue">Owner</Tag>
</Space>
</List.Item>
</template>
</List>
<Typography.Paragraph v-else type="secondary">
{{ memberEmptyMessage }}
</Typography.Paragraph>
</div>
</Tabs.TabPane>
@ -361,14 +413,27 @@ onMounted(async () => {
<div class="section">
<div class="section-header">
<Typography.Title :level="4" style="color: #ffffff !important">
Roles ({{ Roles.length }})
Roles ({{ filteredRoles.length }})
</Typography.Title>
<Space>
<Input
v-model:value="roleSearch"
allow-clear
class="search-input"
placeholder="Search roles by name or description"
style="width: 300px"
/>
<Button type="primary" @click="roleModalVisible = true">
Create Role
</Button>
</Space>
</div>
<List v-if="Roles.length > 0" :data-source="Roles" :bordered="false">
<List
v-if="filteredRoles.length > 0"
:data-source="filteredRoles"
:bordered="false"
>
<template #renderItem="{ item }">
<List.Item class="Role-item">
<List.Item.Meta
@ -390,7 +455,7 @@ onMounted(async () => {
</template>
</List>
<Typography.Paragraph v-else type="secondary">
No Roles in this organization yet.
{{ roleEmptyMessage }}
</Typography.Paragraph>
</div>
</Tabs.TabPane>
@ -495,4 +560,19 @@ onMounted(async () => {
background: #111827;
border-color: #334155;
}
:deep(.search-input) {
background: #1f2937 !important;
border-color: #475569 !important;
color: #f8fafc !important;
}
:deep(.search-input::placeholder) {
color: #cbd5e1 !important;
}
:deep(.search-input::selection) {
background: #475569 !important;
color: #f8fafc !important;
}
</style>

View file

@ -133,7 +133,9 @@ const selectRole = async (roleUuid: string) => {
const fetchTrainingFiles = async () => {
if (!organization.value?.uuid) return
try {
const response = await apiClient.get<TrainingFile[]>(API.knowledge.trainingFiles.list())
const response = await apiClient.get<TrainingFile[]>(API.knowledge.trainingFiles.list(), {
params: { organization_uuid: organization.value.uuid },
})
trainingFiles.value = response.data
} catch (error) {
console.error('Failed to fetch training files:', error)
@ -166,6 +168,20 @@ const selectedFile = ref<File | null>(null)
const fileDescription = ref('')
const selectedRoleUuid = ref<string>('')
const handleOpenUploadModal = () => {
if (!isManager.value) {
message.error('Only managers can upload training files')
return
}
if (roles.value.length === 0) {
message.error('No roles found for this organization. Create a role first in Manage Organization.')
return
}
showUploadModal.value = true
}
const handleFileSelected = (file: File) => {
selectedFile.value = file
}
@ -386,11 +402,19 @@ onMounted(async () => {
<div style="margin-bottom: 1rem">
<Button
type="primary"
@click="showUploadModal = true"
:disabled="!isManager"
@click="handleOpenUploadModal"
style="margin-bottom: 1rem"
>
Upload Training File
</Button>
<Typography.Paragraph
v-if="isManager && roles.length === 0"
type="secondary"
style="margin: 0.25rem 0 0"
>
Create a role in Manage Organization before uploading training files.
</Typography.Paragraph>
</div>
<div v-if="trainingFiles.length > 0">

View file

@ -59,6 +59,11 @@ const resetCreateOrganizationForm = () => {
}
const handleCreateOrganization = async () => {
if (!auth.isGeneralManager) {
message.error('Only managers can create organizations')
return
}
const name = createOrgForm.value.name.trim()
const description = createOrgForm.value.description.trim()