1584 lines
43 KiB
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> |