Add DELETE endpoint for competitions (draft/cancelled only)
continuous-integration/drone/push Build is passing Details

master
Guillermo Pages 1 week ago
parent 447b2125a8
commit c9f153c345

@ -18,6 +18,7 @@ import {
ListOrdered,
FileText,
CalendarClock,
Trash2,
} from 'lucide-react';
import useTranslation from '@/src/hooks/useTranslation';
import Card from '@/src/components/cards/Card';
@ -28,7 +29,9 @@ import {
useStartCompetition,
useFinishCompetition,
useCancelCompetition,
useDeleteCompetition,
} from '@/src/hooks/mutations/useCompetitionMutations';
import { useRouter } from 'next/navigation';
import OverviewTab from './tabs/OverviewTab';
import RegistrationsTab from './tabs/RegistrationsTab';
import ParticipantsTab from './tabs/ParticipantsTab';
@ -71,20 +74,76 @@ function formatDate(dateString: string): string {
function StatusTransitionButton({
status,
competitionId,
facilityId,
locale,
}: {
status: CompetitionStatus;
competitionId: number;
facilityId: number;
locale: string;
}) {
const router = useRouter();
const publishMutation = usePublishCompetition(competitionId);
const startMutation = useStartCompetition(competitionId);
const finishMutation = useFinishCompetition(competitionId);
const cancelMutation = useCancelCompetition(competitionId);
const deleteMutation = useDeleteCompetition(competitionId);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const isPending =
publishMutation.isPending ||
startMutation.isPending ||
finishMutation.isPending ||
cancelMutation.isPending;
cancelMutation.isPending ||
deleteMutation.isPending;
const canDelete = status === 'draft' || status === 'cancelled';
const handleDelete = () => {
deleteMutation.mutate(undefined, {
onSuccess: () => {
router.push(`/${locale}/admin/facilities/${facilityId}/competitions`);
},
});
};
const DeleteButton = () => (
<>
{showDeleteConfirm ? (
<div className="flex items-center gap-2">
<span className="text-sm text-red-700">Delete permanently?</span>
<button
onClick={handleDelete}
disabled={isPending}
className="inline-flex items-center px-3 py-1.5 bg-red-600 text-white text-sm font-semibold rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
>
{deleteMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Yes, delete'
)}
</button>
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={isPending}
className="inline-flex items-center px-3 py-1.5 bg-slate-100 text-slate-700 text-sm font-semibold rounded-lg hover:bg-slate-200 transition-colors disabled:opacity-50"
>
No
</button>
</div>
) : (
<button
onClick={() => setShowDeleteConfirm(true)}
disabled={isPending}
className="inline-flex items-center px-4 py-2 bg-red-100 text-red-700 font-semibold rounded-xl hover:bg-red-200 transition-colors disabled:opacity-50"
title="Delete competition permanently"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</button>
)}
</>
);
if (status === 'draft') {
return (
@ -101,6 +160,7 @@ function StatusTransitionButton({
)}
Publish
</button>
<DeleteButton />
</div>
);
}
@ -155,6 +215,15 @@ function StatusTransitionButton({
);
}
// For cancelled status, show delete button
if (canDelete) {
return (
<div className="flex gap-2">
<DeleteButton />
</div>
);
}
return null;
}
@ -234,7 +303,7 @@ export default function CompetitionDetailComponent({
<FileText className="w-4 h-4 mr-2" />
Save as Template
</button>
<StatusTransitionButton status={competition.status} competitionId={competitionId} />
<StatusTransitionButton status={competition.status} competitionId={competitionId} facilityId={facilityId} locale={locale} />
</div>
</div>

@ -8,6 +8,7 @@ import {
startCompetition,
finishCompetition,
cancelCompetition,
deleteCompetition,
saveCompetitionAsTemplate,
approveRegistration,
rejectRegistration,
@ -237,6 +238,30 @@ export function useCancelCompetition(competitionId: number) {
});
}
/**
* Delete a competition (draft or cancelled only)
*/
export function useDeleteCompetition(competitionId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const result = await deleteCompetition(competitionId);
if (!result.success) {
throw new Error(result.error.detail);
}
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: competitionQueryKeys.lists() });
// Remove the detail query since the competition no longer exists
queryClient.removeQueries({
queryKey: competitionQueryKeys.detail(competitionId),
});
},
});
}
/**
* Save competition as template
*/

@ -412,6 +412,25 @@ export async function cancelCompetition(
}
}
/**
* DELETE /competitions/{competition_id}
* Permanently delete a competition (draft or cancelled only)
*/
export async function deleteCompetition(
competitionId: number
): Promise<CompetitionApiResult<void>> {
try {
const response = await apiFetch(`/competitions/${competitionId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
return handleApiResponse<void>(response);
} catch (error) {
return buildNetworkError(error instanceof Error ? error.message : 'Failed to delete competition');
}
}
/**
* POST /competitions/{competition_id}/save-as-template
* Create a template from an existing competition's config

Loading…
Cancel
Save