telephone updates
Build And Push Image / docker (push) Successful in 2m52s Details

This commit is contained in:
Matt 2025-08-07 22:13:05 +02:00
parent 13fa95a9a2
commit 024eca02ac
2 changed files with 237 additions and 76 deletions

View File

@ -10,7 +10,7 @@
<v-card-title class="d-flex align-center pa-6 bg-primary"> <v-card-title class="d-flex align-center pa-6 bg-primary">
<v-icon class="mr-3 text-white">mdi-account-edit</v-icon> <v-icon class="mr-3 text-white">mdi-account-edit</v-icon>
<h2 class="text-h5 text-white font-weight-bold flex-grow-1"> <h2 class="text-h5 text-white font-weight-bold flex-grow-1">
Edit Member: {{ member?.FullName || `${member?.['First Name']} ${member?.['Last Name']}` }} Edit Member: {{ member?.FullName || `${member?.first_name} ${member?.last_name}` }}
</h2> </h2>
<v-btn <v-btn
icon icon
@ -32,81 +32,81 @@
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="form['First Name']" v-model="form.first_name"
label="First Name" label="First Name"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
:error="hasFieldError('First Name')" :error="hasFieldError('first_name')"
:error-messages="getFieldError('First Name')" :error-messages="getFieldError('first_name')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="form['Last Name']" v-model="form.last_name"
label="Last Name" label="Last Name"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
:error="hasFieldError('Last Name')" :error="hasFieldError('last_name')"
:error-messages="getFieldError('Last Name')" :error-messages="getFieldError('last_name')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="form.Email" v-model="form.email"
label="Email Address" label="Email Address"
type="email" type="email"
variant="outlined" variant="outlined"
:rules="[rules.required, rules.email]" :rules="[rules.required, rules.email]"
required required
:error="hasFieldError('Email')" :error="hasFieldError('email')"
:error-messages="getFieldError('Email')" :error-messages="getFieldError('email')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<PhoneInputWrapper <EnhancedPhoneInput
v-model="form.Phone" v-model="form.phone"
label="Phone Number" label="Phone Number"
placeholder="Enter phone number" placeholder="Enter phone number"
:error="hasFieldError('Phone')" :error="hasFieldError('phone')"
:error-message="getFieldError('Phone')" :error-message="getFieldError('phone')"
@phone-data="handlePhoneData" @phone-data="handlePhoneData"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="form['Date of Birth']" v-model="form.date_of_birth"
label="Date of Birth" label="Date of Birth"
type="date" type="date"
variant="outlined" variant="outlined"
:error="hasFieldError('Date of Birth')" :error="hasFieldError('date_of_birth')"
:error-messages="getFieldError('Date of Birth')" :error-messages="getFieldError('date_of_birth')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<MultipleNationalityInput <MultipleNationalityInput
v-model="form.Nationality" v-model="form.nationality"
label="Nationality" label="Nationality"
:error="hasFieldError('Nationality')" :error="hasFieldError('nationality')"
:error-message="getFieldError('Nationality')" :error-message="getFieldError('nationality')"
:max-nationalities="3" :max-nationalities="3"
/> />
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea
v-model="form.Address" v-model="form.address"
label="Address" label="Address"
variant="outlined" variant="outlined"
rows="2" rows="2"
:error="hasFieldError('Address')" :error="hasFieldError('address')"
:error-messages="getFieldError('Address')" :error-messages="getFieldError('address')"
/> />
</v-col> </v-col>
@ -118,25 +118,25 @@
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-select <v-select
v-model="form['Membership Status']" v-model="form.membership_status"
:items="membershipStatusOptions" :items="membershipStatusOptions"
label="Membership Status" label="Membership Status"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
:error="hasFieldError('Membership Status')" :error="hasFieldError('membership_status')"
:error-messages="getFieldError('Membership Status')" :error-messages="getFieldError('membership_status')"
/> />
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-text-field <v-text-field
v-model="form['Member Since']" v-model="form.member_since"
label="Member Since" label="Member Since"
type="date" type="date"
variant="outlined" variant="outlined"
:error="hasFieldError('Member Since')" :error="hasFieldError('member_since')"
:error-messages="getFieldError('Member Since')" :error-messages="getFieldError('member_since')"
/> />
</v-col> </v-col>
@ -146,30 +146,30 @@
label="Current Year Dues Paid" label="Current Year Dues Paid"
color="success" color="success"
inset inset
:error="hasFieldError('Current Year Dues Paid')" :error="hasFieldError('current_year_dues_paid')"
:error-messages="getFieldError('Current Year Dues Paid')" :error-messages="getFieldError('current_year_dues_paid')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6" v-if="duesPaid"> <v-col cols="12" md="6" v-if="duesPaid">
<v-text-field <v-text-field
v-model="form['Membership Date Paid']" v-model="form.membership_date_paid"
label="Payment Date" label="Payment Date"
type="date" type="date"
variant="outlined" variant="outlined"
:error="hasFieldError('Membership Date Paid')" :error="hasFieldError('membership_date_paid')"
:error-messages="getFieldError('Membership Date Paid')" :error-messages="getFieldError('membership_date_paid')"
/> />
</v-col> </v-col>
<v-col cols="12" md="6" v-if="!duesPaid"> <v-col cols="12" md="6" v-if="!duesPaid">
<v-text-field <v-text-field
v-model="form['Payment Due Date']" v-model="form.payment_due_date"
label="Payment Due Date" label="Payment Due Date"
type="date" type="date"
variant="outlined" variant="outlined"
:error="hasFieldError('Payment Due Date')" :error="hasFieldError('payment_due_date')"
:error-messages="getFieldError('Payment Due Date')" :error-messages="getFieldError('payment_due_date')"
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -201,7 +201,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Member } from '~/utils/types'; import type { Member } from '~/utils/types';
import { formatBooleanAsString } from '~/server/utils/nocodb';
interface Props { interface Props {
modelValue: boolean; modelValue: boolean;
@ -221,20 +220,20 @@ const formRef = ref();
const formValid = ref(false); const formValid = ref(false);
const loading = ref(false); const loading = ref(false);
// Form data // Form data - using snake_case field names
const form = ref({ const form = ref({
'First Name': '', first_name: '',
'Last Name': '', last_name: '',
Email: '', email: '',
Phone: '', phone: '',
'Date of Birth': '', date_of_birth: '',
Nationality: '', nationality: '',
Address: '', address: '',
'Membership Status': 'Active', membership_status: 'Active',
'Member Since': '', member_since: '',
'Current Year Dues Paid': 'false', current_year_dues_paid: 'false',
'Membership Date Paid': '', membership_date_paid: '',
'Payment Due Date': '' payment_due_date: ''
}); });
// Additional form state // Additional form state
@ -246,20 +245,20 @@ const fieldErrors = ref<Record<string, string>>({});
// Watch dues paid switch // Watch dues paid switch
watch(duesPaid, (newValue) => { watch(duesPaid, (newValue) => {
form.value['Current Year Dues Paid'] = formatBooleanAsString(newValue); form.value.current_year_dues_paid = newValue ? 'true' : 'false';
if (newValue) { if (newValue) {
form.value['Payment Due Date'] = ''; form.value.payment_due_date = '';
if (!form.value['Membership Date Paid']) { if (!form.value.membership_date_paid) {
form.value['Membership Date Paid'] = new Date().toISOString().split('T')[0]; form.value.membership_date_paid = new Date().toISOString().split('T')[0];
} }
} else { } else {
form.value['Membership Date Paid'] = ''; form.value.membership_date_paid = '';
if (!form.value['Payment Due Date']) { if (!form.value.payment_due_date) {
// Set due date to one year from member since date or today // 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 memberSince = form.value.member_since || new Date().toISOString().split('T')[0];
const dueDate = new Date(memberSince); const dueDate = new Date(memberSince);
dueDate.setFullYear(dueDate.getFullYear() + 1); dueDate.setFullYear(dueDate.getFullYear() + 1);
form.value['Payment Due Date'] = dueDate.toISOString().split('T')[0]; form.value.payment_due_date = dueDate.toISOString().split('T')[0];
} }
} }
}); });
@ -304,28 +303,44 @@ const handlePhoneData = (data: any) => {
phoneData.value = data; phoneData.value = data;
}; };
// Form pre-population // Form pre-population - Updated to use snake_case field names
const populateForm = () => { const populateForm = () => {
if (!props.member) return; if (!props.member) return;
console.log('[EditMemberDialog] Populating form with member data:', props.member);
const member = props.member; const member = props.member;
// Convert date fields to proper format for input[type="date"]
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date.toISOString().split('T')[0];
} catch {
return dateString;
}
};
form.value = { form.value = {
'First Name': member['First Name'] || '', first_name: member.first_name || '',
'Last Name': member['Last Name'] || '', last_name: member.last_name || '',
Email: member.Email || '', email: member.email || '',
Phone: member.Phone || '', phone: member.phone || '',
'Date of Birth': member['Date of Birth'] || '', date_of_birth: formatDateForInput(member.date_of_birth || ''),
Nationality: member.Nationality || '', nationality: member.nationality || '',
Address: member.Address || '', address: member.address || '',
'Membership Status': member['Membership Status'] || 'Active', membership_status: member.membership_status || 'Active',
'Member Since': member['Member Since'] || '', member_since: formatDateForInput(member.member_since || ''),
'Current Year Dues Paid': member['Current Year Dues Paid'] || 'false', current_year_dues_paid: member.current_year_dues_paid || 'false',
'Membership Date Paid': member['Membership Date Paid'] || '', membership_date_paid: formatDateForInput(member.membership_date_paid || ''),
'Payment Due Date': member['Payment Due Date'] || '' payment_due_date: formatDateForInput(member.payment_due_date || '')
}; };
// Set dues paid switch based on the string value // Set dues paid switch based on the string value
duesPaid.value = member['Current Year Dues Paid'] === 'true'; duesPaid.value = member.current_year_dues_paid === 'true';
console.log('[EditMemberDialog] Form populated:', form.value);
}; };
// Form submission // Form submission
@ -345,13 +360,13 @@ const handleSubmit = async () => {
const memberData = { ...form.value }; const memberData = { ...form.value };
// Ensure required fields are not empty // Ensure required fields are not empty
if (!memberData['First Name']?.trim()) { if (!memberData.first_name?.trim()) {
throw new Error('First Name is required'); throw new Error('First Name is required');
} }
if (!memberData['Last Name']?.trim()) { if (!memberData.last_name?.trim()) {
throw new Error('Last Name is required'); throw new Error('Last Name is required');
} }
if (!memberData.Email?.trim()) { if (!memberData.email?.trim()) {
throw new Error('Email is required'); throw new Error('Email is required');
} }

View File

@ -0,0 +1,146 @@
<template>
<div class="enhanced-phone-input">
<PhoneInput
v-model="phoneValue"
:country-code="selectedCountryCode"
:placeholder="placeholder || 'Enter phone number'"
:preferred-countries="['US', 'FR', 'GB', 'DE', 'CA', 'AU', 'ES', 'IT', 'NL', 'BE']"
:auto-format="true"
:no-formatting-as-you-type="false"
country-locale="en-US"
@update="handlePhoneUpdate"
@input="handlePhoneInput"
class="phone-input-wrapper"
>
<template #input="{ inputValue, updateInputValue, placeholder: inputPlaceholder }">
<v-text-field
:model-value="inputValue"
@update:model-value="updateInputValue"
:placeholder="inputPlaceholder"
:label="label"
variant="outlined"
:error="hasError"
:error-messages="errorMessage"
:rules="rules"
class="phone-number-input"
hide-details="auto"
/>
</template>
</PhoneInput>
</div>
</template>
<script setup lang="ts">
import PhoneInput from 'base-vue-phone-input';
import type { Results as PhoneResults } from 'base-vue-phone-input';
interface Props {
modelValue?: string;
label?: string;
placeholder?: string;
error?: boolean;
errorMessage?: string;
rules?: Array<(value: any) => boolean | string>;
}
interface Emits {
(e: 'update:model-value', value: string): void;
(e: 'phone-data', data: PhoneResults): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
label: 'Phone Number',
placeholder: 'Enter phone number',
error: false,
errorMessage: '',
rules: () => []
});
const emit = defineEmits<Emits>();
// Local state
const phoneValue = ref(props.modelValue);
const selectedCountryCode = ref('US');
const phoneData = ref<PhoneResults | null>(null);
// Computed properties
const hasError = computed(() => props.error);
// Watch for external changes to modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue !== phoneValue.value) {
phoneValue.value = newValue;
}
});
// Handle phone input events
const handlePhoneInput = (value: string) => {
phoneValue.value = value;
emit('update:model-value', value);
};
const handlePhoneUpdate = (data: PhoneResults) => {
phoneData.value = data;
// Update country code if available
if (data.countryCode) {
selectedCountryCode.value = data.countryCode;
}
// Emit the formatted phone number
const formattedNumber = data.formatInternational || data.e164 || phoneValue.value;
if (formattedNumber !== phoneValue.value) {
phoneValue.value = formattedNumber;
emit('update:model-value', formattedNumber);
}
// Emit phone data for parent component
emit('phone-data', data);
};
// Initialize with modelValue
onMounted(() => {
if (props.modelValue) {
phoneValue.value = props.modelValue;
}
});
</script>
<style scoped>
.enhanced-phone-input {
width: 100%;
}
.phone-input-wrapper {
display: flex;
gap: 8px;
align-items: flex-start;
}
.country-select {
flex: 0 0 120px;
}
.phone-number-input {
flex: 1;
}
.flag-emoji {
font-size: 1.2em;
}
.country-code {
font-size: 0.875rem;
color: rgba(var(--v-theme-on-surface), 0.87);
}
/* Ensure proper spacing in Vuetify forms */
:deep(.v-input) {
margin-bottom: 0;
}
:deep(.v-input__details) {
min-height: 20px;
}
</style>