feat: Add SMTP settings and collapsible email configuration UI

- Add SMTP fields to TicketEmailSettings model (host, port, TLS/SSL, credentials, from email/name)
- Update serializers with SMTP fields and is_smtp_configured flag
- Add TicketEmailTestSmtpView for testing SMTP connections
- Update frontend API types and hooks for SMTP settings
- Add collapsible IMAP and SMTP configuration sections with "Configured" badges
- Fix TypeScript errors in mockData.ts (missing required fields, type mismatches)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-29 18:28:29 -05:00
parent 0c7d76e264
commit cfc1b36ada
94 changed files with 13419 additions and 1121 deletions

View File

@@ -121,6 +121,16 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
data.permissions = inviteForm.permissions;
}
// Only include limits if at least one is enabled (boolean true or numeric value set)
const hasLimits = Object.entries(inviteForm.limits).some(([key, value]) => {
if (typeof value === 'boolean') return value === true;
if (typeof value === 'number') return true; // numeric limits are meaningful even if 0
return false;
});
if (hasLimits) {
data.limits = inviteForm.limits;
}
if (inviteForm.personal_message.trim()) {
data.personal_message = inviteForm.personal_message.trim();
}
@@ -320,24 +330,21 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
</div>
</div>
{/* Feature Limits (Not Yet Implemented) */}
{/* Feature Limits & Capabilities */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<div className="flex items-center justify-between mb-3">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Feature Limits & Capabilities
</label>
<span className="text-xs px-2 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full">
Coming Soon
</span>
</div>
<div className="space-y-3 opacity-50">
<div className="space-y-3">
{/* Video Conferencing */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={inviteForm.limits.can_add_video_conferencing}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_add_video_conferencing: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can add video conferencing to events
</label>
@@ -347,8 +354,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.max_event_types === null}
disabled
className="rounded border-gray-300 dark:border-gray-600 mt-1 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_event_types: e.target.checked ? null : 10 } })}
className="rounded border-gray-300 dark:border-gray-600 mt-1"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
@@ -357,10 +364,11 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="number"
min="1"
disabled
disabled={inviteForm.limits.max_event_types === null}
value={inviteForm.limits.max_event_types || ''}
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_event_types: e.target.value ? parseInt(e.target.value) : null } })}
placeholder="Or set a limit"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white cursor-not-allowed"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
</div>
@@ -370,8 +378,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.max_calendars_connected === null}
disabled
className="rounded border-gray-300 dark:border-gray-600 mt-1 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_calendars_connected: e.target.checked ? null : 5 } })}
className="rounded border-gray-300 dark:border-gray-600 mt-1"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
@@ -380,10 +388,11 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="number"
min="1"
disabled
disabled={inviteForm.limits.max_calendars_connected === null}
value={inviteForm.limits.max_calendars_connected || ''}
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_calendars_connected: e.target.value ? parseInt(e.target.value) : null } })}
placeholder="Or set a limit"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white cursor-not-allowed"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
</div>
@@ -393,8 +402,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_connect_to_api}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_connect_to_api: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can connect to external APIs
</label>
@@ -404,8 +413,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_book_repeated_events}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_book_repeated_events: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can book repeated/recurring events
</label>
@@ -415,8 +424,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_require_2fa}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_require_2fa: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can require 2FA for users
</label>
@@ -426,8 +435,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_download_logs}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_download_logs: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can download system logs
</label>
@@ -437,8 +446,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_delete_data}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_delete_data: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can permanently delete data
</label>
@@ -448,8 +457,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_use_masked_phone_numbers}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_masked_phone_numbers: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can use masked phone numbers for privacy
</label>
@@ -459,8 +468,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_use_pos}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_pos: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can use Point of Sale (POS) system
</label>
@@ -470,8 +479,8 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
<input
type="checkbox"
checked={inviteForm.limits.can_use_mobile_app}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_mobile_app: e.target.checked } })}
className="rounded border-gray-300 dark:border-gray-600"
/>
Can use mobile app
</label>