Dynavera/site/src/views/OrganizationsView.vue

179 lines
5.8 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Card, List, Typography, Spin, message, Button, Modal, Form, Input } from 'ant-design-vue'
import { apiClient, isAxiosError, API } from '../router/api'
import type { Organization } from '../types/organization'
import { useUserStore } from '../stores/userStore'
const router = useRouter()
const auth = useUserStore()
const organizations = ref<Organization[]>([])
const loading = ref(false)
const creatingOrganization = ref(false)
const showCreateOrgModal = ref(false)
const createOrgForm = ref({
name: '',
description: '',
})
const fetchOrganizations = async () => {
loading.value = true
try {
const resp = await apiClient.get<Organization[]>(API.organizations())
organizations.value = resp.data || []
if (organizations.value.length === 1 && !auth.isGeneralManager) {
const onlyOrg = organizations.value[0]
const id = onlyOrg.uuid || String(onlyOrg.id)
await router.replace(`/organization/${id}`)
}
} catch (err: unknown) {
console.error('Failed to fetch organizations:', err)
if (isAxiosError(err)) {
message.error(
err.response?.data?.error ||
err.response?.data?.detail ||
'Failed to load organizations',
)
} else if (err instanceof Error) {
message.error(err.message)
} else {
message.error('Failed to load organizations')
}
} finally {
loading.value = false
}
}
onMounted(() => {
auth.fetchSession(true)
fetchOrganizations()
})
const openOrg = (org: Organization) => {
const id = org.uuid || String(org.id)
router.push(`/organization/${id}`)
}
const resetCreateOrganizationForm = () => {
createOrgForm.value = { name: '', description: '' }
}
const handleCreateOrganization = async () => {
const name = createOrgForm.value.name.trim()
const description = createOrgForm.value.description.trim()
if (!name) {
message.error('Organization name is required')
return
}
creatingOrganization.value = true
try {
const response = await apiClient.post<Organization>(API.organizations(), {
name,
description,
})
message.success('Organization created successfully')
showCreateOrgModal.value = false
resetCreateOrganizationForm()
await fetchOrganizations()
router.push(`/organization/${response.data.uuid}`)
} catch (error) {
console.error('Failed to create organization:', error)
if (isAxiosError(error)) {
message.error(error.response?.data?.error || 'Failed to create organization')
} else {
message.error('Failed to create organization')
}
} finally {
creatingOrganization.value = false
}
}
</script>
<template>
<div class="page">
<Spin :spinning="loading" tip="Loading organizations...">
<Card class="panel" :bordered="false">
<div class="header">
<Typography.Title :level="2">Organizations</Typography.Title>
<div class="header-actions">
<Button type="primary" @click="fetchOrganizations">Refresh</Button>
<Button v-if="auth.isGeneralManager" @click="showCreateOrgModal = true">
Create Organization
</Button>
</div>
</div>
<div v-if="organizations.length > 0">
<List :data-source="organizations" :bordered="false">
<template #renderItem="{ item }">
<List.Item>
<List.Item.Meta
:title="item.name"
:description="item.description || 'No description provided'"
/>
<div>
<Button size="small" type="primary" @click="openOrg(item)">
Open
</Button>
</div>
</List.Item>
</template>
</List>
</div>
<Typography.Paragraph v-else type="secondary">
No organizations found.
</Typography.Paragraph>
</Card>
</Spin>
<Modal
v-model:open="showCreateOrgModal"
title="Create Organization"
ok-text="Create"
cancel-text="Cancel"
:ok-button-props="{ loading: creatingOrganization }"
@ok="handleCreateOrganization"
@cancel="resetCreateOrganizationForm"
>
<Form layout="vertical">
<Form.Item label="Organization Name" required>
<Input
v-model:value="createOrgForm.name"
placeholder="Enter organization name"
:maxlength="120"
/>
</Form.Item>
<Form.Item label="Description">
<Input.TextArea
v-model:value="createOrgForm.description"
placeholder="Enter organization description"
:rows="4"
:maxlength="1000"
/>
</Form.Item>
</Form>
</Modal>
</div>
</template>
<style scoped>
.page {
padding: 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.header-actions {
display: flex;
gap: 0.5rem;
}
</style>