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:
@@ -0,0 +1,228 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { DraggableEvent } from '../DraggableEvent';
|
||||
|
||||
// Mock DnD Kit
|
||||
vi.mock('@dnd-kit/core', () => ({
|
||||
DndContext: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
useDraggable: vi.fn(() => ({
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: vi.fn(),
|
||||
transform: null,
|
||||
isDragging: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@dnd-kit/utilities', () => ({
|
||||
CSS: {
|
||||
Translate: {
|
||||
toString: (transform: any) => transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DraggableEvent', () => {
|
||||
const defaultProps = {
|
||||
id: 1,
|
||||
title: 'Test Event',
|
||||
serviceName: 'Test Service',
|
||||
status: 'CONFIRMED' as const,
|
||||
isPaid: false,
|
||||
start: new Date('2024-01-01T10:00:00'),
|
||||
end: new Date('2024-01-01T11:00:00'),
|
||||
laneIndex: 0,
|
||||
height: 80,
|
||||
left: 100,
|
||||
width: 200,
|
||||
top: 10,
|
||||
onResizeStart: vi.fn(),
|
||||
};
|
||||
|
||||
it('renders the event title', () => {
|
||||
render(<DraggableEvent {...defaultProps} />);
|
||||
expect(screen.getByText('Test Event')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the service name when provided', () => {
|
||||
render(<DraggableEvent {...defaultProps} />);
|
||||
expect(screen.getByText('Test Service')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render service name when not provided', () => {
|
||||
render(<DraggableEvent {...defaultProps} serviceName={undefined} />);
|
||||
expect(screen.queryByText('Test Service')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the start time formatted correctly', () => {
|
||||
render(<DraggableEvent {...defaultProps} />);
|
||||
expect(screen.getByText('10:00 AM')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies correct position styles', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
|
||||
expect(eventElement).toHaveStyle({
|
||||
left: '100px',
|
||||
width: '200px',
|
||||
top: '10px',
|
||||
height: '80px',
|
||||
});
|
||||
});
|
||||
|
||||
it('applies confirmed status border color', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} status="CONFIRMED" />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-blue-500');
|
||||
});
|
||||
|
||||
it('applies completed status border color', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} status="COMPLETED" />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-green-500');
|
||||
});
|
||||
|
||||
it('applies cancelled status border color', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} status="CANCELLED" />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-red-500');
|
||||
});
|
||||
|
||||
it('applies no-show status border color', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} status="NO_SHOW" />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-gray-500');
|
||||
});
|
||||
|
||||
it('applies green border when paid', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} isPaid={true} />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-green-500');
|
||||
});
|
||||
|
||||
it('applies default brand border color for scheduled status', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} status="SCHEDULED" />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('border-brand-500');
|
||||
});
|
||||
|
||||
it('calls onResizeStart when top resize handle is clicked', () => {
|
||||
const onResizeStart = vi.fn();
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
|
||||
);
|
||||
|
||||
const topHandle = container.querySelector('.cursor-ns-resize');
|
||||
if (topHandle) {
|
||||
fireEvent.mouseDown(topHandle);
|
||||
expect(onResizeStart).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'left',
|
||||
1
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls onResizeStart when bottom resize handle is clicked', () => {
|
||||
const onResizeStart = vi.fn();
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
|
||||
);
|
||||
|
||||
const handles = container.querySelectorAll('.cursor-ns-resize');
|
||||
const bottomHandle = handles[handles.length - 1]; // Get the last one (bottom)
|
||||
|
||||
if (bottomHandle) {
|
||||
fireEvent.mouseDown(bottomHandle);
|
||||
expect(onResizeStart).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'right',
|
||||
1
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders grip icon', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const gripIcon = container.querySelector('svg');
|
||||
expect(gripIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies hover styles', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass('group', 'hover:shadow-md');
|
||||
});
|
||||
|
||||
it('renders with correct base styling classes', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveClass(
|
||||
'absolute',
|
||||
'rounded-b',
|
||||
'overflow-hidden',
|
||||
'group',
|
||||
'bg-brand-100'
|
||||
);
|
||||
});
|
||||
|
||||
it('has two resize handles', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const handles = container.querySelectorAll('.cursor-ns-resize');
|
||||
expect(handles).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('stops propagation when resize handle is clicked', () => {
|
||||
const onResizeStart = vi.fn();
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} onResizeStart={onResizeStart} />
|
||||
);
|
||||
|
||||
const topHandle = container.querySelector('.cursor-ns-resize');
|
||||
const mockEvent = {
|
||||
stopPropagation: vi.fn(),
|
||||
} as any;
|
||||
|
||||
if (topHandle) {
|
||||
fireEvent.mouseDown(topHandle, mockEvent);
|
||||
// The event handler should call stopPropagation to prevent drag
|
||||
expect(onResizeStart).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders content area with cursor-move', () => {
|
||||
const { container } = render(<DraggableEvent {...defaultProps} />);
|
||||
const contentArea = container.querySelector('.cursor-move');
|
||||
expect(contentArea).toBeInTheDocument();
|
||||
expect(contentArea).toHaveClass('select-none');
|
||||
});
|
||||
|
||||
it('applies different heights correctly', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} height={100} />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveStyle({ height: '100px' });
|
||||
});
|
||||
|
||||
it('applies different widths correctly', () => {
|
||||
const { container } = render(
|
||||
<DraggableEvent {...defaultProps} width={300} />
|
||||
);
|
||||
const eventElement = container.querySelector('.absolute.rounded-b');
|
||||
expect(eventElement).toHaveStyle({ width: '300px' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user