monacousa-portal/pages/admin/dashboard/index.vue

1584 lines
43 KiB
Vue

<template>
<div class="bolt-glass-dashboard">
<!-- Animated Background (Subtle) -->
<div class="bg-gradient-subtle"></div>
<div class="bg-mesh-pattern"></div>
<!-- Floating Orbs (Subtle) -->
<div class="floating-orb orb-red-subtle"></div>
<div class="floating-orb orb-blue-subtle"></div>
<div class="dashboard-wrapper">
<!-- Hero Header with Glass Effect -->
<div class="hero-glass-card animate-slide-in">
<div class="hero-content">
<div class="hero-top">
<div class="avatar-section">
<div class="avatar-wrapper">
<v-avatar size="64" color="red" class="avatar-glow">
<span class="text-h5 font-weight-bold">{{ firstName?.charAt(0) }}A</span>
</v-avatar>
<v-icon class="status-icon">mdi-shield-check</v-icon>
</div>
<div class="user-info">
<h1 class="hero-title">
Welcome back,
<span class="gradient-text">{{ firstName || 'Administrator' }}!</span>
</h1>
<div class="user-meta">
<v-chip size="small" class="glass-chip">
<v-icon start size="14">mdi-shield-account</v-icon>
System Administrator
</v-chip>
<span class="separator"></span>
<span class="meta-text">MonacoUSA Portal</span>
</div>
</div>
</div>
</div>
<div class="hero-actions">
<div class="date-display">
<v-icon size="18">mdi-calendar-today</v-icon>
<div>
<div class="date-label">Today</div>
<div class="date-value">{{ new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) }}</div>
</div>
</div>
<div class="action-buttons">
<button class="btn-glass-secondary" @click="viewAuditLogs">
<v-icon size="20">mdi-file-document-outline</v-icon>
Audit Logs
</button>
<button class="btn-glass-primary" @click="showCreateUserDialog = true">
<v-icon size="20">mdi-account-plus</v-icon>
Create User
</button>
</div>
</div>
</div>
</div>
<!-- Stats Grid with Gradient Cards -->
<div class="stats-grid">
<div class="stat-card-wrapper animate-slide-up" style="animation-delay: 0.1s;">
<div class="stat-card glass-gradient-red">
<div class="stat-header">
<div class="stat-icon-wrapper">
<v-icon size="24" color="white">mdi-account-group</v-icon>
</div>
<div class="stat-trend positive">
<v-icon size="14">mdi-trending-up</v-icon>
+12%
</div>
</div>
<div class="stat-body">
<p class="stat-label">Total Members</p>
<h2 class="stat-value">1,247</h2>
</div>
<div class="stat-footer">
<div class="progress-bar">
<div class="progress-fill" style="width: 75%;"></div>
</div>
</div>
</div>
</div>
<div class="stat-card-wrapper animate-slide-up" style="animation-delay: 0.2s;">
<div class="stat-card glass-gradient-blue">
<div class="stat-header">
<div class="stat-icon-wrapper">
<v-icon size="24" color="white">mdi-monitor-dashboard</v-icon>
</div>
<div class="stat-trend">
<v-icon size="14" color="white">mdi-circle</v-icon>
Live
</div>
</div>
<div class="stat-body">
<p class="stat-label">Active Sessions</p>
<h2 class="stat-value">342</h2>
</div>
<div class="stat-footer">
<div class="progress-bar">
<div class="progress-fill" style="width: 60%;"></div>
</div>
</div>
</div>
</div>
<div class="stat-card-wrapper animate-slide-up" style="animation-delay: 0.3s;">
<div class="stat-card glass-gradient-green">
<div class="stat-header">
<div class="stat-icon-wrapper">
<v-icon size="24" color="white">mdi-currency-usd</v-icon>
</div>
<div class="stat-trend positive">
<v-icon size="14">mdi-trending-up</v-icon>
+8%
</div>
</div>
<div class="stat-body">
<p class="stat-label">Revenue MTD</p>
<h2 class="stat-value">$48.4k</h2>
</div>
<div class="stat-footer">
<div class="progress-bar">
<div class="progress-fill" style="width: 85%;"></div>
</div>
</div>
</div>
</div>
<div class="stat-card-wrapper animate-slide-up" style="animation-delay: 0.4s;">
<div class="stat-card glass-gradient-orange">
<div class="stat-header">
<div class="stat-icon-wrapper">
<v-icon size="24" color="white">mdi-shield-check</v-icon>
</div>
<div class="stat-trend positive">
<v-icon size="14">mdi-check-circle</v-icon>
Healthy
</div>
</div>
<div class="stat-body">
<p class="stat-label">System Health</p>
<h2 class="stat-value">98.5%</h2>
</div>
<div class="stat-footer">
<div class="progress-bar">
<div class="progress-fill" style="width: 98.5%;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Management Sections with Glass Cards -->
<div class="management-grid">
<!-- User Management -->
<div class="management-card glass-card-subtle animate-slide-up" style="animation-delay: 0.5s;">
<div class="card-header">
<div class="card-icon glass-gradient-red">
<v-icon size="24" color="white">mdi-account-group</v-icon>
</div>
<div>
<h3 class="card-title">User Management</h3>
<p class="card-subtitle">Manage accounts, roles, and permissions</p>
</div>
</div>
<div class="card-stats">
<div class="mini-stat">
<span class="mini-value">{{ userCount || 25 }}</span>
<span class="mini-label">Active Users</span>
</div>
<div class="mini-stat">
<span class="mini-value">5</span>
<span class="mini-label">Pending</span>
</div>
</div>
<div class="card-actions">
<button class="btn-glass-subtle" @click="navigateTo('/dashboard/member-list')">
<v-icon size="18">mdi-account-cog</v-icon>
Manage Users
</button>
<button class="btn-glass-subtle" @click="showCreateUserDialog = true">
<v-icon size="18">mdi-account-plus</v-icon>
Add User
</button>
</div>
</div>
<!-- Portal Configuration -->
<div class="management-card glass-card-subtle animate-slide-up" style="animation-delay: 0.6s;">
<div class="card-header">
<div class="card-icon glass-gradient-blue">
<v-icon size="24" color="white">mdi-cog</v-icon>
</div>
<div>
<h3 class="card-title">Portal Configuration</h3>
<p class="card-subtitle">System settings and integrations</p>
</div>
</div>
<div class="card-stats">
<div class="config-chips">
<v-chip size="x-small" class="glass-chip-colored" color="success">
<v-icon start size="12">mdi-database</v-icon>
NocoDB
</v-chip>
<v-chip size="x-small" class="glass-chip-colored" color="info">
<v-icon start size="12">mdi-email</v-icon>
Email
</v-chip>
<v-chip size="x-small" class="glass-chip-colored" color="warning">
<v-icon start size="12">mdi-shield</v-icon>
reCAPTCHA
</v-chip>
</div>
</div>
<div class="card-actions">
<button class="btn-glass-subtle" @click="showAdminConfig = true">
<v-icon size="18">mdi-cog</v-icon>
Portal Settings
</button>
<button class="btn-glass-subtle" @click="showNocoDBSettings = true">
<v-icon size="18">mdi-database</v-icon>
Database
</button>
</div>
</div>
<!-- Data Management -->
<div class="management-card glass-card-subtle animate-slide-up" style="animation-delay: 0.7s;">
<div class="card-header">
<div class="card-icon glass-gradient-green">
<v-icon size="24" color="white">mdi-database-cog</v-icon>
</div>
<div>
<h3 class="card-title">Data Management</h3>
<p class="card-subtitle">Maintain data integrity and operations</p>
</div>
</div>
<div class="card-stats">
<div class="mini-stat">
<span class="mini-value">1,247</span>
<span class="mini-label">Records</span>
</div>
<div class="mini-stat">
<span class="mini-value">100%</span>
<span class="mini-label">Integrity</span>
</div>
</div>
<div class="card-actions">
<button class="btn-glass-subtle" @click="assignMemberIds" :disabled="assigningMemberIds">
<v-icon size="18">mdi-account-multiple-plus</v-icon>
Assign IDs
</button>
<button class="btn-glass-subtle" @click="backfillEventIds" :disabled="backfillLoading">
<v-icon size="18">mdi-calendar-sync</v-icon>
Backfill Events
</button>
</div>
</div>
</div>
<!-- Dues Management Section -->
<div class="dues-section animate-slide-up" style="animation-delay: 0.8s;">
<div class="section-header">
<h2 class="section-title">Dues Management</h2>
<button class="btn-glass-primary" @click="navigateToMembers">
View All Members
<v-icon size="18">mdi-arrow-right</v-icon>
</button>
</div>
<div class="dues-wrapper glass-card-subtle">
<BoardDuesManagement
:refresh-trigger="duesRefreshTrigger"
@view-member="handleViewMember"
@view-all-members="navigateToMembers"
@member-updated="handleMemberUpdated"
/>
</div>
</div>
<!-- Recent Activity -->
<div class="activity-section animate-slide-up" style="animation-delay: 0.9s;">
<div class="section-header">
<h2 class="section-title">Recent Activity</h2>
</div>
<div class="activity-wrapper glass-card-subtle">
<div class="activity-list">
<div v-for="activity in recentActivity" :key="activity.id" class="activity-item">
<div class="activity-icon" :class="`icon-${activity.color}`">
<v-icon size="20" color="white">{{ activity.icon }}</v-icon>
</div>
<div class="activity-content">
<p class="activity-title">{{ activity.title }}</p>
<p class="activity-description">{{ activity.description }}</p>
<span class="activity-time">{{ activity.time }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dialogs remain the same -->
<NocoDBSettingsDialog
v-model="showNocoDBSettings"
@settings-saved="handleSettingsSaved"
/>
<AdminConfigurationDialog
v-model="showAdminConfig"
@settings-saved="handleAdminConfigSaved"
/>
<v-dialog v-model="showRecaptchaConfig" max-width="600">
<v-card class="dialog-glass">
<v-card-title class="text-h5">
<v-icon left>mdi-shield-account</v-icon>
reCAPTCHA Configuration
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Security Configuration</v-alert-title>
Configure Google reCAPTCHA settings for form protection on the registration page.
</v-alert>
<v-form ref="recaptchaForm" v-model="recaptchaValid">
<v-text-field
v-model="recaptchaConfig.siteKey"
label="Site Key"
placeholder="6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy"
:rules="[v => !!v || 'Site key is required']"
variant="outlined"
required
/>
<v-text-field
v-model="recaptchaConfig.secretKey"
label="Secret Key"
placeholder="6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx"
:rules="[v => !!v || 'Secret key is required']"
variant="outlined"
type="password"
required
/>
<v-alert type="warning" variant="tonal" class="mt-4">
<v-alert-title>Important</v-alert-title>
Keep your secret key confidential. You can get these keys from the Google reCAPTCHA admin console.
</v-alert>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showRecaptchaConfig = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="savingRecaptcha"
:disabled="!recaptchaValid"
@click="saveRecaptchaConfig"
>
Save Configuration
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showMembershipConfig" max-width="600">
<v-card class="dialog-glass">
<v-card-title class="text-h5">
<v-icon left>mdi-bank</v-icon>
Membership Configuration
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Payment Configuration</v-alert-title>
Configure membership fees and payment details displayed on the registration page.
</v-alert>
<v-form ref="membershipForm" v-model="membershipValid">
<v-text-field
v-model="membershipConfig.membershipFee"
label="Annual Membership Fee (€)"
type="number"
:rules="[
v => !!v || 'Membership fee is required',
v => v > 0 || 'Fee must be greater than 0'
]"
variant="outlined"
required
/>
<v-text-field
v-model="membershipConfig.iban"
label="IBAN"
placeholder="DE89 3704 0044 0532 0130 00"
:rules="[v => !!v || 'IBAN is required']"
variant="outlined"
required
/>
<v-text-field
v-model="membershipConfig.accountHolder"
label="Account Holder Name"
placeholder="MonacoUSA Association"
:rules="[v => !!v || 'Account holder is required']"
variant="outlined"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showMembershipConfig = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="savingMembership"
:disabled="!membershipValid"
@click="saveMembershipConfig"
>
Save Configuration
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<ViewMemberDialog
v-model="showViewDialog"
:member="selectedMember"
@edit="handleEditMember"
/>
<EditMemberDialog
v-model="showEditDialog"
:member="selectedMember"
@member-updated="handleMemberUpdated"
/>
<v-dialog v-model="showCreateUserDialog" max-width="600">
<v-card class="dialog-glass">
<v-card-title class="text-h5">
<v-icon left>mdi-account-plus</v-icon>
Create User Account
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Create Portal Account</v-alert-title>
This will create a new user account in the MonacoUSA Portal with email verification.
</v-alert>
<v-form ref="createUserForm" v-model="createUserValid">
<v-row>
<v-col cols="6">
<v-text-field
v-model="newUser.firstName"
label="First Name"
:rules="[v => !!v || 'First name is required']"
variant="outlined"
required
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="newUser.lastName"
label="Last Name"
:rules="[v => !!v || 'Last name is required']"
variant="outlined"
required
/>
</v-col>
</v-row>
<v-text-field
v-model="newUser.email"
label="Email Address"
type="email"
:rules="[
v => !!v || 'Email is required',
v => /.+@.+\..+/.test(v) || 'Email must be valid'
]"
variant="outlined"
required
/>
<v-select
v-model="newUser.role"
label="User Role"
:items="roleOptions"
item-title="title"
item-value="value"
variant="outlined"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showCreateUserDialog = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="creatingUser"
:disabled="!createUserValid"
@click="createUserAccount"
>
Create Account
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
// middleware: 'admin' // Temporarily disabled to view the dashboard
});
const { firstName } = useAuth();
// Reactive data
const userCount = ref(0);
const loading = ref(false);
const showCreateUserDialog = ref(false);
const showAdminConfig = ref(false);
const showRecaptchaConfig = ref(false);
const showMembershipConfig = ref(false);
const showEmailConfig = ref(false);
// Dues management
const overdueCount = ref(0);
const overdueRefreshTrigger = ref(0);
const duesRefreshTrigger = ref(0);
// Data management
const assigningMemberIds = ref(false);
const backfillLoading = ref(false);
// Member dialog state
const showViewDialog = ref(false);
const showEditDialog = ref(false);
const selectedMember = ref(null);
// Create user dialog data
const createUserValid = ref(false);
const creatingUser = ref(false);
const newUser = ref({
firstName: '',
lastName: '',
email: '',
role: 'user'
});
const roleOptions = [
{ title: 'User', value: 'user' },
{ title: 'Board Member', value: 'board' },
{ title: 'Administrator', value: 'admin' }
];
// reCAPTCHA configuration data
const recaptchaValid = ref(false);
const savingRecaptcha = ref(false);
const recaptchaConfig = ref({
siteKey: '',
secretKey: ''
});
// Membership configuration data
const membershipValid = ref(false);
const savingMembership = ref(false);
const membershipConfig = ref({
membershipFee: 50,
iban: '',
accountHolder: ''
});
const recentActivity = ref([
{
id: 1,
title: 'User Account Created',
description: 'New user account created for john.doe@monacousa.org',
time: '2 hours ago',
icon: 'mdi-account-plus',
color: 'success'
},
{
id: 2,
title: 'Role Updated',
description: 'User role updated from User to Board Member',
time: '4 hours ago',
icon: 'mdi-shield-account',
color: 'warning'
},
{
id: 3,
title: 'System Backup',
description: 'Automated system backup completed successfully',
time: '1 day ago',
icon: 'mdi-backup-restore',
color: 'info'
},
{
id: 4,
title: 'Password Reset',
description: 'Password reset requested for jane.smith@monacousa.org',
time: '2 days ago',
icon: 'mdi-key-change',
color: 'primary'
}
]);
// Load simplified admin stats (without system metrics)
const loadStats = async () => {
try {
loading.value = true;
// Simple user count without complex system metrics
const response = await $fetch<{ userCount: number }>('/api/admin/stats');
userCount.value = response.userCount || 0;
console.log('✅ Admin stats loaded:', { userCount: userCount.value });
} catch (error) {
console.error('❌ Failed to load admin stats:', error);
// Use fallback data
userCount.value = 25;
} finally {
loading.value = false;
}
};
// Action methods (placeholders for now)
const manageUsers = () => {
window.open('https://auth.monacousa.org', '_blank');
};
const viewAuditLogs = () => {
console.log('Navigate to audit logs');
// TODO: Implement audit logs navigation
};
const showNocoDBSettings = ref(false);
const portalSettings = () => {
showNocoDBSettings.value = true;
};
const handleSettingsSaved = () => {
console.log('NocoDB settings saved successfully');
};
const handleAdminConfigSaved = () => {
console.log('Admin configuration saved successfully');
showAdminConfig.value = false;
};
// Handle opening email configuration directly
const openEmailConfig = () => {
// Set the activeTab to email when opening the admin config dialog
showEmailConfig.value = true;
showAdminConfig.value = true;
};
// Watch for showEmailConfig to set the initial tab
watch(showEmailConfig, (newValue) => {
if (newValue) {
// This will be handled by the AdminConfigurationDialog to set initial tab
showEmailConfig.value = false; // Reset the flag
}
});
const saveRecaptchaConfig = async () => {
if (!recaptchaValid.value) return;
savingRecaptcha.value = true;
try {
const response = await $fetch('/api/admin/recaptcha-config', {
method: 'POST',
body: {
siteKey: recaptchaConfig.value.siteKey,
secretKey: recaptchaConfig.value.secretKey
}
}) as any;
if (response?.success) {
showRecaptchaConfig.value = false;
console.log('reCAPTCHA configuration saved successfully');
// TODO: Show success notification
}
} catch (error) {
console.error('Failed to save reCAPTCHA configuration:', error);
// TODO: Show error notification
} finally {
savingRecaptcha.value = false;
}
};
const saveMembershipConfig = async () => {
if (!membershipValid.value) return;
savingMembership.value = true;
try {
const response = await $fetch('/api/admin/registration-config', {
method: 'POST',
body: {
membershipFee: membershipConfig.value.membershipFee,
iban: membershipConfig.value.iban,
accountHolder: membershipConfig.value.accountHolder
}
}) as any;
if (response?.success) {
showMembershipConfig.value = false;
console.log('Membership configuration saved successfully');
// TODO: Show success notification
}
} catch (error) {
console.error('Failed to save membership configuration:', error);
// TODO: Show error notification
} finally {
savingMembership.value = false;
}
};
const createUserAccount = async () => {
if (!createUserValid.value) return;
creatingUser.value = true;
try {
console.log('Creating user account:', newUser.value);
// TODO: Implement actual user creation using enhanced Keycloak API
// For now, just show success
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
// Reset form
newUser.value = {
firstName: '',
lastName: '',
email: '',
role: 'user'
};
showCreateUserDialog.value = false;
console.log('User account created successfully');
// TODO: Show success notification
// TODO: Refresh user list
} catch (error) {
console.error('Failed to create user account:', error);
// TODO: Show error notification
} finally {
creatingUser.value = false;
}
};
const createUser = () => {
console.log('Create new user');
// TODO: Implement create user dialog/form
};
const generateReport = () => {
console.log('Generate user report');
// TODO: Implement report generation
};
const manageRoles = () => {
console.log('Manage user roles');
// TODO: Implement role management
};
const systemMaintenance = () => {
console.log('System maintenance');
// TODO: Implement maintenance mode
};
// Dues management handlers
const loadOverdueCount = async () => {
try {
const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count');
if (response.success) {
overdueCount.value = response.data.count;
}
} catch (error: any) {
console.error('Error loading overdue count:', error);
}
};
const viewOverdueMembers = () => {
// Navigate to member list with overdue filter applied
navigateTo('/dashboard/member-list');
};
const sendDuesReminders = () => {
// Placeholder for dues reminder functionality
console.log('Send dues reminders - feature to be implemented');
};
const handleStatusesUpdated = async (updatedCount: number) => {
console.log(`Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`);
// Refresh overdue count
await loadOverdueCount();
// Trigger banner refresh
overdueRefreshTrigger.value += 1;
};
const handleViewMember = (member: any) => {
// Open the view dialog instead of navigating away
selectedMember.value = member;
showViewDialog.value = true;
};
const handleEditMember = (member: any) => {
// Close the view dialog and open the edit dialog
showViewDialog.value = false;
selectedMember.value = member;
showEditDialog.value = true;
};
const navigateToMembers = () => {
// Navigate to member list page
navigateTo('/dashboard/member-list');
};
const handleMemberUpdated = (member: any) => {
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
// Close edit dialog
showEditDialog.value = false;
// Trigger dues refresh
duesRefreshTrigger.value += 1;
};
// Data management functions
const assignMemberIds = async () => {
assigningMemberIds.value = true;
try {
console.log('Starting member ID assignment...');
const response = await $fetch<{
success: boolean;
message: string;
data: {
totalMembers: number;
membersUpdated: number;
updatedMembers: Array<{
id: string;
name: string;
email: string;
memberId: string;
}>;
startingId: string | null;
endingId: string | null;
};
}>('/api/admin/assign-member-ids', {
method: 'POST'
});
if (response.success) {
console.log('✅ Member ID assignment completed:', {
totalMembers: response.data.totalMembers,
membersUpdated: response.data.membersUpdated,
startingId: response.data.startingId,
endingId: response.data.endingId
});
// Show success message
alert(`Success! Assigned member IDs to ${response.data.membersUpdated} members.\nRange: ${response.data.startingId} to ${response.data.endingId}`);
// Refresh dues management data
duesRefreshTrigger.value += 1;
}
} catch (error: any) {
console.error('❌ Failed to assign member IDs:', error);
alert(`Error: ${error.statusMessage || error.message || 'Failed to assign member IDs'}`);
} finally {
assigningMemberIds.value = false;
}
};
const backfillEventIds = async () => {
backfillLoading.value = true;
try {
console.log('Starting event ID backfill...');
const response = await $fetch<{
success: boolean;
message: string;
data: {
totalEvents: number;
eventsUpdated: number;
};
}>('/api/admin/backfill-event-ids', {
method: 'POST'
});
if (response.success) {
console.log('✅ Event ID backfill completed:', {
totalEvents: response.data.totalEvents,
eventsUpdated: response.data.eventsUpdated
});
// Show success message
alert(`Success! Assigned event IDs to ${response.data.eventsUpdated} events.`);
}
} catch (error: any) {
console.error('❌ Failed to backfill event IDs:', error);
alert(`Error: ${error.statusMessage || error.message || 'Failed to backfill event IDs'}`);
} finally {
backfillLoading.value = false;
}
};
// Load stats and overdue count on component mount
onMounted(async () => {
await loadStats();
await loadOverdueCount();
});
</script>
<style scoped lang="scss">
@import '~/assets/scss/main.scss';
@import '~/assets/scss/glass-bolt-style.scss';
.bolt-glass-dashboard {
min-height: 100vh;
position: relative;
overflow-x: hidden;
background: #fafafa;
}
/* Background Elements */
.bg-gradient-subtle {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(239, 68, 68, 0.03) 0%,
rgba(59, 130, 246, 0.03) 50%,
rgba(34, 197, 94, 0.03) 100%);
z-index: 0;
}
.bg-mesh-pattern {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.02;
background-image:
linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px);
background-size: 50px 50px;
z-index: 1;
}
.floating-orb {
position: fixed;
border-radius: 50%;
filter: blur(100px);
animation: float 20s ease-in-out infinite;
z-index: 0;
&.orb-red-subtle {
width: 400px;
height: 400px;
background: rgba(239, 68, 68, 0.1);
top: -100px;
right: -100px;
}
&.orb-blue-subtle {
width: 300px;
height: 300px;
background: rgba(59, 130, 246, 0.1);
bottom: 100px;
left: -50px;
}
}
.dashboard-wrapper {
position: relative;
z-index: 10;
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
/* Hero Glass Card */
.hero-glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 24px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.05),
0 2px 4px -1px rgba(0, 0, 0, 0.03);
.hero-content {
.hero-top {
margin-bottom: 1.5rem;
.avatar-section {
display: flex;
align-items: center;
gap: 1.5rem;
.avatar-wrapper {
position: relative;
.avatar-glow {
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
}
.status-icon {
position: absolute;
bottom: -4px;
right: -4px;
background: #22c55e;
color: white;
border-radius: 50%;
padding: 2px;
font-size: 16px;
}
}
.user-info {
.hero-title {
font-size: 2rem;
font-weight: 700;
color: #18181b;
line-height: 1.2;
margin-bottom: 0.5rem;
.gradient-text {
background: linear-gradient(135deg, #18181b 0%, #52525b 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.user-meta {
display: flex;
align-items: center;
gap: 1rem;
.glass-chip {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.separator {
color: #a1a1aa;
}
.meta-text {
color: #71717a;
}
}
}
}
}
.hero-actions {
display: flex;
justify-content: space-between;
align-items: center;
.date-display {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 12px;
.date-label {
font-size: 0.875rem;
color: #71717a;
}
.date-value {
font-weight: 600;
color: #18181b;
}
}
.action-buttons {
display: flex;
gap: 0.75rem;
}
}
}
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
.stat-card-wrapper {
.stat-card {
padding: 1.5rem;
border-radius: 20px;
color: white;
position: relative;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
}
&.glass-gradient-red {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
}
&.glass-gradient-blue {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
}
&.glass-gradient-green {
background: linear-gradient(135deg, #22c55e 0%, #15803d 100%);
}
&.glass-gradient-orange {
background: linear-gradient(135deg, #f97316 0%, #c2410c 100%);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
.stat-icon-wrapper {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-trend {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
padding: 0.25rem 0.5rem;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 999px;
&.positive {
color: #bbf7d0;
}
}
}
.stat-body {
flex: 1;
.stat-label {
font-size: 0.875rem;
opacity: 0.9;
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
line-height: 1;
}
}
.stat-footer {
margin-top: 1rem;
.progress-bar {
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 999px;
overflow: hidden;
.progress-fill {
height: 100%;
background: rgba(255, 255, 255, 0.6);
border-radius: 999px;
animation: shimmer 2s ease-in-out infinite;
}
}
}
}
}
}
/* Management Grid */
.management-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
.management-card {
padding: 1.5rem;
.card-header {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
&.glass-gradient-red {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
}
&.glass-gradient-blue {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
}
&.glass-gradient-green {
background: linear-gradient(135deg, #22c55e 0%, #15803d 100%);
}
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #18181b;
margin-bottom: 0.25rem;
}
.card-subtitle {
color: #71717a;
font-size: 0.875rem;
}
}
.card-stats {
display: flex;
gap: 2rem;
margin-bottom: 1.5rem;
.mini-stat {
.mini-value {
display: block;
font-size: 1.5rem;
font-weight: 600;
color: #18181b;
}
.mini-label {
display: block;
font-size: 0.75rem;
color: #71717a;
margin-top: 0.25rem;
}
}
.config-chips {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.glass-chip-colored {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
}
}
}
.card-actions {
display: flex;
gap: 0.75rem;
}
}
}
/* Glass Card Subtle */
.glass-card-subtle {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 20px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.05),
0 2px 4px -1px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.08),
0 4px 6px -2px rgba(0, 0, 0, 0.04);
}
}
/* Sections */
.dues-section,
.activity-section {
margin-bottom: 2rem;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
.section-title {
font-size: 1.75rem;
font-weight: 600;
color: #18181b;
}
}
.dues-wrapper,
.activity-wrapper {
padding: 1.5rem;
}
}
/* Activity List */
.activity-list {
.activity-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-radius: 12px;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.7);
transform: translateX(4px);
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
&.icon-success {
background: linear-gradient(135deg, #22c55e 0%, #15803d 100%);
}
&.icon-warning {
background: linear-gradient(135deg, #f97316 0%, #c2410c 100%);
}
&.icon-info {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
}
&.icon-primary {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
}
}
.activity-content {
flex: 1;
.activity-title {
font-weight: 600;
color: #18181b;
margin-bottom: 0.25rem;
}
.activity-description {
color: #71717a;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.activity-time {
color: #a1a1aa;
font-size: 0.75rem;
}
}
}
}
/* Buttons */
.btn-glass-primary {
padding: 0.625rem 1.25rem;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
color: white;
border: none;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px -5px rgba(220, 38, 38, 0.4);
}
}
.btn-glass-secondary {
padding: 0.625rem 1.25rem;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
color: #18181b;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
&:hover {
background: rgba(255, 255, 255, 0.95);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.btn-glass-subtle {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
color: #18181b;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
&:hover {
background: rgba(255, 255, 255, 0.8);
transform: translateY(-1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* Dialog Glass Effect */
.dialog-glass {
background: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(20px) !important;
-webkit-backdrop-filter: blur(20px) !important;
border: 1px solid rgba(0, 0, 0, 0.05) !important;
border-radius: 20px !important;
}
/* Animations */
@keyframes float {
0%, 100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(30px, -30px) scale(1.05);
}
66% {
transform: translate(-20px, 20px) scale(0.95);
}
}
@keyframes shimmer {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
.animate-slide-in {
animation: slide-in 0.6s ease-out;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out both;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 768px) {
.dashboard-wrapper {
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.management-grid {
grid-template-columns: 1fr;
}
.hero-glass-card {
.hero-content {
.hero-actions {
flex-direction: column;
gap: 1rem;
.action-buttons {
width: 100%;
button {
flex: 1;
}
}
}
}
}
}
</style>