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

818 lines
26 KiB
Vue

<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">System Settings</h1>
<p class="text-body-1 text-medium-emphasis">Configure system preferences and options</p>
</v-col>
</v-row>
<!-- Settings Tabs -->
<v-card elevation="2">
<v-tabs v-model="activeTab" color="primary">
<v-tab value="general">
<v-icon start>mdi-cog</v-icon>
General
</v-tab>
<v-tab value="email">
<v-icon start>mdi-email</v-icon>
Email
</v-tab>
<v-tab value="nocodb">
<v-icon start>mdi-database</v-icon>
NocoDB
</v-tab>
</v-tabs>
<v-window v-model="activeTab">
<!-- General Settings -->
<v-window-item value="general">
<v-card-text>
<!-- Edit Mode Toggle -->
<v-row class="mb-4">
<v-col>
<v-alert
v-if="!generalEditMode"
type="info"
variant="tonal"
density="compact"
>
<template v-slot:text>
Click "Edit Settings" to modify these values
</template>
</v-alert>
</v-col>
<v-col cols="auto">
<v-btn
v-if="!generalEditMode"
color="primary"
variant="outlined"
@click="generalEditMode = true"
>
<v-icon start>mdi-pencil</v-icon>
Edit Settings
</v-btn>
<v-btn-group v-else>
<v-btn
color="success"
variant="flat"
@click="saveGeneralSettings"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
<v-btn
color="error"
variant="outlined"
@click="cancelGeneralEdit"
>
<v-icon start>mdi-close</v-icon>
Cancel
</v-btn>
</v-btn-group>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">Organization Information</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.general.orgName"
label="Organization Name"
variant="outlined"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
autocomplete="off"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.general.orgEmail"
label="Contact Email"
variant="outlined"
type="email"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
autocomplete="off"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="settings.general.orgDescription"
label="Description"
variant="outlined"
rows="3"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
autocomplete="off"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Regional Settings</h3>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.timezone"
label="Timezone"
:items="timezones"
variant="outlined"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.dateFormat"
label="Date Format"
:items="dateFormats"
variant="outlined"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.currency"
label="Currency"
:items="currencies"
variant="outlined"
:readonly="!generalEditMode"
:disabled="!generalEditMode"
:class="{ 'readonly-field': !generalEditMode }"
/>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- Email Settings -->
<v-window-item value="email">
<v-card-text>
<!-- Edit Mode Toggle -->
<v-row class="mb-4">
<v-col>
<v-alert
v-if="!emailEditMode"
type="info"
variant="tonal"
density="compact"
>
<template v-slot:text>
Click "Edit Email Configuration" to modify SMTP settings
</template>
</v-alert>
<v-alert
v-if="emailEditMode"
type="warning"
variant="tonal"
density="compact"
>
<template v-slot:text>
Be careful when editing email settings. Incorrect values may prevent emails from being sent.
</template>
</v-alert>
</v-col>
<v-col cols="auto">
<v-btn
v-if="!emailEditMode"
color="primary"
variant="outlined"
@click="emailEditMode = true"
>
<v-icon start>mdi-pencil</v-icon>
Edit Email Configuration
</v-btn>
<v-btn-group v-else>
<v-btn
color="success"
variant="flat"
@click="saveEmailSettings"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
<v-btn
color="warning"
variant="outlined"
@click="testEmailSettings"
:loading="testingEmail"
>
<v-icon start>mdi-email-check</v-icon>
Test
</v-btn>
<v-btn
color="error"
variant="outlined"
@click="cancelEmailEdit"
>
<v-icon start>mdi-close</v-icon>
Cancel
</v-btn>
</v-btn-group>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">SMTP Configuration</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpHost"
label="SMTP Host"
variant="outlined"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="new-password"
:type="emailEditMode ? 'text' : 'password'"
:class="{ 'readonly-field': !emailEditMode }"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpPort"
label="SMTP Port"
variant="outlined"
type="number"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="off"
:class="{ 'readonly-field': !emailEditMode }"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpUsername"
label="SMTP Username"
variant="outlined"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="new-password"
:type="emailEditMode ? 'text' : 'password'"
:class="{ 'readonly-field': !emailEditMode }"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpPassword"
label="SMTP Password"
variant="outlined"
:type="showPassword ? 'text' : 'password'"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="new-password"
:class="{ 'readonly-field': !emailEditMode }"
>
<template v-slot:append-inner>
<v-icon
v-if="emailEditMode"
@click="showPassword = !showPassword"
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
class="cursor-pointer"
/>
</template>
</v-text-field>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.email.useTLS"
label="Use TLS/SSL"
color="primary"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Email Templates</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.fromName"
label="From Name"
variant="outlined"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="off"
:class="{ 'readonly-field': !emailEditMode }"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.fromEmail"
label="From Email"
variant="outlined"
type="email"
:readonly="!emailEditMode"
:disabled="!emailEditMode"
autocomplete="off"
:class="{ 'readonly-field': !emailEditMode }"
/>
</v-col>
<v-col cols="12">
<v-btn variant="outlined" color="primary">
<v-icon start>mdi-email-edit</v-icon>
Manage Email Templates
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- NocoDB Settings -->
<v-window-item value="nocodb">
<v-card-text>
<!-- Edit Mode Toggle -->
<v-row class="mb-4">
<v-col>
<v-alert
v-if="!nocodbEditMode"
type="info"
variant="tonal"
density="compact"
>
<template v-slot:text>
Click "Edit NocoDB Configuration" to modify database settings
</template>
</v-alert>
<v-alert
v-if="nocodbEditMode"
type="warning"
variant="tonal"
density="compact"
>
<template v-slot:text>
NocoDB configuration is required for member management functionality
</template>
</v-alert>
</v-col>
<v-col cols="auto">
<v-btn
v-if="!nocodbEditMode"
color="primary"
variant="outlined"
@click="nocodbEditMode = true"
>
<v-icon start>mdi-pencil</v-icon>
Edit NocoDB Configuration
</v-btn>
<v-btn-group v-else>
<v-btn
color="success"
variant="flat"
@click="saveNocodbSettings"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
<v-btn
color="warning"
variant="outlined"
@click="testNocodbConnection"
:loading="testingNocodb"
>
<v-icon start>mdi-connection</v-icon>
Test Connection
</v-btn>
<v-btn
color="error"
variant="outlined"
@click="cancelNocodbEdit"
>
<v-icon start>mdi-close</v-icon>
Cancel
</v-btn>
</v-btn-group>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">NocoDB Database Configuration</h3>
</v-col>
<v-col cols="12">
<v-text-field
v-model="settings.nocodb.url"
label="NocoDB URL"
variant="outlined"
placeholder="https://your-nocodb-instance.com"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="off"
:class="{ 'readonly-field': !nocodbEditMode }"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="settings.nocodb.apiKey"
label="API Key"
variant="outlined"
:type="showNocodbApiKey ? 'text' : 'password'"
placeholder="Enter your NocoDB API token"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="new-password"
:class="{ 'readonly-field': !nocodbEditMode }"
>
<template v-slot:append-inner>
<v-icon
v-if="nocodbEditMode"
@click="showNocodbApiKey = !showNocodbApiKey"
:icon="showNocodbApiKey ? 'mdi-eye-off' : 'mdi-eye'"
class="cursor-pointer"
/>
</template>
</v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="settings.nocodb.baseId"
label="Base ID"
variant="outlined"
placeholder="Your NocoDB base ID"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="off"
:class="{ 'readonly-field': !nocodbEditMode }"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Table Mappings</h3>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.nocodb.tables.members"
label="Members Table"
variant="outlined"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="off"
:class="{ 'readonly-field': !nocodbEditMode }"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.nocodb.tables.events"
label="Events Table"
variant="outlined"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="off"
:class="{ 'readonly-field': !nocodbEditMode }"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.nocodb.tables.rsvps"
label="RSVPs Table"
variant="outlined"
:readonly="!nocodbEditMode"
:disabled="!nocodbEditMode"
autocomplete="off"
:class="{ 'readonly-field': !nocodbEditMode }"
/>
</v-col>
</v-row>
<!-- Connection Status -->
<v-row v-if="nocodbConnectionStatus" class="mt-4">
<v-col>
<v-alert
:type="nocodbConnectionStatus.success ? 'success' : 'error'"
variant="tonal"
>
{{ nocodbConnectionStatus.message }}
</v-alert>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
</v-window>
</v-card>
<!-- Snackbar for notifications -->
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar = false"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
// State
const activeTab = ref('general');
const generalEditMode = ref(false);
const emailEditMode = ref(false);
const nocodbEditMode = ref(false);
const showPassword = ref(false);
const showNocodbApiKey = ref(false);
const testingEmail = ref(false);
const testingNocodb = ref(false);
const nocodbConnectionStatus = ref<{ success: boolean; message: string } | null>(null);
const snackbar = ref(false);
const snackbarText = ref('');
const snackbarColor = ref('success');
// Original settings backup for cancel functionality
const originalSettings = ref<any>(null);
// Settings data
const settings = ref({
general: {
orgName: 'MonacoUSA',
orgEmail: 'info@monacousa.org',
orgDescription: 'Monaco USA Association - Connecting Monaco and USA',
timezone: 'America/New_York',
dateFormat: 'MM/DD/YYYY',
currency: 'EUR'
},
email: {
smtpHost: 'smtp.gmail.com',
smtpPort: 587,
smtpUsername: '',
smtpPassword: '',
useTLS: true,
fromName: 'MonacoUSA',
fromEmail: 'noreply@monacousa.org'
},
nocodb: {
url: '',
apiKey: '',
baseId: '',
tables: {
members: 'Members',
events: 'Events',
rsvps: 'RSVPs'
}
}
});
// Options
const timezones = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/Monaco'
];
const dateFormats = [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD'
];
const currencies = [
'EUR',
'USD',
'GBP'
];
// Load settings on mount
onMounted(async () => {
await loadSettings();
await loadNocodbSettings();
});
// Methods
const loadSettings = async () => {
try {
// Load settings from API
// For now, we'll keep the defaults
console.log('Loading settings...');
} catch (error) {
console.error('Error loading settings:', error);
showNotification('Failed to load settings', 'error');
}
};
const saveGeneralSettings = async () => {
try {
console.log('Saving general settings:', settings.value.general);
// TODO: Save to API
generalEditMode.value = false;
showNotification('General settings saved successfully', 'success');
} catch (error) {
console.error('Error saving general settings:', error);
showNotification('Failed to save general settings', 'error');
}
};
const cancelGeneralEdit = () => {
if (originalSettings.value) {
settings.value.general = { ...originalSettings.value.general };
}
generalEditMode.value = false;
};
const saveEmailSettings = async () => {
try {
console.log('Saving email settings:', settings.value.email);
// TODO: Save to API
emailEditMode.value = false;
showPassword.value = false;
showNotification('Email settings saved successfully', 'success');
} catch (error) {
console.error('Error saving email settings:', error);
showNotification('Failed to save email settings', 'error');
}
};
const testEmailSettings = async () => {
testingEmail.value = true;
try {
console.log('Testing email settings...');
// TODO: Test email configuration via API
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
showNotification('Test email sent successfully', 'success');
} catch (error) {
console.error('Error testing email:', error);
showNotification('Failed to send test email', 'error');
} finally {
testingEmail.value = false;
}
};
const cancelEmailEdit = () => {
if (originalSettings.value) {
settings.value.email = { ...originalSettings.value.email };
}
emailEditMode.value = false;
showPassword.value = false;
};
const showNotification = (text: string, color: string = 'success') => {
snackbarText.value = text;
snackbarColor.value = color;
snackbar.value = true;
};
const loadNocodbSettings = async () => {
try {
const response = await $fetch<{ success: boolean; data?: any }>('/api/admin/nocodb-config');
if (response.success && response.data) {
settings.value.nocodb = {
url: response.data.url || '',
apiKey: response.data.apiKey || '',
baseId: response.data.baseId || '',
tables: response.data.tables || {
members: 'Members',
events: 'Events',
rsvps: 'RSVPs'
}
};
}
} catch (error) {
console.error('Error loading NocoDB settings:', error);
showNotification('Failed to load NocoDB settings', 'error');
}
};
const saveNocodbSettings = async () => {
try {
const response = await $fetch('/api/admin/nocodb-config', {
method: 'POST',
body: settings.value.nocodb
});
nocodbEditMode.value = false;
showNocodbApiKey.value = false;
showNotification('NocoDB settings saved successfully', 'success');
// Reload settings to ensure they're persistent
await loadNocodbSettings();
} catch (error) {
console.error('Error saving NocoDB settings:', error);
showNotification('Failed to save NocoDB settings', 'error');
}
};
const testNocodbConnection = async () => {
testingNocodb.value = true;
nocodbConnectionStatus.value = null;
try {
const response = await $fetch<{ success: boolean; message: string }>('/api/admin/nocodb-test', {
method: 'POST',
body: settings.value.nocodb
});
nocodbConnectionStatus.value = {
success: response.success,
message: response.message
};
if (response.success) {
showNotification('NocoDB connection successful', 'success');
} else {
showNotification(response.message, 'error');
}
} catch (error: any) {
nocodbConnectionStatus.value = {
success: false,
message: error.data?.message || 'Failed to connect to NocoDB'
};
showNotification('Connection test failed', 'error');
} finally {
testingNocodb.value = false;
}
};
const cancelNocodbEdit = () => {
if (originalSettings.value) {
settings.value.nocodb = { ...originalSettings.value.nocodb };
}
nocodbEditMode.value = false;
showNocodbApiKey.value = false;
nocodbConnectionStatus.value = null;
};
// Watch for edit mode changes to backup original settings
watch(generalEditMode, (newVal) => {
if (newVal) {
originalSettings.value = {
general: { ...settings.value.general }
};
}
});
watch(emailEditMode, (newVal) => {
if (newVal) {
originalSettings.value = {
email: { ...settings.value.email }
};
}
});
watch(nocodbEditMode, (newVal) => {
if (newVal) {
originalSettings.value = {
nocodb: { ...settings.value.nocodb }
};
}
});
// Prevent browser autofill on mount
onMounted(() => {
// Disable autofill for all inputs initially
const inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.setAttribute('autocomplete', 'off');
input.setAttribute('data-lpignore', 'true'); // LastPass
input.setAttribute('data-form-type', 'other'); // Dashlane
});
});
</script>
<style scoped>
.readonly-field :deep(.v-field) {
background-color: rgba(0, 0, 0, 0.02);
}
.readonly-field :deep(.v-field__input) {
cursor: default !important;
}
.cursor-pointer {
cursor: pointer;
}
/* Prevent browser autofill styling */
:deep(input:-webkit-autofill),
:deep(input:-webkit-autofill:hover),
:deep(input:-webkit-autofill:focus),
:deep(input:-webkit-autofill:active) {
-webkit-box-shadow: 0 0 0 30px white inset !important;
box-shadow: 0 0 0 30px white inset !important;
}
</style>