feat: Reorganize platform into member, board, and admin sections
Some checks failed
Build And Push Image / docker (push) Failing after 55s

Major platform reorganization implementing role-based portal sections:

## Infrastructure Changes
- Created role-based middleware for member, board, and admin access
- Updated main dashboard router to redirect based on highest privilege
- Implemented access hierarchy: Admin > Board > Member

## New Layouts
- Member layout: Simplified navigation for regular members
- Board layout: Enhanced tools for board member management
- Admin layout: Full system administration capabilities

## Member Portal (/member/*)
- Dashboard: Profile overview, events, payments, activity tracking
- Events: Browse, register, and manage event participation
- Profile: Complete personal and professional information management
- Resources: Access to documents, guides, FAQs, and quick links

## Board Portal (/board/*)
- Dashboard: Statistics, dues management, board-specific tools
- Members: Comprehensive member management with filtering

## Admin Portal (/admin/*)
- Dashboard: System overview and administrative controls (existing)

## Design Implementation
- Monaco red (#dc2626) as primary accent color
- Modern card-based layouts with consistent spacing
- Responsive design for all screen sizes
- Glass morphism effects for enhanced visual appeal

This reorganization provides clear separation of concerns based on user privileges while maintaining a cohesive user experience across all sections.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-30 22:00:59 +02:00
parent 27e38d98e5
commit d9d8627e97
15 changed files with 6039 additions and 449 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="profile-mockup">
<!-- Profile Header -->
<!-- Admin Header -->
<div
v-motion
:initial="{ opacity: 0, y: -20 }"
@@ -22,51 +22,64 @@
<div v-else class="profile-avatar__placeholder">
{{ initials }}
</div>
<button class="profile-avatar__edit">
<span>📷</span>
</button>
</div>
<div class="profile-status">
<span class="profile-status__indicator"></span>
<span class="profile-status__text">Online</span>
<div class="member-status-badge">
<span
class="status-indicator"
:class="`status-indicator--${profile.memberStatus}`"
></span>
<span class="status-text">{{ profile.memberStatus }}</span>
</div>
</div>
<div class="profile-header__info">
<h1 class="profile-name">{{ profile.name }}</h1>
<p class="profile-title">{{ profile.title }} at {{ profile.company }}</p>
<p class="profile-bio">{{ profile.bio }}</p>
<p class="profile-member-id">Member ID: #{{ profile.memberId }}</p>
<div class="profile-badges">
<span class="badge badge--verified"> Verified Member</span>
<span class="badge badge--board">Board Member</span>
<span class="badge badge--year">Member Since {{ profile.memberSince }}</span>
<span
class="badge"
:class="`badge--${profile.memberStatus}`"
>
{{ profile.memberStatus }} Member
</span>
<span
class="badge"
:class="`badge--${profile.duesStatus}`"
>
Dues: {{ profile.duesStatus }}
</span>
<span class="badge badge--year">Joined {{ profile.memberSince }}</span>
</div>
</div>
<div class="profile-header__actions">
<MonacoButton variant="primary" icon="edit">
Edit Profile
Edit Member
</MonacoButton>
<MonacoButton variant="glass" icon="share">
Share
<MonacoButton variant="glass" icon="mail">
Send Email
</MonacoButton>
<MonacoButton variant="ghost" icon="settings">
Settings
<MonacoButton variant="ghost" icon="ban">
Suspend
</MonacoButton>
<MonacoButton variant="ghost" icon="trash">
Remove
</MonacoButton>
</div>
</div>
</div>
<!-- Stats Overview -->
<!-- Admin Stats Overview -->
<div
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
class="profile-stats"
>
<div class="stat-card" v-for="stat in stats" :key="stat.label">
<div class="stat-card" v-for="stat in adminStats" :key="stat.label">
<span class="stat-card__value">{{ stat.value }}</span>
<span class="stat-card__label">{{ stat.label }}</span>
</div>
@@ -116,63 +129,92 @@
</div>
</GlassCard>
<!-- Skills & Expertise -->
<!-- Account Information -->
<GlassCard
title="Skills & Expertise"
title="Account Information"
variant="glass"
:delay="400"
>
<div class="skills-cloud">
<span
v-for="skill in profile.skills"
:key="skill"
class="skill-tag"
:class="`skill-tag--${getSkillLevel(skill)}`"
>
{{ skill }}
</span>
<div class="account-info">
<div class="account-item">
<span class="account-item__label">Portal Access</span>
<span
class="account-item__value"
:class="{ 'account-item__value--active': profile.hasPortalAccess }"
>
{{ profile.hasPortalAccess ? 'Active' : 'No Access' }}
</span>
</div>
<div class="account-item">
<span class="account-item__label">Last Login</span>
<span class="account-item__value">{{ profile.lastLogin }}</span>
</div>
<div class="account-item">
<span class="account-item__label">Login Count</span>
<span class="account-item__value">{{ profile.loginCount }}</span>
</div>
<div class="account-item">
<span class="account-item__label">Account Created</span>
<span class="account-item__value">{{ profile.accountCreated }}</span>
</div>
</div>
<MonacoButton
v-if="!profile.hasPortalAccess"
variant="primary"
size="sm"
block
>
Send Portal Invitation
</MonacoButton>
<MonacoButton
v-else
variant="ghost"
size="sm"
block
>
Reset Password
</MonacoButton>
</GlassCard>
<!-- Membership Details -->
<!-- Payment History -->
<GlassCard
title="Membership"
title="Payment History"
variant="gradient"
:delay="500"
>
<div class="membership-details">
<div class="membership-item">
<span class="membership-item__label">Status</span>
<span class="membership-item__value membership-item__value--active">
Active
<div class="payment-history">
<div
v-for="payment in paymentHistory"
:key="payment.id"
class="payment-item"
>
<div class="payment-item__date">{{ payment.date }}</div>
<div class="payment-item__info">
<span class="payment-item__type">{{ payment.type }}</span>
<span class="payment-item__amount">${{ payment.amount }}</span>
</div>
<span
class="payment-item__status"
:class="`payment-item__status--${payment.status}`"
>
{{ payment.status }}
</span>
</div>
<div class="membership-item">
<span class="membership-item__label">Type</span>
<span class="membership-item__value">Executive</span>
</div>
<div class="membership-item">
<span class="membership-item__label">Dues</span>
<span class="membership-item__value membership-item__value--paid">
Paid through 2025
</span>
</div>
<div class="membership-item">
<span class="membership-item__label">Next Renewal</span>
<span class="membership-item__value">January 2025</span>
</div>
</div>
<MonacoButton variant="primary" size="sm" block>
Manage Membership
Record Payment
</MonacoButton>
<MonacoButton variant="ghost" size="sm" block>
Send Invoice
</MonacoButton>
</GlassCard>
</div>
<!-- Main Column -->
<div class="profile-grid__main">
<!-- Activity Timeline -->
<!-- Administrative Actions Log -->
<GlassCard
title="Recent Activity"
title="Administrative Actions"
variant="glass"
:delay="350"
>
@@ -203,22 +245,33 @@
</div>
</GlassCard>
<!-- Achievements -->
<!-- Member Notes -->
<GlassCard
title="Achievements"
title="Internal Notes"
variant="glass"
:delay="450"
>
<div class="achievements-grid">
<div class="notes-section">
<div
v-for="achievement in achievements"
:key="achievement.id"
class="achievement-card"
v-for="note in memberNotes"
:key="note.id"
class="note-item"
>
<div class="achievement-card__icon">{{ achievement.icon }}</div>
<h4 class="achievement-card__title">{{ achievement.title }}</h4>
<p class="achievement-card__description">{{ achievement.description }}</p>
<span class="achievement-card__date">{{ achievement.date }}</span>
<div class="note-item__header">
<span class="note-item__author">{{ note.author }}</span>
<span class="note-item__date">{{ note.date }}</span>
</div>
<p class="note-item__content">{{ note.content }}</p>
</div>
<div class="add-note">
<textarea
placeholder="Add a new note..."
class="note-input"
rows="3"
></textarea>
<MonacoButton variant="primary" size="sm">
Add Note
</MonacoButton>
</div>
</div>
</GlassCard>
@@ -253,60 +306,54 @@
<!-- Right Column -->
<div class="profile-grid__aside">
<!-- Quick Stats -->
<!-- Admin Quick Actions -->
<GlassCard
variant="glass"
:delay="400"
>
<h3 class="card-title">Network Stats</h3>
<div class="quick-stats">
<div class="quick-stat">
<span class="quick-stat__icon">👥</span>
<span class="quick-stat__value">234</span>
<span class="quick-stat__label">Connections</span>
</div>
<div class="quick-stat">
<span class="quick-stat__icon">📅</span>
<span class="quick-stat__value">45</span>
<span class="quick-stat__label">Events</span>
</div>
<div class="quick-stat">
<span class="quick-stat__icon">🏆</span>
<span class="quick-stat__value">12</span>
<span class="quick-stat__label">Awards</span>
</div>
<h3 class="card-title">Quick Actions</h3>
<div class="admin-actions">
<MonacoButton variant="glass" size="sm" icon="mail" block>
Send Email
</MonacoButton>
<MonacoButton variant="glass" size="sm" icon="dollar" block>
Record Payment
</MonacoButton>
<MonacoButton variant="glass" size="sm" icon="refresh" block>
Reset Password
</MonacoButton>
<MonacoButton variant="glass" size="sm" icon="user-plus" block>
Add to Group
</MonacoButton>
<MonacoButton variant="primary" size="sm" icon="chart" block>
View Reports
</MonacoButton>
</div>
</GlassCard>
<!-- Connected Members -->
<!-- Member Flags & Warnings -->
<GlassCard
title="Connections"
title="Flags & Warnings"
variant="glass"
:delay="500"
>
<div class="connections-list">
<div class="warnings-list">
<div
v-for="connection in connections"
:key="connection.id"
class="connection-item"
v-for="warning in memberWarnings"
:key="warning.id"
class="warning-item"
:class="`warning-item--${warning.severity}`"
>
<img
v-if="connection.avatar"
:src="connection.avatar"
:alt="connection.name"
class="connection-item__avatar"
/>
<div v-else class="connection-item__avatar-placeholder">
{{ connection.name.split(' ').map(n => n[0]).join('') }}
</div>
<div class="connection-item__info">
<h5 class="connection-item__name">{{ connection.name }}</h5>
<p class="connection-item__title">{{ connection.title }}</p>
<span class="warning-item__icon">{{ warning.icon }}</span>
<div class="warning-item__content">
<h5 class="warning-item__title">{{ warning.title }}</h5>
<p class="warning-item__description">{{ warning.description }}</p>
<span class="warning-item__date">{{ warning.date }}</span>
</div>
</div>
</div>
<MonacoButton variant="primary" size="sm" block>
View All Connections
<MonacoButton variant="ghost" size="sm" block>
Add Flag
</MonacoButton>
</GlassCard>
</div>
@@ -323,82 +370,81 @@ const profile = ref({
name: 'Alexandra Martin',
title: 'CEO & Founder',
company: 'Monaco Ventures',
bio: 'Passionate about fostering Monaco-US business relationships and cultural exchange. Leading innovation in international commerce.',
memberId: 'MCA2021-0234',
avatar: '/api/placeholder/200/200',
email: 'alexandra@monacoventures.com',
phone: '+1 (555) 123-4567',
linkedin: 'linkedin.com/in/alexandra-martin',
location: 'Monaco & New York',
memberSince: '2021',
skills: [
'Leadership', 'Strategy', 'Investment', 'International Business',
'Networking', 'Public Speaking', 'Venture Capital', 'M&A'
]
memberSince: 'January 2021',
memberStatus: 'active',
duesStatus: 'current',
hasPortalAccess: true,
lastLogin: '2 hours ago',
loginCount: 234,
accountCreated: 'Jan 15, 2021'
})
const initials = computed(() => {
return profile.value.name.split(' ').map(n => n[0]).join('').toUpperCase()
})
const stats = ref([
{ label: 'Years Active', value: '3' },
{ label: 'Events Hosted', value: '24' },
{ label: 'Connections', value: '234' },
{ label: 'Contributions', value: '156' }
const adminStats = ref([
{ label: 'Total Paid', value: '$12,500' },
{ label: 'Portal Logins', value: '234' },
{ label: 'Events Attended', value: '18' },
{ label: 'Days Overdue', value: '0' }
])
const activities = ref([
{
type: 'event',
icon: '📅',
title: 'Attended Monaco Winter Gala',
description: 'Networked with 50+ members at the annual gala',
type: 'admin',
icon: '',
title: 'Profile Updated',
description: 'Admin Jane Doe updated contact information',
time: '2 days ago'
},
{
type: 'achievement',
icon: '🏆',
title: 'Earned Top Contributor Badge',
description: 'Recognized for outstanding community engagement',
time: '1 week ago'
type: 'payment',
icon: '💵',
title: 'Annual Dues Paid',
description: 'Payment of $2,500 processed successfully',
time: '1 month ago'
},
{
type: 'connection',
icon: '🤝',
title: 'Connected with 5 new members',
description: 'Expanded network in the Tech sector',
time: '2 weeks ago'
type: 'email',
icon: '',
title: 'Portal Invitation Sent',
description: 'Member accepted invitation and logged in',
time: '2 months ago'
},
{
type: 'post',
icon: '📝',
title: 'Published article on Monaco innovation',
description: 'Shared insights on emerging markets',
time: '3 weeks ago'
type: 'status',
icon: '',
title: 'Status Changed to Active',
description: 'Member activated after payment verification',
time: '3 months ago'
}
])
const achievements = ref([
const memberNotes = ref([
{
id: 1,
icon: '🌟',
title: 'Founding Member',
description: 'One of the first 100 members',
date: 'March 2021'
author: 'Admin Jane Doe',
date: 'Dec 1, 2024',
content: 'VIP member - ensure priority support and invitations to exclusive events.'
},
{
id: 2,
icon: '🎯',
title: 'Event Champion',
description: 'Hosted 10+ successful events',
date: 'June 2023'
author: 'Admin John Smith',
date: 'Nov 15, 2024',
content: 'Requested paper invoices instead of electronic billing. Updated preferences.'
},
{
id: 3,
icon: '💎',
title: 'Diamond Contributor',
description: 'Top 1% activity level',
date: 'December 2023'
author: 'System',
date: 'Oct 10, 2024',
content: 'Automatic renewal scheduled for January 2025.'
}
])
@@ -408,21 +454,32 @@ const eventsAttended = ref([
{ id: 3, day: '20', month: 'OCT', title: 'Tech Innovation Day', role: 'Panelist' }
])
const connections = ref([
{ id: 1, name: 'John Doe', title: 'CFO at TechCorp', avatar: '/api/placeholder/40/40' },
{ id: 2, name: 'Sarah Smith', title: 'Director at Monaco Bank', avatar: '' },
{ id: 3, name: 'Marc Blanc', title: 'Partner at Law Firm', avatar: '/api/placeholder/40/40' },
{ id: 4, name: 'Lisa Chen', title: 'VP Marketing', avatar: '/api/placeholder/40/40' }
const paymentHistory = ref([
{ id: 1, date: 'Dec 2024', type: 'Annual Dues', amount: 2500, status: 'paid' },
{ id: 2, date: 'Nov 2024', type: 'Event Ticket', amount: 150, status: 'paid' },
{ id: 3, date: 'Oct 2024', type: 'Special Assessment', amount: 500, status: 'paid' },
{ id: 4, date: 'Jan 2024', type: 'Annual Dues', amount: 2500, status: 'paid' }
])
const memberWarnings = ref([
{
id: 1,
severity: 'info',
icon: '',
title: 'VIP Member',
description: 'Priority support required',
date: 'Active'
},
{
id: 2,
severity: 'warning',
icon: '⚠',
title: 'Paper Invoices',
description: 'Requires printed billing',
date: 'Nov 2024'
}
])
const getSkillLevel = (skill: string) => {
const expertSkills = ['Leadership', 'Strategy', 'Investment']
const advancedSkills = ['International Business', 'Networking']
if (expertSkills.includes(skill)) return 'expert'
if (advancedSkills.includes(skill)) return 'advanced'
return 'intermediate'
}
</script>
<style scoped lang="scss">
@@ -540,7 +597,7 @@ const getSkillLevel = (skill: string) => {
}
}
.profile-status {
.member-status-badge {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -548,20 +605,36 @@ const getSkillLevel = (skill: string) => {
background: white;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-indicator {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
&__indicator {
width: 0.5rem;
height: 0.5rem;
&--active {
background: #10b981;
border-radius: 50%;
animation: pulse 2s infinite;
}
&__text {
font-size: 0.875rem;
font-weight: 500;
color: #10b981;
&--inactive {
background: #6b7280;
}
&--suspended {
background: #ef4444;
}
&--pending {
background: #fb923c;
}
}
.status-text {
font-size: 0.875rem;
font-weight: 500;
text-transform: capitalize;
color: #27272a;
}
.profile-name {
@@ -578,11 +651,11 @@ const getSkillLevel = (skill: string) => {
color: rgba(255, 255, 255, 0.9);
}
.profile-bio {
.profile-member-id {
margin: 0 0 1.5rem;
font-size: 1rem;
color: #27272a;
max-width: 600px;
color: #6b7280;
font-family: monospace;
}
.profile-badges {
@@ -597,14 +670,39 @@ const getSkillLevel = (skill: string) => {
font-size: 0.875rem;
font-weight: 600;
&--verified {
&--active {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
&--board {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
&--inactive {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
&--suspended {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
&--pending {
background: rgba(251, 146, 60, 0.1);
color: #fb923c;
}
&--current {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
&--overdue {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
&--exempt {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
&--year {
@@ -710,38 +808,99 @@ const getSkillLevel = (skill: string) => {
}
}
.skills-cloud {
.account-info {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
}
.skill-tag {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
cursor: pointer;
.account-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
&--expert {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
}
&--advanced {
background: rgba(220, 38, 38, 0.15);
color: #dc2626;
}
&--intermediate {
background: rgba(107, 114, 128, 0.1);
&__label {
font-size: 0.875rem;
color: #6b7280;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&__value {
font-size: 0.875rem;
font-weight: 500;
color: #27272a;
&--active {
color: #10b981;
font-weight: 600;
}
}
}
.payment-history {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
.payment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
&__date {
font-size: 0.75rem;
color: #6b7280;
min-width: 60px;
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.125rem;
margin: 0 1rem;
}
&__type {
font-size: 0.875rem;
font-weight: 500;
color: #27272a;
}
&__amount {
font-size: 0.75rem;
color: #6b7280;
}
&__status {
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: capitalize;
&--paid {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
&--pending {
background: rgba(251, 146, 60, 0.1);
color: #fb923c;
}
&--overdue {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
}
}
@@ -870,51 +1029,63 @@ const getSkillLevel = (skill: string) => {
}
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
.notes-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.achievement-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.05) 0%,
rgba(220, 38, 38, 0.02) 100%);
border-radius: 12px;
text-align: center;
transition: all 0.2s;
.note-item {
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border-left: 3px solid #dc2626;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
&__icon {
font-size: 2rem;
&__header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
&__title {
margin: 0 0 0.25rem;
&__author {
font-size: 0.875rem;
font-weight: 600;
color: #27272a;
}
&__description {
margin: 0 0 0.5rem;
font-size: 0.75rem;
color: #6b7280;
color: #dc2626;
}
&__date {
font-size: 0.75rem;
color: #dc2626;
font-weight: 500;
color: #6b7280;
}
&__content {
margin: 0;
font-size: 0.875rem;
color: #27272a;
line-height: 1.5;
}
}
.add-note {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.note-input {
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.7);
border: 2px solid rgba(220, 38, 38, 0.1);
border-radius: 8px;
font-size: 0.875rem;
resize: vertical;
outline: none;
transition: all 0.2s;
&:focus {
border-color: #dc2626;
background: white;
}
}
@@ -987,36 +1158,78 @@ const getSkillLevel = (skill: string) => {
color: #dc2626;
}
.quick-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
.admin-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.quick-stat {
.warnings-list {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0.5rem;
background: rgba(255, 255, 255, 0.5);
gap: 0.75rem;
margin-bottom: 1rem;
}
.warning-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
border-left: 3px solid;
&--info {
background: rgba(59, 130, 246, 0.05);
border-color: #3b82f6;
.warning-item__icon {
color: #3b82f6;
}
}
&--warning {
background: rgba(251, 146, 60, 0.05);
border-color: #fb923c;
.warning-item__icon {
color: #fb923c;
}
}
&--error {
background: rgba(239, 68, 68, 0.05);
border-color: #ef4444;
.warning-item__icon {
color: #ef4444;
}
}
&__icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
&__value {
font-size: 1.25rem;
font-weight: 700;
color: #dc2626;
margin-bottom: 0.25rem;
flex-shrink: 0;
}
&__label {
font-size: 0.625rem;
&__content {
flex: 1;
}
&__title {
margin: 0 0 0.25rem;
font-size: 0.875rem;
font-weight: 600;
color: #27272a;
}
&__description {
margin: 0 0 0.25rem;
font-size: 0.75rem;
color: #6b7280;
text-align: center;
}
&__date {
font-size: 0.625rem;
color: #a3a3a3;
}
}