2026-02-26 01:32:04 +00:00
|
|
|
<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 {
|
2026-02-27 12:26:51 +00:00
|
|
|
const resp = await apiClient.get<Organization[]>(API.organization.list())
|
2026-02-26 01:32:04 +00:00
|
|
|
organizations.value = resp.data || []
|
|
|
|
|
|
|
|
|
|
if (organizations.value.length === 1 && !auth.isGeneralManager) {
|
|
|
|
|
const onlyOrg = organizations.value[0]
|
2026-02-27 12:53:19 +00:00
|
|
|
await router.replace(`/organization/${onlyOrg.uuid}`)
|
2026-02-26 01:32:04 +00:00
|
|
|
}
|
|
|
|
|
} 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) => {
|
2026-02-27 12:53:19 +00:00
|
|
|
router.push(`/organization/${org.uuid}`)
|
2026-02-26 01:32:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-02-27 12:26:51 +00:00
|
|
|
const response = await apiClient.post<Organization>(API.organization.list(), {
|
2026-02-26 01:32:04 +00:00
|
|
|
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>
|