feat: optional customer phone field gated by global toggle (#322)
Adds a `customer_phone` column on events plus an `event_phone_field_enabled` admin setting (default off) that surfaces the input in the create-event and event-detail forms. Designed for downstream automation tooling — once exposed via the upcoming public API, n8n / similar can pick it up to deliver gallery links over WhatsApp, SMS, etc. - Migration 080 adds the column + seeds the setting as false. Existing deployments see no UI change unless the admin opts in via Settings → Events. - Backend strips the field server-side when the toggle is off (defence in depth against form bypass). - Frontend renders the input only when the public-settings flag is true; always optional even then. - publicSettings + EventSettings types extended; CreateEventPage and EventDetailsPage wired to read the toggle and submit the value.
This commit is contained in:
@@ -52,6 +52,7 @@ export interface EventSettings {
|
||||
event_require_expiration: boolean;
|
||||
event_default_require_password: boolean;
|
||||
gallery_show_filter_bar: boolean;
|
||||
event_phone_field_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SeoSettings {
|
||||
@@ -127,7 +128,8 @@ export function useSettingsState() {
|
||||
event_require_event_date: true,
|
||||
event_require_expiration: true,
|
||||
event_default_require_password: true,
|
||||
gallery_show_filter_bar: true
|
||||
gallery_show_filter_bar: true,
|
||||
event_phone_field_enabled: false
|
||||
});
|
||||
|
||||
// SEO settings state
|
||||
@@ -212,7 +214,8 @@ export function useSettingsState() {
|
||||
event_require_event_date: toBoolean(settings.event_require_event_date, true),
|
||||
event_require_expiration: toBoolean(settings.event_require_expiration, true),
|
||||
event_default_require_password: toBoolean(settings.event_default_require_password, true),
|
||||
gallery_show_filter_bar: toBoolean(settings.gallery_show_filter_bar, true)
|
||||
gallery_show_filter_bar: toBoolean(settings.gallery_show_filter_bar, true),
|
||||
event_phone_field_enabled: toBoolean(settings.event_phone_field_enabled, false)
|
||||
});
|
||||
|
||||
setSeoSettings({
|
||||
|
||||
@@ -187,6 +187,25 @@ export const EventsTab: React.FC<EventsTabProps> = ({
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={eventSettings.event_phone_field_enabled}
|
||||
onChange={(e) => setEventSettings(prev => ({ ...prev, event_phone_field_enabled: e.target.checked }))}
|
||||
className="mt-1 w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
{t('settings.events.enablePhoneField', 'Enable phone number field')}
|
||||
</span>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
{t('settings.events.enablePhoneFieldHelp', 'Adds an optional phone number input to the event form. Useful for downstream automations like WhatsApp delivery via n8n. Always optional even when enabled.')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -35,6 +35,7 @@ interface FormData {
|
||||
event_date: string;
|
||||
customer_name: string;
|
||||
customer_email: string;
|
||||
customer_phone: string;
|
||||
admin_email: string;
|
||||
require_password: boolean;
|
||||
password: string;
|
||||
@@ -95,6 +96,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
event_date: new Date().toISOString().split('T')[0], // Initialize with ISO date format
|
||||
customer_name: '',
|
||||
customer_email: '',
|
||||
customer_phone: '',
|
||||
admin_email: '',
|
||||
require_password: true,
|
||||
password: '',
|
||||
@@ -177,6 +179,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
// Get field requirements (default to true if not set)
|
||||
const requireCustomerName = publicSettings?.event_require_customer_name !== false;
|
||||
const requireCustomerEmail = publicSettings?.event_require_customer_email !== false;
|
||||
const phoneFieldEnabled = publicSettings?.event_phone_field_enabled === true;
|
||||
const requireAdminEmail = publicSettings?.event_require_admin_email !== false;
|
||||
const requireEventDate = publicSettings?.event_require_event_date !== false;
|
||||
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
||||
@@ -351,6 +354,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
event_date: formData.event_date || undefined,
|
||||
customer_name: formData.customer_name,
|
||||
customer_email: formData.customer_email,
|
||||
...(phoneFieldEnabled && formData.customer_phone ? { customer_phone: formData.customer_phone.trim() } : {}),
|
||||
admin_email: formData.admin_email,
|
||||
require_password: formData.require_password,
|
||||
password: formData.require_password ? formData.password : undefined,
|
||||
@@ -654,6 +658,16 @@ export const CreateEventPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{phoneFieldEnabled && (
|
||||
<Input
|
||||
type="tel"
|
||||
label={`${t('events.customerPhone', 'Customer Phone')} (${t('common.optional')})`}
|
||||
placeholder={t('events.customerPhonePlaceholder', '+1 555 555 1234')}
|
||||
value={formData.customer_phone}
|
||||
onChange={handleInputChange('customer_phone')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
label={requireAdminEmail ? t('events.adminEmail') : `${t('events.adminEmail')} (${t('common.optional')})`}
|
||||
|
||||
@@ -149,6 +149,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_photo_id: number | null;
|
||||
customer_name: string;
|
||||
customer_email: string;
|
||||
customer_phone: string;
|
||||
source_mode: 'managed' | 'reference';
|
||||
external_path: string;
|
||||
require_password: boolean;
|
||||
@@ -184,6 +185,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_photo_id: null,
|
||||
customer_name: '',
|
||||
customer_email: '',
|
||||
customer_phone: '',
|
||||
source_mode: 'managed',
|
||||
external_path: '',
|
||||
require_password: true,
|
||||
@@ -338,6 +340,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
queryFn: () => publicSettingsService.getPublicSettings(),
|
||||
});
|
||||
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
||||
const phoneFieldEnabled = publicSettings?.event_phone_field_enabled === true;
|
||||
|
||||
// Fetch categories for the event
|
||||
const { data: categories = [] } = useQuery({
|
||||
@@ -430,6 +433,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_photo_id: event.hero_photo_id || null,
|
||||
customer_name: event.customer_name || '',
|
||||
customer_email: event.customer_email || '',
|
||||
customer_phone: (event as any).customer_phone || '',
|
||||
source_mode: event.source_mode === 'reference' ? 'reference' : 'managed',
|
||||
external_path: event.external_path || '',
|
||||
require_password: normalizeRequirePassword(event.require_password),
|
||||
@@ -617,6 +621,11 @@ export const EventDetailsPage: React.FC = () => {
|
||||
if (editForm.customer_email !== undefined && editForm.customer_email !== null && editForm.customer_email.trim()) {
|
||||
updateData.customer_email = editForm.customer_email;
|
||||
}
|
||||
if (editForm.customer_phone !== undefined) {
|
||||
// Send empty string as null so an admin can clear the field. Backend
|
||||
// strips this entirely if the global phone-field toggle is off.
|
||||
updateData.customer_phone = editForm.customer_phone.trim() || null;
|
||||
}
|
||||
|
||||
if (editForm.new_password) {
|
||||
updateData.password = editForm.new_password;
|
||||
@@ -970,6 +979,20 @@ export const EventDetailsPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{phoneFieldEnabled && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('events.customerPhone', 'Customer Phone')} ({t('common.optional')})
|
||||
</label>
|
||||
<Input
|
||||
type="tel"
|
||||
value={editForm.customer_phone}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, customer_phone: e.target.value }))}
|
||||
placeholder={t('events.customerPhonePlaceholder', '+1 555 555 1234')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('events.expirationDate')}
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface PublicSettings {
|
||||
event_require_expiration?: boolean;
|
||||
event_default_require_password?: boolean;
|
||||
gallery_show_filter_bar?: boolean;
|
||||
event_phone_field_enabled?: boolean;
|
||||
}
|
||||
|
||||
export const publicSettingsService = {
|
||||
|
||||
Reference in New Issue
Block a user