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

@@ -39,6 +39,7 @@ vi.mock('../../components/TopBar', () => ({
TopBar - {user.name} - {isDarkMode ? 'Dark' : 'Light'}
<button onClick={toggleTheme} data-testid="theme-toggle">Toggle Theme</button>
<button onClick={onMenuClick} data-testid="menu-button">Menu</button>
<div data-testid="help-button">Help</div>
</div>
),
}));
@@ -99,9 +100,7 @@ vi.mock('../../components/TicketModal', () => ({
),
}));
vi.mock('../../components/FloatingHelpButton', () => ({
default: () => <div data-testid="floating-help-button">Help</div>,
}));
// HelpButton is now rendered inside TopBar, not as a separate component
// Mock hooks
vi.mock('../../hooks/useAuth', () => ({
@@ -224,7 +223,7 @@ describe('BusinessLayout', () => {
expect(screen.getAllByTestId('sidebar').length).toBeGreaterThan(0);
expect(screen.getByTestId('topbar')).toBeInTheDocument();
expect(screen.getByTestId('outlet')).toBeInTheDocument();
expect(screen.getByTestId('floating-help-button')).toBeInTheDocument();
expect(screen.getByTestId('help-button')).toBeInTheDocument();
});
it('should render children content via Outlet', () => {
@@ -649,7 +648,7 @@ describe('BusinessLayout', () => {
it('should render floating help button', () => {
renderLayout();
expect(screen.getByTestId('floating-help-button')).toBeInTheDocument();
expect(screen.getByTestId('help-button')).toBeInTheDocument();
});
});
@@ -795,7 +794,7 @@ describe('BusinessLayout', () => {
expect(screen.getAllByTestId('sidebar').length).toBeGreaterThan(0);
expect(screen.getByTestId('topbar')).toBeInTheDocument();
expect(screen.getByTestId('outlet')).toBeInTheDocument();
expect(screen.getByTestId('floating-help-button')).toBeInTheDocument();
expect(screen.getByTestId('help-button')).toBeInTheDocument();
});
});
});