monacousa-portal/components/AddMemberDialog.vue

490 lines
14 KiB
Vue

<template>
<v-dialog
:model-value="modelValue"
@update:model-value="$emit('update:model-value', $event)"
max-width="900"
persistent
scrollable
>
<v-card>
<v-card-title class="d-flex align-center pa-6 bg-primary">
<v-icon class="mr-3 text-white">mdi-account-plus</v-icon>
<h2 class="text-h5 text-white font-weight-bold flex-grow-1">
Add New Member
</h2>
<v-btn
icon
variant="text"
color="white"
@click="closeDialog"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-6">
<v-form ref="formRef" v-model="formValid" @submit.prevent="handleSubmit">
<v-row>
<!-- Personal Information Section -->
<v-col cols="12">
<h3 class="text-h6 mb-4 text-primary">Personal Information</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="form['First Name']"
label="First Name"
variant="outlined"
:rules="[rules.required]"
required
:error="hasFieldError('First Name')"
:error-messages="getFieldError('First Name')"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="form['Last Name']"
label="Last Name"
variant="outlined"
:rules="[rules.required]"
required
:error="hasFieldError('Last Name')"
:error-messages="getFieldError('Last Name')"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="form.Email"
label="Email Address"
type="email"
variant="outlined"
:rules="[rules.required, rules.email]"
required
:error="hasFieldError('Email')"
:error-messages="getFieldError('Email')"
/>
</v-col>
<v-col cols="12" md="6">
<PhoneInputWrapper
v-model="form.Phone"
label="Phone Number"
placeholder="Enter phone number"
:error="hasFieldError('Phone')"
:error-message="getFieldError('Phone')"
@phone-data="handlePhoneData"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="form['Date of Birth']"
label="Date of Birth"
type="date"
variant="outlined"
:error="hasFieldError('Date of Birth')"
:error-messages="getFieldError('Date of Birth')"
/>
</v-col>
<v-col cols="12" md="6">
<MultipleNationalityInput
v-model="form.Nationality"
label="Nationality"
:error="hasFieldError('Nationality')"
:error-message="getFieldError('Nationality')"
:max-nationalities="3"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="form.Address"
label="Address"
variant="outlined"
rows="2"
:error="hasFieldError('Address')"
:error-messages="getFieldError('Address')"
/>
</v-col>
<!-- Membership Information Section -->
<v-col cols="12">
<v-divider class="my-4" />
<h3 class="text-h6 mb-4 text-primary">Membership Information</h3>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="form['Membership Status']"
:items="membershipStatusOptions"
label="Membership Status"
variant="outlined"
:rules="[rules.required]"
required
:error="hasFieldError('Membership Status')"
:error-messages="getFieldError('Membership Status')"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="form['Member Since']"
label="Member Since"
type="date"
variant="outlined"
:error="hasFieldError('Member Since')"
:error-messages="getFieldError('Member Since')"
/>
</v-col>
<v-col cols="12" md="4">
<v-switch
v-model="duesPaid"
label="Current Year Dues Paid"
color="success"
inset
:error="hasFieldError('Current Year Dues Paid')"
:error-messages="getFieldError('Current Year Dues Paid')"
/>
</v-col>
<v-col cols="12" md="6" v-if="duesPaid">
<v-text-field
v-model="form['Membership Date Paid']"
label="Payment Date"
type="date"
variant="outlined"
:error="hasFieldError('Membership Date Paid')"
:error-messages="getFieldError('Membership Date Paid')"
/>
</v-col>
<v-col cols="12" md="6" v-if="!duesPaid">
<v-text-field
v-model="form['Payment Due Date']"
label="Payment Due Date"
type="date"
variant="outlined"
:error="hasFieldError('Payment Due Date')"
:error-messages="getFieldError('Payment Due Date')"
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions class="pa-6 pt-0">
<v-spacer />
<v-btn
variant="text"
@click="closeDialog"
:disabled="loading"
>
Cancel
</v-btn>
<v-btn
color="primary"
@click="handleSubmit"
:loading="loading"
:disabled="!formValid"
>
<v-icon start>mdi-account-plus</v-icon>
Add Member
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import type { Member } from '~/utils/types';
import { formatBooleanAsString } from '~/utils/client-utils';
interface Props {
modelValue: boolean;
}
interface Emits {
(e: 'update:model-value', value: boolean): void;
(e: 'member-created', member: Member): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// Form state
const formRef = ref();
const formValid = ref(false);
const loading = ref(false);
// Form data
const form = ref({
'First Name': '',
'Last Name': '',
Email: '',
Phone: '',
'Date of Birth': '',
Nationality: '',
Address: '',
'Membership Status': 'Active',
'Member Since': new Date().toISOString().split('T')[0], // Today's date
'Current Year Dues Paid': 'false',
'Membership Date Paid': '',
'Payment Due Date': ''
});
// Additional form state
const duesPaid = ref(false);
const phoneData = ref(null);
// Error handling
const fieldErrors = ref<Record<string, string>>({});
// Watch dues paid switch
watch(duesPaid, (newValue) => {
form.value['Current Year Dues Paid'] = formatBooleanAsString(newValue);
if (newValue) {
form.value['Payment Due Date'] = '';
if (!form.value['Membership Date Paid']) {
form.value['Membership Date Paid'] = new Date().toISOString().split('T')[0];
}
} else {
form.value['Membership Date Paid'] = '';
if (!form.value['Payment Due Date']) {
// Set due date to one year from member since date or today
const memberSince = form.value['Member Since'] || new Date().toISOString().split('T')[0];
const dueDate = new Date(memberSince);
dueDate.setFullYear(dueDate.getFullYear() + 1);
form.value['Payment Due Date'] = dueDate.toISOString().split('T')[0];
}
}
});
// Membership status options
const membershipStatusOptions = [
{ title: 'Active', value: 'Active' },
{ title: 'Inactive', value: 'Inactive' },
{ title: 'Pending', value: 'Pending' },
{ title: 'Expired', value: 'Expired' }
];
// Validation rules
const rules = {
required: (value: any) => {
if (typeof value === 'string') {
return !!value?.trim() || 'This field is required';
}
return !!value || 'This field is required';
},
email: (value: string) => {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return !value || pattern.test(value) || 'Please enter a valid email address';
}
};
// Error handling methods
const hasFieldError = (fieldName: string) => {
return !!fieldErrors.value[fieldName];
};
const getFieldError = (fieldName: string) => {
return fieldErrors.value[fieldName] || '';
};
const clearFieldErrors = () => {
fieldErrors.value = {};
};
// Phone data handler
const handlePhoneData = (data: any) => {
phoneData.value = data;
};
// Form submission
const handleSubmit = async () => {
if (!formRef.value) return;
const isValid = await formRef.value.validate();
if (!isValid.valid) {
return;
}
loading.value = true;
clearFieldErrors();
try {
// Debug: Log the current form state
console.log('[AddMemberDialog] Form validation passed');
console.log('[AddMemberDialog] Current form.value:', JSON.stringify(form.value, null, 2));
console.log('[AddMemberDialog] Form keys:', Object.keys(form.value));
console.log('[AddMemberDialog] duesPaid switch value:', duesPaid.value);
// Get current form values
const currentForm = unref(form);
console.log('[AddMemberDialog] Unref form access test:');
console.log(' - First Name:', currentForm['First Name']);
console.log(' - Last Name:', currentForm['Last Name']);
console.log(' - Email:', currentForm.Email);
console.log(' - Phone:', currentForm.Phone);
// Simple approach - send the form data as-is with display names
// Let the server handle field normalization
const memberData = {
'First Name': currentForm['First Name']?.trim(),
'Last Name': currentForm['Last Name']?.trim(),
'Email': currentForm.Email?.trim(),
'Phone': currentForm.Phone?.trim() || '',
'Date of Birth': currentForm['Date of Birth'] || '',
'Nationality': currentForm.Nationality?.trim() || '',
'Address': currentForm.Address?.trim() || '',
'Membership Status': currentForm['Membership Status'],
'Member Since': currentForm['Member Since'] || '',
'Current Year Dues Paid': currentForm['Current Year Dues Paid'],
'Membership Date Paid': currentForm['Membership Date Paid'] || '',
'Payment Due Date': currentForm['Payment Due Date'] || ''
};
// Ensure required fields are not empty
if (!memberData['First Name']) {
console.error('[AddMemberDialog] First Name is empty. Raw value:', currentForm['First Name']);
throw new Error('First Name is required');
}
if (!memberData['Last Name']) {
console.error('[AddMemberDialog] Last Name is empty. Raw value:', currentForm['Last Name']);
throw new Error('Last Name is required');
}
if (!memberData['Email']) {
console.error('[AddMemberDialog] Email is empty. Raw value:', currentForm.Email);
throw new Error('Email is required');
}
console.log('[AddMemberDialog] Final memberData:', JSON.stringify(memberData, null, 2));
console.log('[AddMemberDialog] About to submit to API...');
const response = await $fetch<{ success: boolean; data: Member; message?: string }>('/api/members', {
method: 'POST',
body: memberData
});
if (response.success && response.data) {
console.log('[AddMemberDialog] Member created successfully:', response.data);
emit('member-created', response.data);
closeDialog();
resetForm();
} else {
throw new Error(response.message || 'Failed to create member');
}
} catch (error: any) {
console.error('[AddMemberDialog] Error creating member:', error);
// Handle validation errors
if (error.data?.fieldErrors) {
fieldErrors.value = error.data.fieldErrors;
} else {
// Show general error
fieldErrors.value = {
general: error.message || 'Failed to create member. Please try again.'
};
}
} finally {
loading.value = false;
}
};
// Dialog management
const closeDialog = () => {
emit('update:model-value', false);
};
const resetForm = () => {
form.value = {
'First Name': '',
'Last Name': '',
Email: '',
Phone: '',
'Date of Birth': '',
Nationality: '',
Address: '',
'Membership Status': 'Active',
'Member Since': new Date().toISOString().split('T')[0],
'Current Year Dues Paid': 'false',
'Membership Date Paid': '',
'Payment Due Date': ''
};
duesPaid.value = false;
phoneData.value = null;
clearFieldErrors();
// Reset form validation
nextTick(() => {
formRef.value?.resetValidation();
});
};
// Watch for dialog open/close
watch(() => props.modelValue, (newValue) => {
if (newValue) {
// Dialog opened - reset form
resetForm();
}
});
</script>
<style scoped>
.bg-primary {
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%) !important;
}
.text-primary {
color: #a31515 !important;
}
.v-card {
border-radius: 12px !important;
}
/* Form section spacing */
.v-card-text .v-row .v-col:first-child h3 {
border-bottom: 2px solid rgba(var(--v-theme-primary), 0.12);
padding-bottom: 8px;
}
/* Error message styling */
.field-error {
color: rgb(var(--v-theme-error));
font-size: 0.75rem;
margin-top: 4px;
}
/* Switch styling */
.v-switch {
flex: none;
}
/* Responsive adjustments */
@media (max-width: 960px) {
.v-dialog {
margin: 16px;
}
}
@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>