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

master
Guillermo Pages 2 weeks ago
parent 447b2125a8
commit c9f153c345

@ -18,6 +18,7 @@ import {
ListOrdered, ListOrdered,
FileText, FileText,
CalendarClock, CalendarClock,
Trash2,
} from 'lucide-react'; } from 'lucide-react';
import useTranslation from '@/src/hooks/useTranslation'; import useTranslation from '@/src/hooks/useTranslation';
import Card from '@/src/components/cards/Card'; import Card from '@/src/components/cards/Card';
@ -28,7 +29,9 @@ import {
useStartCompetition, useStartCompetition,
useFinishCompetition, useFinishCompetition,
useCancelCompetition, useCancelCompetition,
useDeleteCompetition,
} from '@/src/hooks/mutations/useCompetitionMutations'; } from '@/src/hooks/mutations/useCompetitionMutations';
import { useRouter } from 'next/navigation';
import OverviewTab from './tabs/OverviewTab'; import OverviewTab from './tabs/OverviewTab';
import RegistrationsTab from './tabs/RegistrationsTab'; import RegistrationsTab from './tabs/RegistrationsTab';
import ParticipantsTab from './tabs/ParticipantsTab'; import ParticipantsTab from './tabs/ParticipantsTab';
@ -71,20 +74,76 @@ function formatDate(dateString: string): string {
function StatusTransitionButton({ function StatusTransitionButton({
status, status,
competitionId, competitionId,
facilityId,
locale,
}: { }: {
status: CompetitionStatus; status: CompetitionStatus;
competitionId: number; competitionId: number;
facilityId: number;
locale: string;
}) { }) {
const router = useRouter();
const publishMutation = usePublishCompetition(competitionId); const publishMutation = usePublishCompetition(competitionId);
const startMutation = useStartCompetition(competitionId); const startMutation = useStartCompetition(competitionId);
const finishMutation = useFinishCompetition(competitionId); const finishMutation = useFinishCompetition(competitionId);
const cancelMutation = useCancelCompetition(competitionId); const cancelMutation = useCancelCompetition(competitionId);
const deleteMutation = useDeleteCompetition(competitionId);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const isPending = const isPending =
publishMutation.isPending || publishMutation.isPending ||
startMutation.isPending || startMutation.isPending ||
finishMutation.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') { if (status === 'draft') {
return ( return (
@ -101,6 +160,7 @@ function StatusTransitionButton({
)} )}
Publish Publish
</button> </button>
<DeleteButton />
</div> </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; return null;
} }
@ -234,7 +303,7 @@ export default function CompetitionDetailComponent({
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
Save as Template Save as Template
</button> </button>
<StatusTransitionButton status={competition.status} competitionId={competitionId} /> <StatusTransitionButton status={competition.status} competitionId={competitionId} facilityId={facilityId} locale={locale} />
</div> </div>
</div> </div>

@ -8,6 +8,7 @@ import {
startCompetition, startCompetition,
finishCompetition, finishCompetition,
cancelCompetition, cancelCompetition,
deleteCompetition,
saveCompetitionAsTemplate, saveCompetitionAsTemplate,
approveRegistration, approveRegistration,
rejectRegistration, 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 * 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 * POST /competitions/{competition_id}/save-as-template
* Create a template from an existing competition's config * Create a template from an existing competition's config

Loading…
Cancel
Save