Add staff permission controls for editing staff and customers

- Add can_edit_staff and can_edit_customers dangerous permissions
- Move Site Builder, Services, Locations, Time Blocks, Payments to Settings permissions
- Link Edit Others' Schedules and Edit Own Schedule permissions
- Add permission checks to StaffViewSet (partial_update, toggle_active, verify_email)
- Add permission checks to CustomerViewSet (update, partial_update, verify_email)
- Fix CustomerViewSet permission key mismatch (can_access_customers)
- Hide Edit/Verify buttons on Staff and Customers pages without permission
- Make dangerous permissions section more visually distinct (darker red)
- Fix StaffDashboard links to use correct paths (/dashboard/my-schedule)
- Disable settings sub-permissions when Access Settings is unchecked

🤖 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-29 17:38:48 -05:00
parent d7700a68fd
commit 47657e7076
105 changed files with 29709 additions and 873 deletions

View File

@@ -1,13 +1,16 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import LanguageSelector from '../LanguageSelector';
// Create mock function for changeLanguage
const mockChangeLanguage = vi.fn();
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
i18n: {
language: 'en',
changeLanguage: vi.fn(),
changeLanguage: mockChangeLanguage,
},
}),
}));
@@ -22,6 +25,10 @@ vi.mock('../../i18n', () => ({
}));
describe('LanguageSelector', () => {
beforeEach(() => {
mockChangeLanguage.mockClear();
});
describe('dropdown variant', () => {
it('renders dropdown button', () => {
render(<LanguageSelector />);
@@ -63,6 +70,71 @@ describe('LanguageSelector', () => {
const { container } = render(<LanguageSelector className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
});
it('changes language when clicking a language option in dropdown', () => {
render(<LanguageSelector />);
const button = screen.getByRole('button');
fireEvent.click(button);
const spanishOption = screen.getByText('Español').closest('button');
expect(spanishOption).toBeInTheDocument();
fireEvent.click(spanishOption!);
expect(mockChangeLanguage).toHaveBeenCalledWith('es');
});
it('closes dropdown when language is selected', () => {
render(<LanguageSelector />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(screen.getByRole('listbox')).toBeInTheDocument();
const frenchOption = screen.getByText('Français').closest('button');
fireEvent.click(frenchOption!);
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
it('closes dropdown when clicking outside', () => {
render(<LanguageSelector />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(screen.getByRole('listbox')).toBeInTheDocument();
// Click outside the dropdown
fireEvent.mouseDown(document.body);
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
it('does not close dropdown when clicking inside dropdown', () => {
render(<LanguageSelector />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(screen.getByRole('listbox')).toBeInTheDocument();
const listbox = screen.getByRole('listbox');
fireEvent.mouseDown(listbox);
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
it('toggles dropdown open/closed on button clicks', () => {
render(<LanguageSelector />);
const button = screen.getByRole('button');
// Open dropdown
fireEvent.click(button);
expect(screen.getByRole('listbox')).toBeInTheDocument();
// Close dropdown
fireEvent.click(button);
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
});
describe('inline variant', () => {
@@ -89,5 +161,51 @@ describe('LanguageSelector', () => {
render(<LanguageSelector variant="inline" />);
expect(screen.getByText(/🇺🇸/)).toBeInTheDocument();
});
it('changes language when clicking a language button', () => {
render(<LanguageSelector variant="inline" />);
const spanishButton = screen.getByText(/Español/).closest('button');
expect(spanishButton).toBeInTheDocument();
fireEvent.click(spanishButton!);
expect(mockChangeLanguage).toHaveBeenCalledWith('es');
});
it('calls changeLanguage with correct code for each language', () => {
render(<LanguageSelector variant="inline" />);
// Test English
const englishButton = screen.getByText(/English/).closest('button');
fireEvent.click(englishButton!);
expect(mockChangeLanguage).toHaveBeenCalledWith('en');
mockChangeLanguage.mockClear();
// Test French
const frenchButton = screen.getByText(/Français/).closest('button');
fireEvent.click(frenchButton!);
expect(mockChangeLanguage).toHaveBeenCalledWith('fr');
});
it('hides flags when showFlag is false', () => {
render(<LanguageSelector variant="inline" showFlag={false} />);
// Flags should not be visible
expect(screen.queryByText('🇺🇸')).not.toBeInTheDocument();
expect(screen.queryByText('🇪🇸')).not.toBeInTheDocument();
expect(screen.queryByText('🇫🇷')).not.toBeInTheDocument();
// But names should still be there
expect(screen.getByText('English')).toBeInTheDocument();
expect(screen.getByText('Español')).toBeInTheDocument();
expect(screen.getByText('Français')).toBeInTheDocument();
});
it('applies custom className', () => {
const { container } = render(<LanguageSelector variant="inline" className="custom-inline-class" />);
expect(container.firstChild).toHaveClass('custom-inline-class');
});
});
});