Add staff email client with WebSocket real-time updates

Implements a complete email client for platform staff members:

Backend:
- Add routing_mode field to PlatformEmailAddress (PLATFORM/STAFF)
- Create staff_email app with models for folders, emails, attachments, labels
- IMAP service for fetching emails with folder mapping
- SMTP service for sending emails with attachment support
- Celery tasks for periodic sync and full sync operations
- WebSocket consumer for real-time notifications
- Comprehensive API viewsets with filtering and actions

Frontend:
- Thunderbird-style three-pane email interface
- Multi-account support with drag-and-drop ordering
- Email composer with rich text editor
- Email viewer with thread support
- Real-time WebSocket updates for new emails and sync status
- 94 unit tests covering models, serializers, views, services, and consumers

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 01:50:40 -05:00
parent 7b380fa903
commit 18eeda62e8
62 changed files with 8943 additions and 410 deletions

View File

@@ -1,31 +1,113 @@
/**
* HelpButton Component
*
* A contextual help button that appears at the top-right of pages
* and links to the relevant help documentation.
* A help button for the top bar that navigates to context-aware help pages.
* Automatically determines the help path based on the current route.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { HelpCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface HelpButtonProps {
helpPath: string;
className?: string;
}
// Map route suffixes to their help page suffixes
// These get prefixed appropriately based on context (tenant dashboard or public)
const routeToHelpSuffix: Record<string, string> = {
'/': 'dashboard',
'/dashboard': 'dashboard',
'/scheduler': 'scheduler',
'/my-schedule': 'scheduler',
'/tasks': 'tasks',
'/customers': 'customers',
'/services': 'services',
'/resources': 'resources',
'/locations': 'locations',
'/staff': 'staff',
'/time-blocks': 'time-blocks',
'/my-availability': 'time-blocks',
'/messages': 'messages',
'/tickets': 'ticketing',
'/payments': 'payments',
'/contracts': 'contracts',
'/contracts/templates': 'contracts',
'/automations': 'automations',
'/automations/marketplace': 'automations',
'/automations/my-automations': 'automations',
'/automations/create': 'automations/docs',
'/site-editor': 'site-builder',
'/gallery': 'site-builder',
'/settings': 'settings/general',
'/settings/general': 'settings/general',
'/settings/resource-types': 'settings/resource-types',
'/settings/booking': 'settings/booking',
'/settings/appearance': 'settings/appearance',
'/settings/branding': 'settings/appearance',
'/settings/business-hours': 'settings/business-hours',
'/settings/email': 'settings/email',
'/settings/email-templates': 'settings/email-templates',
'/settings/embed-widget': 'settings/embed-widget',
'/settings/staff-roles': 'settings/staff-roles',
'/settings/sms-calling': 'settings/communication',
'/settings/domains': 'settings/domains',
'/settings/api': 'settings/api',
'/settings/auth': 'settings/auth',
'/settings/billing': 'settings/billing',
'/settings/quota': 'settings/quota',
};
const HelpButton: React.FC<HelpButtonProps> = ({ helpPath, className = '' }) => {
const HelpButton: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
// Check if we're on a tenant dashboard route
const isOnDashboard = location.pathname.startsWith('/dashboard');
// Get the help path for the current route
const getHelpPath = (): string => {
// Determine the base help path based on context
const helpBase = isOnDashboard ? '/dashboard/help' : '/help';
// Get the route to look up (strip /dashboard prefix if present)
const lookupPath = isOnDashboard
? location.pathname.replace(/^\/dashboard/, '') || '/'
: location.pathname;
// Exact match first
if (routeToHelpSuffix[lookupPath]) {
return `${helpBase}/${routeToHelpSuffix[lookupPath]}`;
}
// Try matching with a prefix (for dynamic routes like /customers/:id)
const pathSegments = lookupPath.split('/').filter(Boolean);
if (pathSegments.length > 0) {
// Try progressively shorter paths
for (let i = pathSegments.length; i > 0; i--) {
const testPath = '/' + pathSegments.slice(0, i).join('/');
if (routeToHelpSuffix[testPath]) {
return `${helpBase}/${routeToHelpSuffix[testPath]}`;
}
}
}
// Default to the main help page
return helpBase;
};
const helpPath = getHelpPath();
// Don't show on help pages themselves
if (location.pathname.includes('/help')) {
return null;
}
return (
<Link
to={helpPath}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors ${className}`}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
title={t('common.help', 'Help')}
aria-label={t('common.help', 'Help')}
>
<HelpCircle size={18} />
<span className="hidden sm:inline">{t('common.help', 'Help')}</span>
<HelpCircle size={20} />
</Link>
);
};