You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/sys/org/detail.html

488 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Org Detail">
<title>{{ org ? org.name : $t('org.detail') }}</title>
</head>
<style>
body {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.header-left {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.btn-back {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: var(--bg-color-tertiary);
color: var(--text-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-back:hover {
background: var(--border-color);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
}
.section {
background: var(--bg-color-secondary);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--spacing-md);
color: var(--text-color);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
.info-item {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.info-label {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
}
.info-value {
font-size: var(--font-size-md);
font-weight: 500;
color: var(--text-color);
}
.org-icon-large {
width: 64px;
height: 64px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28px;
font-weight: bold;
}
.org-header-info {
display: flex;
align-items: center;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.org-header-text {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.org-header-name {
font-size: var(--font-size-2xl);
font-weight: 600;
color: var(--text-color);
}
.org-header-desc {
font-size: var(--font-size-md);
color: var(--text-color-secondary);
}
/* Members Table */
.members-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--bg-color-tertiary);
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
}
td {
font-size: var(--font-size-md);
color: var(--text-color);
}
tr:hover td {
background-color: var(--bg-color-tertiary);
}
.role-badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
}
.role-admin {
background-color: var(--color-primary);
color: var(--color-primary-text);
}
.role-member {
background-color: var(--bg-color-tertiary);
color: var(--text-color-secondary);
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
}
.status-active {
background-color: color-mix(in srgb, var(--color-success), transparent 85%);
color: var(--color-success);
}
.status-inactive {
background-color: color-mix(in srgb, var(--text-color-disabled), transparent 85%);
color: var(--text-color-secondary);
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
gap: var(--spacing-md);
color: var(--text-color-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-lg);
gap: var(--spacing-sm);
color: var(--text-color-secondary);
text-align: center;
}
.empty-state i {
font-size: 48px;
color: var(--border-color);
}
</style>
<body>
<!-- Loading State -->
<div class="loading-state" v-if="loading">
<div class="spinner"></div>
<span>{{ $t('common.loading') }}</span>
</div>
<div v-if="!loading && org">
<!-- Page Header -->
<div class="page-header">
<div class="header-left">
<button class="btn-back" @click="goBack" :title="$t('common.back')">
<i class="fas fa-arrow-left"></i>
</button>
<h1>{{ $t('org.detail') }}</h1>
</div>
<div class="header-actions">
<v-btn variant="outline" :click="openEditModal">
<i class="fas fa-edit"></i>
{{ $t('common.edit') }}
</v-btn>
<v-btn color="danger" :click="deleteOrg">
<i class="fas fa-trash"></i>
{{ $t('common.delete') }}
</v-btn>
</div>
</div>
<!-- Org Info Section -->
<div class="section">
<div class="org-header-info">
<div class="org-icon-large">{{ org.name ? org.name.charAt(0).toUpperCase() : 'O' }}</div>
<div class="org-header-text">
<div class="org-header-name">{{ org.name }}</div>
<div class="org-header-desc">{{ org.description || $t('org.no_description') }}</div>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">ID</span>
<span class="info-value">{{ org.id }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('org.code') }}</span>
<span class="info-value">{{ org.code }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('common.status') }}</span>
<span class="info-value">
<span class="status-badge" :class="org.status === 1 ? 'status-active' : 'status-inactive'">
{{ org.status === 1 ? 'Active' : 'Inactive' }}
</span>
</span>
</div>
<div class="info-item">
<span class="info-label">{{ $t('org.created_at') }}</span>
<span class="info-value">{{ formatDate(org.created_at) }}</span>
</div>
<div class="info-item" v-if="org.updated_at">
<span class="info-label">{{ $t('org.updated_at') }}</span>
<span class="info-value">{{ formatDate(org.updated_at) }}</span>
</div>
</div>
</div>
<!-- Members Section -->
<div class="section">
<div class="members-header">
<div class="section-title">
<i class="fas fa-users"></i>
{{ $t('org.members') }}
<span style="font-size: var(--font-size-sm); color: var(--text-color-tertiary);">({{ members.length }})</span>
</div>
</div>
<div class="empty-state" v-if="members.length === 0">
<i class="fas fa-user-slash"></i>
<p>{{ $t('org.no_members') }}</p>
</div>
<table v-if="members.length > 0">
<thead>
<tr>
<th>{{ $t('user.username') }}</th>
<th>{{ $t('user.email') }}</th>
<th>{{ $t('user.role') }}</th>
<th>{{ $t('common.status') }}</th>
<th>{{ $t('org.joined_at') }}</th>
<th>{{ $t('common.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="member in members">
<td>{{ member.username }}</td>
<td>{{ member.email || '-' }}</td>
<td>
<span class="role-badge" :class="member.role_ids === 'admin' ? 'role-admin' : 'role-member'">
{{ member.role_ids || 'member' }}
</span>
</td>
<td>
<span class="status-badge" :class="member.status === 1 ? 'status-active' : 'status-inactive'">
{{ member.status === 1 ? 'Active' : 'Inactive' }}
</span>
</td>
<td>{{ formatDate(member.joined_at) }}</td>
<td>
<v-btn size="sm" color="danger" variant="outline" :click="() => removeMember(member)"
v-if="member.id !== currentUserId">
<i class="fas fa-user-minus"></i>
{{ $t('org.remove_member') }}
</v-btn>
<span v-else style="color: var(--text-color-tertiary); font-size: var(--font-size-sm);">
{{ $t('org.you') }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Edit Dialog -->
<v-dialog v:visible="showEditModal" :title="$t('org.edit')">
<v-input type="text" v:value="editForm.name" :label="$t('org.name')" required
:placeholder="$t('org.name_placeholder')">
</v-input>
<v-input type="textarea" v:value="editForm.description" :label="$t('org.description')"
:placeholder="$t('org.desc_placeholder')">
</v-input>
<div vslot="footer">
<v-btn variant="outline" :click="closeEditModal">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary" :disabled="!editForm.name" :click="saveOrg">
{{ $t('common.save') }}
</v-btn>
</div>
</v-dialog>
</body>
<script setup>
orgId = $router.params.id;
org = null;
members = [];
totalMembers = 0;
loading = false;
currentUserId = $env.$vbase.user?.id;
// Edit modal state
showEditModal = false;
editForm = {
name: "",
description: ""
};
loadData = async () => {
loading = true;
try {
const [orgRes, membersRes] = await Promise.all([
$axios.get(`/api/orgs/${orgId}`),
$axios.get(`/api/orgs/${orgId}/members`)
]);
org = orgRes;
members = membersRes.items || [];
totalMembers = membersRes.total || 0;
} catch (e) {
$message.error(e.message);
if (e.status === 404) {
$router.push('/org');
}
} finally {
loading = false;
}
};
formatDate = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
};
goBack = () => {
$router.back();
};
// Edit modal
openEditModal = () => {
editForm = {
name: org.name,
description: org.description || ""
};
showEditModal = true;
};
closeEditModal = () => {
showEditModal = false;
};
saveOrg = async () => {
if (!editForm.name) return;
try {
await $axios.patch(`/api/orgs/${orgId}`, {
name: editForm.name,
description: editForm.description
});
$message.success($t('org.updated'));
closeEditModal();
loadData();
} catch (e) {
$message.error(e.message);
}
};
deleteOrg = async () => {
try {
await $message.confirm($t('org.delete_confirm'));
await $axios.delete(`/api/orgs/${orgId}`);
$message.success($t('org.deleted'));
$router.push('/org');
} catch (e) {
// Cancelled
}
};
removeMember = async (member) => {
try {
await $message.confirm($t('org.remove_confirm'));
// API might not support this yet, but prepare for it
await $axios.delete(`/api/orgs/${orgId}/members/${member.id}`);
$message.success($t('org.member_removed'));
loadData();
} catch (e) {
// Cancelled or not implemented
if (e.status === 404) {
$message.info($t('org.feature_coming'));
}
}
};
</script>
<script>
$data.loadData();
</script>
</html>