fix(frontend): restore member self-assignment for shifts and tasks
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 1m12s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 35s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
CI Pipeline / Backend Build & Test (pull_request) Successful in 52s
CI Pipeline / Frontend Lint, Test & Build (pull_request) Successful in 33s
CI Pipeline / Infrastructure Validation (pull_request) Successful in 4s

Root Cause:
- Shift: Next.js 16.1.6 incompatible rewrite pattern caused runtime SyntaxError
- Task: Missing self-assignment UI for member role

Fix:
- Updated next.config.ts rewrite pattern from regex to wildcard syntax
- Added "Assign to Me" button to task detail page with useSession integration
- Added test coverage for self-assignment behavior with session mocks

Testing:
- Lint:  PASS (ESLint v9)
- Tests:  47/47 PASS (Vitest v4.0.18)
- Build:  PASS (Next.js 16.1.6, 12 routes)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
WorkClub Automation
2026-03-08 19:07:19 +01:00
parent 785502f113
commit add4c4c627
5 changed files with 159 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import { useTask, useUpdateTask } from '@/hooks/useTasks';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useSession } from 'next-auth/react';
const VALID_TRANSITIONS: Record<string, string[]> = {
Open: ['Assigned'],
@@ -26,6 +27,7 @@ export default function TaskDetailPage({ params }: { params: Promise<{ id: strin
const resolvedParams = use(params);
const { data: task, isLoading, error } = useTask(resolvedParams.id);
const { mutate: updateTask, isPending } = useUpdateTask();
const { data: session } = useSession();
if (isLoading) return <div className="p-8">Loading task...</div>;
if (error || !task) return <div className="p-8 text-red-500">Failed to load task.</div>;
@@ -36,6 +38,12 @@ export default function TaskDetailPage({ params }: { params: Promise<{ id: strin
updateTask({ id: task.id, data: { status: newStatus } });
};
const handleAssignToMe = () => {
if (session?.user?.id) {
updateTask({ id: task.id, data: { assigneeId: session.user.id } });
}
};
const getTransitionLabel = (status: string, newStatus: string) => {
if (status === 'Review' && newStatus === 'InProgress') return 'Back to InProgress';
if (newStatus === 'Done') return 'Mark as Done';
@@ -93,6 +101,15 @@ export default function TaskDetailPage({ params }: { params: Promise<{ id: strin
<div className="pt-6 border-t">
<h3 className="text-lg font-medium mb-4">Actions</h3>
<div className="flex flex-wrap gap-2">
{!task.assigneeId && session?.user && (
<Button
onClick={handleAssignToMe}
disabled={isPending}
variant="outline"
>
{isPending ? 'Assigning...' : 'Assign to Me'}
</Button>
)}
{validTransitions.map((nextStatus) => (
<Button
key={nextStatus}

View File

@@ -11,6 +11,13 @@ vi.mock('next/navigation', () => ({
})),
}));
vi.mock('next-auth/react', () => ({
useSession: vi.fn(() => ({
data: { user: { id: 'user-123' } },
status: 'authenticated',
})),
}));
vi.mock('@/hooks/useTasks', () => ({
useTask: vi.fn(),
useUpdateTask: vi.fn(),
@@ -74,4 +81,63 @@ describe('TaskDetailPage', () => {
expect(screen.getByText('Mark as Done')).toBeInTheDocument();
expect(screen.getByText('Back to InProgress')).toBeInTheDocument();
});
it('renders Assign to Me button when task unassigned and session exists', async () => {
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
data: {
id: '1',
title: 'Task 1',
status: 'Open',
assigneeId: null,
description: 'Desc',
createdAt: '2024-01-01',
updatedAt: '2024-01-01'
},
isLoading: false,
error: null,
});
const params = Promise.resolve({ id: '1' });
await act(async () => {
render(<TaskDetailPage params={params} />);
});
expect(screen.getByText('Assign to Me')).toBeInTheDocument();
});
it('calls updateTask with assigneeId when Assign to Me clicked', async () => {
const mockMutate = vi.fn();
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
data: {
id: '1',
title: 'Task 1',
status: 'Open',
assigneeId: null,
description: 'Desc',
createdAt: '2024-01-01',
updatedAt: '2024-01-01'
},
isLoading: false,
error: null,
});
(useUpdateTask as ReturnType<typeof vi.fn>).mockReturnValue({
mutate: mockMutate,
isPending: false,
});
const params = Promise.resolve({ id: '1' });
await act(async () => {
render(<TaskDetailPage params={params} />);
});
const button = screen.getByText('Assign to Me');
await act(async () => {
button.click();
});
expect(mockMutate).toHaveBeenCalledWith({
id: '1',
data: { assigneeId: 'user-123' },
});
});
});