1212 lines
42 KiB
Vue
1212 lines
42 KiB
Vue
<template>
|
|
<v-dialog
|
|
:model-value="modelValue"
|
|
@update:model-value="$emit('update:model-value', $event)"
|
|
max-width="800"
|
|
persistent
|
|
scrollable
|
|
>
|
|
<v-card>
|
|
<v-card-title class="d-flex align-center pa-6 bg-primary">
|
|
<v-icon class="mr-3 text-white">mdi-cog</v-icon>
|
|
<h2 class="text-h5 text-white font-weight-bold flex-grow-1">
|
|
Admin Configuration
|
|
</h2>
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
color="white"
|
|
@click="closeDialog"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-tabs v-model="activeTab" class="border-b">
|
|
<v-tab value="nocodb">
|
|
<v-icon start>mdi-database</v-icon>
|
|
NocoDB
|
|
</v-tab>
|
|
<v-tab value="recaptcha">
|
|
<v-icon start>mdi-shield-check</v-icon>
|
|
reCAPTCHA
|
|
</v-tab>
|
|
<v-tab value="registration">
|
|
<v-icon start>mdi-account-plus</v-icon>
|
|
Registration
|
|
</v-tab>
|
|
<v-tab value="email">
|
|
<v-icon start>mdi-email</v-icon>
|
|
Email
|
|
</v-tab>
|
|
</v-tabs>
|
|
|
|
<v-card-text class="pa-6" style="min-height: 400px;">
|
|
<v-tabs-window v-model="activeTab">
|
|
<!-- NocoDB Configuration Tab -->
|
|
<v-tabs-window-item value="nocodb">
|
|
<v-alert
|
|
type="info"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<template #title>Database Configuration</template>
|
|
Configure the NocoDB database connection for the Member Management system.
|
|
</v-alert>
|
|
|
|
<v-form ref="nocodbFormRef" v-model="nocodbFormValid">
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.url"
|
|
label="NocoDB URL"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.url]"
|
|
:readonly="!editingFields.nocodbUrl"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="https://database.monacousa.org"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.nocodbUrl ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.nocodbUrl ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('nocodbUrl')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.apiKey"
|
|
label="API Token"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.nocodbApiKey"
|
|
:type="showNocodbApiKey ? 'text' : 'password'"
|
|
:append-inner-icon="showNocodbApiKey ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="showNocodbApiKey = !showNocodbApiKey"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="Enter your NocoDB API token"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.nocodbApiKey ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.nocodbApiKey ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('nocodbApiKey')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.baseId"
|
|
label="Base ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.nocodbBaseId"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="your-base-id"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.nocodbBaseId ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.nocodbBaseId ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('nocodbBaseId')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<h3 class="text-h6 mb-4 text-primary">Table Configuration</h3>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.tables.members"
|
|
label="Members Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.membersTableId"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="members-table-id"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.membersTableId ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.membersTableId ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('membersTableId')"
|
|
/>
|
|
</div>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Members functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.tables.events"
|
|
label="Events Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.eventsTableId"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="events-table-id"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.eventsTableId ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.eventsTableId ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('eventsTableId')"
|
|
/>
|
|
</div>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Events functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="nocodbForm.tables.rsvps"
|
|
label="RSVPs Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.rsvpsTableId"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="rsvps-table-id"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.rsvpsTableId ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.rsvpsTableId ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('rsvpsTableId')"
|
|
/>
|
|
</div>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Event RSVPs functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-btn
|
|
@click="testNocodbConnection"
|
|
:loading="nocodbTestLoading"
|
|
:disabled="!nocodbFormValid || nocodbLoading"
|
|
color="info"
|
|
variant="outlined"
|
|
block
|
|
>
|
|
<v-icon start>mdi-database-check</v-icon>
|
|
Test Connection
|
|
</v-btn>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center h-100">
|
|
<v-chip
|
|
v-if="nocodbConnectionStatus"
|
|
:color="nocodbConnectionStatus.success ? 'success' : 'error'"
|
|
variant="flat"
|
|
size="small"
|
|
>
|
|
<v-icon start size="14">
|
|
{{ nocodbConnectionStatus.success ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
|
</v-icon>
|
|
{{ nocodbConnectionStatus.message }}
|
|
</v-chip>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- reCAPTCHA Configuration Tab -->
|
|
<v-tabs-window-item value="recaptcha">
|
|
<v-alert
|
|
type="warning"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<template #title>reCAPTCHA Integration</template>
|
|
Configure Google reCAPTCHA v2 for spam protection on the registration form.
|
|
Get your keys from <a href="https://www.google.com/recaptcha/admin" target="_blank">Google reCAPTCHA Admin</a>.
|
|
</v-alert>
|
|
|
|
<v-form ref="recaptchaFormRef" v-model="recaptchaFormValid">
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="recaptchaForm.siteKey"
|
|
label="Site Key (Public)"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.recaptchaSiteKey"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="6Lc..."
|
|
hint="This key is visible to users on the frontend"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.recaptchaSiteKey ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.recaptchaSiteKey ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('recaptchaSiteKey')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="recaptchaForm.secretKey"
|
|
label="Secret Key (Private)"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.recaptchaSecretKey"
|
|
:type="showRecaptchaSecret ? 'text' : 'password'"
|
|
:append-inner-icon="showRecaptchaSecret ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="showRecaptchaSecret = !showRecaptchaSecret"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="6Lc..."
|
|
hint="This key is kept secret on the server"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.recaptchaSecretKey ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.recaptchaSecretKey ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('recaptchaSecretKey')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-alert
|
|
type="info"
|
|
variant="outlined"
|
|
class="mt-2"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>Setup Instructions:</strong>
|
|
<ol class="mt-2 ml-4">
|
|
<li>Visit <a href="https://www.google.com/recaptcha/admin" target="_blank">Google reCAPTCHA Admin</a></li>
|
|
<li>Create a new site with reCAPTCHA v2 "I'm not a robot" Checkbox</li>
|
|
<li>Add your domain to the domains list</li>
|
|
<li>Copy the Site Key and Secret Key here</li>
|
|
</ol>
|
|
</div>
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Registration Configuration Tab -->
|
|
<v-tabs-window-item value="registration">
|
|
<v-alert
|
|
type="success"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<template #title>Member Registration Settings</template>
|
|
Configure payment instructions that will be displayed to new members during registration.
|
|
</v-alert>
|
|
|
|
<v-form ref="registrationFormRef" v-model="registrationFormValid">
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model.number="registrationForm.membershipFee"
|
|
label="Annual Membership Fee (EUR)"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.positiveNumber]"
|
|
:readonly="!editingFields.membershipFee"
|
|
autocomplete="off"
|
|
required
|
|
type="number"
|
|
min="1"
|
|
placeholder="50"
|
|
prefix="€"
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.membershipFee ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.membershipFee ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('membershipFee')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<!-- Spacer for alignment -->
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="registrationForm.iban"
|
|
label="Bank IBAN"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.iban]"
|
|
:readonly="!editingFields.iban"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="DE89 3704 0044 0532 0130 00"
|
|
hint="International Bank Account Number for membership dues"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.iban ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.iban ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('iban')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="registrationForm.accountHolder"
|
|
label="Account Holder Name"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.accountHolder"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="MonacoUSA Association"
|
|
hint="Name on the bank account"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.accountHolder ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.accountHolder ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('accountHolder')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-card variant="outlined" class="pa-4 bg-grey-lighten-5">
|
|
<v-card-title class="text-h6 pb-2">
|
|
<v-icon left color="primary">mdi-eye</v-icon>
|
|
Preview
|
|
</v-card-title>
|
|
<v-card-text class="pt-2">
|
|
<div class="text-body-2 mb-2">
|
|
<strong>Payment Instructions as shown to new members:</strong>
|
|
</div>
|
|
<v-row dense>
|
|
<v-col cols="4" class="text-body-2 font-weight-bold">Amount:</v-col>
|
|
<v-col cols="8" class="text-body-2">€{{ registrationForm.membershipFee || '0' }}/year</v-col>
|
|
</v-row>
|
|
<v-row dense v-if="registrationForm.iban">
|
|
<v-col cols="4" class="text-body-2 font-weight-bold">IBAN:</v-col>
|
|
<v-col cols="8" class="text-body-2 font-family-monospace">{{ registrationForm.iban }}</v-col>
|
|
</v-row>
|
|
<v-row dense v-if="registrationForm.accountHolder">
|
|
<v-col cols="4" class="text-body-2 font-weight-bold">Account:</v-col>
|
|
<v-col cols="8" class="text-body-2">{{ registrationForm.accountHolder }}</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
|
|
<!-- Email Configuration Tab -->
|
|
<v-tabs-window-item value="email">
|
|
<v-alert
|
|
type="info"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<template #title>SMTP Email Configuration</template>
|
|
Configure SMTP settings for sending emails from the portal (registration confirmations, password resets, dues reminders).
|
|
</v-alert>
|
|
|
|
<v-form ref="emailFormRef" v-model="emailFormValid">
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="emailForm.host"
|
|
label="SMTP Host"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.smtpHost"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="smtp.gmail.com"
|
|
hint="Your SMTP server hostname"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpHost ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpHost ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpHost')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model.number="emailForm.port"
|
|
label="Port"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.validPort]"
|
|
:readonly="!editingFields.smtpPort"
|
|
autocomplete="off"
|
|
required
|
|
type="number"
|
|
min="1"
|
|
max="65535"
|
|
placeholder="587"
|
|
hint="Usually 587 (TLS) or 465 (SSL)"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpPort ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpPort ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpPort')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-switch
|
|
v-model="emailForm.secure"
|
|
label="Use SSL/TLS encryption"
|
|
color="primary"
|
|
hide-details
|
|
class="mb-2"
|
|
/>
|
|
<div class="text-caption text-medium-emphasis">
|
|
Enable for secure connection (recommended for production)
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="emailForm.username"
|
|
label="Username"
|
|
variant="outlined"
|
|
:readonly="!editingFields.smtpUsername"
|
|
autocomplete="off"
|
|
placeholder="your-email@domain.com"
|
|
hint="SMTP authentication username (usually your email)"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpUsername ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpUsername ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpUsername')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="emailForm.password"
|
|
label="Password"
|
|
variant="outlined"
|
|
:readonly="!editingFields.smtpPassword"
|
|
:type="showEmailPassword ? 'text' : 'password'"
|
|
:append-inner-icon="showEmailPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="showEmailPassword = !showEmailPassword"
|
|
autocomplete="off"
|
|
placeholder="Enter SMTP password"
|
|
hint="SMTP authentication password or app password"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpPassword ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpPassword ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpPassword')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="emailForm.fromAddress"
|
|
label="From Email Address"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.email]"
|
|
:readonly="!editingFields.smtpFromAddress"
|
|
autocomplete="off"
|
|
required
|
|
type="email"
|
|
placeholder="noreply@monacousa.org"
|
|
hint="Email address that emails will be sent from"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpFromAddress ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpFromAddress ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpFromAddress')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center gap-2">
|
|
<v-text-field
|
|
v-model="emailForm.fromName"
|
|
label="From Name"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
:readonly="!editingFields.smtpFromName"
|
|
autocomplete="off"
|
|
required
|
|
placeholder="MonacoUSA Portal"
|
|
hint="Display name for outgoing emails"
|
|
persistent-hint
|
|
class="flex-grow-1"
|
|
/>
|
|
<v-btn
|
|
:icon="editingFields.smtpFromName ? 'mdi-check' : 'mdi-pencil'"
|
|
:color="editingFields.smtpFromName ? 'success' : 'primary'"
|
|
variant="outlined"
|
|
size="small"
|
|
@click="toggleEdit('smtpFromName')"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="testEmailAddress"
|
|
label="Test Email Address"
|
|
variant="outlined"
|
|
:rules="[rules.email]"
|
|
type="email"
|
|
placeholder="admin@monacousa.org"
|
|
hint="Email address to send test email to"
|
|
persistent-hint
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-btn
|
|
@click="sendTestEmail"
|
|
:loading="emailTestLoading"
|
|
:disabled="!emailFormValid || emailLoading || !testEmailAddress"
|
|
color="info"
|
|
variant="outlined"
|
|
block
|
|
>
|
|
<v-icon start>mdi-email-send</v-icon>
|
|
Send Test Email
|
|
</v-btn>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center h-100">
|
|
<v-chip
|
|
v-if="emailTestStatus"
|
|
:color="emailTestStatus.success ? 'success' : 'error'"
|
|
variant="flat"
|
|
size="small"
|
|
>
|
|
<v-icon start size="14">
|
|
{{ emailTestStatus.success ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
|
</v-icon>
|
|
{{ emailTestStatus.message }}
|
|
</v-chip>
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-alert
|
|
type="warning"
|
|
variant="outlined"
|
|
class="mt-2"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>Common SMTP Providers:</strong>
|
|
<ul class="mt-2 ml-4">
|
|
<li><strong>Gmail:</strong> smtp.gmail.com:587 (use App Password, not regular password)</li>
|
|
<li><strong>SendGrid:</strong> smtp.sendgrid.net:587</li>
|
|
<li><strong>Amazon SES:</strong> email-smtp.region.amazonaws.com:587</li>
|
|
<li><strong>Mailgun:</strong> smtp.mailgun.org:587</li>
|
|
</ul>
|
|
</div>
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
</v-tabs-window>
|
|
|
|
<!-- Success Message -->
|
|
<v-alert
|
|
v-if="showSuccessMessage"
|
|
type="success"
|
|
variant="tonal"
|
|
class="mt-4"
|
|
closable
|
|
@click:close="showSuccessMessage = false"
|
|
>
|
|
{{ successMessage }}
|
|
</v-alert>
|
|
|
|
<!-- Error Message -->
|
|
<v-alert
|
|
v-if="errorMessage"
|
|
type="error"
|
|
variant="tonal"
|
|
class="mt-4"
|
|
closable
|
|
@click:close="errorMessage = ''"
|
|
>
|
|
{{ errorMessage }}
|
|
</v-alert>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="pa-6 pt-0">
|
|
<v-spacer />
|
|
<v-btn
|
|
variant="text"
|
|
@click="closeDialog"
|
|
:disabled="isLoading"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
@click="saveCurrentTab"
|
|
:loading="isLoading"
|
|
:disabled="!isCurrentTabValid"
|
|
>
|
|
<v-icon start>mdi-content-save</v-icon>
|
|
Save {{ getCurrentTabName }}
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { NocoDBSettings, RecaptchaConfig, RegistrationConfig, SMTPConfig } from '~/utils/types';
|
|
|
|
interface Props {
|
|
modelValue: boolean;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:model-value', value: boolean): void;
|
|
(e: 'settings-saved'): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Tab management
|
|
const activeTab = ref('nocodb');
|
|
|
|
// Form refs
|
|
const nocodbFormRef = ref();
|
|
const recaptchaFormRef = ref();
|
|
const registrationFormRef = ref();
|
|
const emailFormRef = ref();
|
|
|
|
// Form validity
|
|
const nocodbFormValid = ref(false);
|
|
const recaptchaFormValid = ref(false);
|
|
const registrationFormValid = ref(false);
|
|
const emailFormValid = ref(false);
|
|
|
|
// Loading states
|
|
const nocodbLoading = ref(false);
|
|
const recaptchaLoading = ref(false);
|
|
const registrationLoading = ref(false);
|
|
const emailLoading = ref(false);
|
|
const nocodbTestLoading = ref(false);
|
|
const emailTestLoading = ref(false);
|
|
|
|
// Display states
|
|
const showNocodbApiKey = ref(false);
|
|
const showRecaptchaSecret = ref(false);
|
|
const showEmailPassword = ref(false);
|
|
const showSuccessMessage = ref(false);
|
|
const successMessage = ref('');
|
|
const errorMessage = ref('');
|
|
const nocodbConnectionStatus = ref<{ success: boolean; message: string } | null>(null);
|
|
const emailTestStatus = ref<{ success: boolean; message: string } | null>(null);
|
|
|
|
// Test email address
|
|
const testEmailAddress = ref('');
|
|
|
|
// Editing state for fields (to prevent autofill interference)
|
|
const editingFields = ref({
|
|
nocodbUrl: false,
|
|
nocodbApiKey: false,
|
|
nocodbBaseId: false,
|
|
membersTableId: false,
|
|
eventsTableId: false,
|
|
rsvpsTableId: false,
|
|
recaptchaSiteKey: false,
|
|
recaptchaSecretKey: false,
|
|
membershipFee: false,
|
|
iban: false,
|
|
accountHolder: false,
|
|
smtpHost: false,
|
|
smtpPort: false,
|
|
smtpUsername: false,
|
|
smtpPassword: false,
|
|
smtpFromAddress: false,
|
|
smtpFromName: false
|
|
});
|
|
|
|
// Toggle edit mode for a field
|
|
const toggleEdit = (fieldName: keyof typeof editingFields.value) => {
|
|
editingFields.value[fieldName] = !editingFields.value[fieldName];
|
|
};
|
|
|
|
// Form data
|
|
const nocodbForm = ref<NocoDBSettings>({
|
|
url: 'https://database.monacousa.org',
|
|
apiKey: '',
|
|
baseId: '',
|
|
tables: {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
}
|
|
});
|
|
|
|
const recaptchaForm = ref<RecaptchaConfig>({
|
|
siteKey: '',
|
|
secretKey: ''
|
|
});
|
|
|
|
const registrationForm = ref<RegistrationConfig>({
|
|
membershipFee: 50,
|
|
iban: '',
|
|
accountHolder: ''
|
|
});
|
|
|
|
const emailForm = ref<SMTPConfig>({
|
|
host: '',
|
|
port: 587,
|
|
secure: false,
|
|
username: '',
|
|
password: '',
|
|
fromAddress: '',
|
|
fromName: 'MonacoUSA Portal'
|
|
});
|
|
|
|
// Validation rules
|
|
const rules = {
|
|
required: (value: string | number) => {
|
|
return (!!value && value.toString().trim() !== '') || 'This field is required';
|
|
},
|
|
url: (value: string) => {
|
|
if (!value) return true;
|
|
const pattern = /^https?:\/\/.+/;
|
|
return pattern.test(value) || 'Please enter a valid URL';
|
|
},
|
|
positiveNumber: (value: number) => {
|
|
return (value && value > 0) || 'Must be a positive number';
|
|
},
|
|
validPort: (value: number) => {
|
|
return (value && value >= 1 && value <= 65535) || 'Port must be between 1 and 65535';
|
|
},
|
|
email: (value: string) => {
|
|
if (!value) return true;
|
|
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return pattern.test(value) || 'Please enter a valid email address';
|
|
},
|
|
iban: (value: string) => {
|
|
if (!value) return true;
|
|
// Basic IBAN validation (length and format)
|
|
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
return (cleaned.length >= 15 && cleaned.length <= 34 && /^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(cleaned)) || 'Please enter a valid IBAN';
|
|
}
|
|
};
|
|
|
|
// Computed properties
|
|
const isLoading = computed(() => {
|
|
return nocodbLoading.value || recaptchaLoading.value || registrationLoading.value;
|
|
});
|
|
|
|
const isCurrentTabValid = computed(() => {
|
|
switch (activeTab.value) {
|
|
case 'nocodb':
|
|
return nocodbFormValid.value;
|
|
case 'recaptcha':
|
|
return recaptchaFormValid.value;
|
|
case 'registration':
|
|
return registrationFormValid.value;
|
|
case 'email':
|
|
return emailFormValid.value;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
const getCurrentTabName = computed(() => {
|
|
switch (activeTab.value) {
|
|
case 'nocodb':
|
|
return 'NocoDB Settings';
|
|
case 'recaptcha':
|
|
return 'reCAPTCHA Settings';
|
|
case 'registration':
|
|
return 'Registration Settings';
|
|
case 'email':
|
|
return 'Email Settings';
|
|
default:
|
|
return 'Settings';
|
|
}
|
|
});
|
|
|
|
// Load configurations
|
|
const loadConfigurations = async () => {
|
|
try {
|
|
// Load NocoDB config
|
|
const nocodbResponse = await $fetch<{ success: boolean; data?: NocoDBSettings }>('/api/admin/nocodb-config');
|
|
if (nocodbResponse.success && nocodbResponse.data) {
|
|
nocodbForm.value = { ...nocodbResponse.data };
|
|
if (!nocodbForm.value.tables) {
|
|
nocodbForm.value.tables = {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
};
|
|
}
|
|
}
|
|
|
|
// Load reCAPTCHA config
|
|
const recaptchaResponse = await $fetch<{ success: boolean; data?: RecaptchaConfig }>('/api/admin/recaptcha-config');
|
|
if (recaptchaResponse.success && recaptchaResponse.data) {
|
|
recaptchaForm.value = { ...recaptchaResponse.data };
|
|
}
|
|
|
|
// Load registration config
|
|
const registrationResponse = await $fetch<{ success: boolean; data?: RegistrationConfig }>('/api/admin/registration-config');
|
|
if (registrationResponse.success && registrationResponse.data) {
|
|
registrationForm.value = { ...registrationResponse.data };
|
|
}
|
|
|
|
// Load SMTP/Email config
|
|
const smtpResponse = await $fetch<{ success: boolean; data?: SMTPConfig }>('/api/admin/smtp-config');
|
|
if (smtpResponse.success && smtpResponse.data) {
|
|
emailForm.value = { ...smtpResponse.data };
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load configurations:', error);
|
|
errorMessage.value = 'Failed to load current settings';
|
|
}
|
|
};
|
|
|
|
// Test NocoDB connection
|
|
const testNocodbConnection = async () => {
|
|
if (!nocodbFormRef.value) return;
|
|
|
|
const isValid = await nocodbFormRef.value.validate();
|
|
if (!isValid.valid) return;
|
|
|
|
nocodbTestLoading.value = true;
|
|
nocodbConnectionStatus.value = null;
|
|
|
|
try {
|
|
const response = await $fetch<{ success: boolean; message: string }>('/api/admin/nocodb-test', {
|
|
method: 'POST',
|
|
body: nocodbForm.value
|
|
});
|
|
|
|
nocodbConnectionStatus.value = {
|
|
success: response.success,
|
|
message: response.message || (response.success ? 'Connection successful' : 'Connection failed')
|
|
};
|
|
} catch (error: any) {
|
|
nocodbConnectionStatus.value = {
|
|
success: false,
|
|
message: error.message || 'Connection test failed'
|
|
};
|
|
} finally {
|
|
nocodbTestLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Send test email
|
|
const sendTestEmail = async () => {
|
|
if (!emailFormRef.value || !testEmailAddress.value) return;
|
|
|
|
const isValid = await emailFormRef.value.validate();
|
|
if (!isValid.valid) return;
|
|
|
|
// First save email configuration
|
|
emailLoading.value = true;
|
|
try {
|
|
await $fetch('/api/admin/smtp-config', {
|
|
method: 'POST',
|
|
body: emailForm.value
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Failed to save SMTP config:', error);
|
|
errorMessage.value = 'Failed to save SMTP configuration: ' + error.message;
|
|
emailLoading.value = false;
|
|
return;
|
|
} finally {
|
|
emailLoading.value = false;
|
|
}
|
|
|
|
// Then send test email
|
|
emailTestLoading.value = true;
|
|
emailTestStatus.value = null;
|
|
|
|
try {
|
|
const response = await $fetch<{ success: boolean; message: string }>('/api/admin/test-email', {
|
|
method: 'POST',
|
|
body: { testEmail: testEmailAddress.value }
|
|
});
|
|
|
|
emailTestStatus.value = {
|
|
success: response.success,
|
|
message: response.message || (response.success ? 'Test email sent successfully' : 'Test email failed')
|
|
};
|
|
} catch (error: any) {
|
|
emailTestStatus.value = {
|
|
success: false,
|
|
message: error.message || 'Test email failed'
|
|
};
|
|
} finally {
|
|
emailTestLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Save current tab
|
|
const saveCurrentTab = async () => {
|
|
let formRef, loading, data, endpoint;
|
|
|
|
switch (activeTab.value) {
|
|
case 'nocodb':
|
|
formRef = nocodbFormRef.value;
|
|
loading = nocodbLoading;
|
|
data = nocodbForm.value;
|
|
endpoint = '/api/admin/nocodb-config';
|
|
break;
|
|
case 'recaptcha':
|
|
formRef = recaptchaFormRef.value;
|
|
loading = recaptchaLoading;
|
|
data = recaptchaForm.value;
|
|
endpoint = '/api/admin/recaptcha-config';
|
|
break;
|
|
case 'registration':
|
|
formRef = registrationFormRef.value;
|
|
loading = registrationLoading;
|
|
data = registrationForm.value;
|
|
endpoint = '/api/admin/registration-config';
|
|
break;
|
|
case 'email':
|
|
formRef = emailFormRef.value;
|
|
loading = emailLoading;
|
|
data = emailForm.value;
|
|
endpoint = '/api/admin/smtp-config';
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (!formRef) return;
|
|
|
|
const isValid = await formRef.validate();
|
|
if (!isValid.valid) return;
|
|
|
|
loading.value = true;
|
|
errorMessage.value = '';
|
|
|
|
try {
|
|
const response = await $fetch<{ success: boolean; message?: string }>(endpoint, {
|
|
method: 'POST',
|
|
body: data
|
|
});
|
|
|
|
if (response.success) {
|
|
successMessage.value = `${getCurrentTabName.value} saved successfully!`;
|
|
showSuccessMessage.value = true;
|
|
emit('settings-saved');
|
|
|
|
// Auto-hide success message
|
|
setTimeout(() => {
|
|
showSuccessMessage.value = false;
|
|
}, 3000);
|
|
} else {
|
|
throw new Error(response.message || 'Failed to save settings');
|
|
}
|
|
} catch (error: any) {
|
|
console.error(`Error saving ${getCurrentTabName.value}:`, error);
|
|
errorMessage.value = error.message || `Failed to save ${getCurrentTabName.value}. Please try again.`;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Dialog management
|
|
const closeDialog = () => {
|
|
emit('update:model-value', false);
|
|
};
|
|
|
|
const resetForms = () => {
|
|
// Reset form data to defaults
|
|
nocodbForm.value = {
|
|
url: 'https://database.monacousa.org',
|
|
apiKey: '',
|
|
baseId: '',
|
|
tables: {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
}
|
|
};
|
|
|
|
recaptchaForm.value = {
|
|
siteKey: '',
|
|
secretKey: ''
|
|
};
|
|
|
|
registrationForm.value = {
|
|
membershipFee: 50,
|
|
iban: '',
|
|
accountHolder: ''
|
|
};
|
|
|
|
// Clear status and messages
|
|
nocodbConnectionStatus.value = null;
|
|
errorMessage.value = '';
|
|
showSuccessMessage.value = false;
|
|
activeTab.value = 'nocodb';
|
|
|
|
// Reset form validation
|
|
nextTick(() => {
|
|
nocodbFormRef.value?.resetValidation();
|
|
recaptchaFormRef.value?.resetValidation();
|
|
registrationFormRef.value?.resetValidation();
|
|
});
|
|
};
|
|
|
|
// Watch for dialog open
|
|
watch(() => props.modelValue, async (newValue) => {
|
|
if (newValue) {
|
|
resetForms();
|
|
await loadConfigurations();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.bg-primary {
|
|
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%) !important;
|
|
}
|
|
|
|
.border-b {
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.v-card {
|
|
border-radius: 12px !important;
|
|
}
|
|
|
|
/* Tab styling */
|
|
.v-tabs :deep(.v-tab) {
|
|
text-transform: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Form styling */
|
|
.v-card-text .v-row .v-col .v-alert {
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Password field styling */
|
|
.v-text-field :deep(.v-input__append-inner) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Preview card styling */
|
|
.bg-grey-lighten-5 {
|
|
background-color: rgba(0, 0, 0, 0.04) !important;
|
|
}
|
|
|
|
/* Connection status styling */
|
|
.h-100 {
|
|
height: 100%;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 600px) {
|
|
.v-card-title {
|
|
padding: 16px !important;
|
|
}
|
|
|
|
.v-card-text {
|
|
padding: 16px !important;
|
|
}
|
|
|
|
.v-card-actions {
|
|
padding: 16px !important;
|
|
padding-top: 0 !important;
|
|
}
|
|
}
|
|
</style>
|