146 lines
5.6 KiB
Vue
146 lines
5.6 KiB
Vue
<script setup>
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
import { useChapters } from '../composables/useChapters.js';
|
|
import { useAuth } from '../composables/Core/useAuth.js';
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
|
|
usePageTitle('Chapter Org Chart');
|
|
|
|
const { fetchOrgChart, loading } = useChapters();
|
|
const { isCoopMember } = useAuth();
|
|
|
|
const ownChapter = ref(null);
|
|
const children = ref([]);
|
|
const showOfficers = reactive({});
|
|
const loadingChild = reactive({});
|
|
|
|
const roleLabel = (role) => {
|
|
const map = {
|
|
PRESIDENT: 'President',
|
|
VICE_PRESIDENT: 'Vice President',
|
|
SECRETARY: 'Secretary',
|
|
TREASURER: 'Treasurer',
|
|
AUDITOR: 'Auditor',
|
|
BOARD_MEMBER: 'Board Member',
|
|
};
|
|
return map[role] || role || 'Member';
|
|
};
|
|
|
|
const levelLabel = (level) => (level || '').toUpperCase();
|
|
|
|
const toggleChild = async (child) => {
|
|
showOfficers[child.id] = !showOfficers[child.id];
|
|
if (showOfficers[child.id] && (!child.officers || child.officers.length === 0) && !child._loaded) {
|
|
loadingChild[child.id] = true;
|
|
try {
|
|
const res = await fetchOrgChart({ chapterId: child.id });
|
|
child.officers = res?.officers ?? [];
|
|
child._loaded = true;
|
|
} finally {
|
|
loadingChild[child.id] = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
const res = await fetchOrgChart({});
|
|
ownChapter.value = res?.own_chapter ?? null;
|
|
children.value = (res?.children ?? []).map((c) => ({ ...c, officers: c.officers ?? [], _loaded: false }));
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container py-4" style="max-width: 720px;">
|
|
<div v-if="loading && !ownChapter" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
</div>
|
|
|
|
<template v-else-if="ownChapter">
|
|
<!-- Header card -->
|
|
<div class="header-card rounded-4 p-4 mb-3">
|
|
<span class="badge rounded-pill mb-2 level-badge">{{ levelLabel(ownChapter.level) }}</span>
|
|
<h4 class="fw-bold mb-0">{{ ownChapter.name }}</h4>
|
|
</div>
|
|
|
|
<!-- Own chapter officers -->
|
|
<div class="panel rounded-4 p-3 mb-3">
|
|
<h6 class="fw-semibold mb-3"><i class="fas fa-user-tie me-2"></i>Chapter Officers</h6>
|
|
<div v-if="!ownChapter.officers?.length" class="text-muted small">No officers assigned yet.</div>
|
|
<div v-else class="d-flex flex-wrap gap-2">
|
|
<span v-for="(o, i) in ownChapter.officers" :key="i" class="officer-pill rounded-pill px-3 py-2 small">
|
|
<strong>{{ o.name }}</strong>
|
|
<span class="opacity-75"> · {{ roleLabel(o.role) }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Child chapters (officers only, no members) -->
|
|
<div v-if="!isCoopMember" class="panel rounded-4 p-3">
|
|
<h6 class="fw-semibold mb-3"><i class="fas fa-sitemap me-2"></i>Sub-Chapters</h6>
|
|
<div v-if="!children.length" class="text-muted small">No sub-chapters yet.</div>
|
|
|
|
<div v-for="child in children" :key="child.id" class="child-row rounded-3 mb-2 p-3">
|
|
<div class="d-flex align-items-center gap-2" role="button" @click="toggleChild(child)">
|
|
<span class="badge rounded-pill level-badge-sm">{{ levelLabel(child.level) }}</span>
|
|
<span class="fw-semibold flex-grow-1 text-truncate">{{ child.name }}</span>
|
|
<span class="small text-muted">{{ child.member_count }} member{{ child.member_count !== 1 ? 's' : '' }}</span>
|
|
<i class="fas" :class="showOfficers[child.id] ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
|
|
</div>
|
|
|
|
<div v-if="showOfficers[child.id]" class="mt-2 pt-2 border-top">
|
|
<div v-if="loadingChild[child.id]" class="small text-muted">Loading officers...</div>
|
|
<div v-else-if="!child.officers?.length" class="small text-muted">No officers assigned yet.</div>
|
|
<div v-else class="d-flex flex-wrap gap-2">
|
|
<span v-for="(o, i) in child.officers" :key="i" class="officer-pill rounded-pill px-3 py-1 small">
|
|
<strong>{{ o.name }}</strong>
|
|
<span class="opacity-75"> · {{ roleLabel(o.role) }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="text-center py-5 text-muted">
|
|
<i class="fas fa-sitemap fa-2x opacity-25 mb-3"></i>
|
|
<p>You are not assigned to a chapter yet.</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.header-card {
|
|
background: var(--accent-color);
|
|
color: #fff;
|
|
}
|
|
.level-badge {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
color: #fff;
|
|
}
|
|
.level-badge-sm {
|
|
background: var(--accent-color);
|
|
color: #fff;
|
|
font-size: 0.65rem;
|
|
}
|
|
.panel {
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.child-row {
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.officer-pill {
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
}
|
|
:global(.dark-mode) .panel,
|
|
:global(.dark-mode) .child-row,
|
|
:global(.dark-mode) .officer-pill {
|
|
border-color: rgba(255, 255, 255, 0.08);
|
|
}
|
|
</style>
|