Customised training file filters, role and member search and fixed invite redirect bug
This commit is contained in:
parent
e9f329be54
commit
c40c7b5e3a
5 changed files with 144 additions and 25 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<Button type="primary" @click="roleModalVisible = true">
|
||||
Create Role
|
||||
</Button>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue