errors
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-14 23:19:39 +03:00
parent 28a0062015
commit e5c7a35ebd
8 changed files with 811 additions and 98 deletions

View file

@ -1,5 +1,18 @@
import { adminApiClient } from './client' import { adminApiClient } from './client'
import type { GameCardDto, PaginatedResponse } from '@/types/models' import type { GameCardDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
export class CardsApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public field?: string,
public originalError?: unknown
) {
super(message)
this.name = 'CardsApiError'
}
}
export const cardsApi = { export const cardsApi = {
// Get all cards with pagination and search // Get all cards with pagination and search
@ -8,31 +21,91 @@ export const cardsApi = {
limit?: number limit?: number
search?: string search?: string
}): Promise<PaginatedResponse<GameCardDto>> => { }): Promise<PaginatedResponse<GameCardDto>> => {
const response = await adminApiClient.get('/api/v2/admin/cards', { try {
params: { const response = await adminApiClient.get('/api/v2/admin/cards', {
page: params?.page || 1, params: {
limit: params?.limit || 20, page: params?.page || 1,
search: params?.search, limit: params?.limit || 20,
}, search: params?.search,
}) },
return response.data })
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string }>
throw new CardsApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
'Failed to load cards',
axiosError.response?.status,
axiosError.response?.data?.field,
error
)
}
}, },
// Get a specific card by ID // Get a specific card by ID
getCard: async (cardId: number): Promise<GameCardDto> => { getCard: async (cardId: number): Promise<GameCardDto> => {
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`) try {
return response.data const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string }>
throw new CardsApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to load card ${cardId}`,
axiosError.response?.status,
undefined,
error
)
}
}, },
// Create or update a card // Create or update a card
upsertCard: async (card: GameCardDto): Promise<{ success: boolean; card: GameCardDto }> => { upsertCard: async (card: GameCardDto): Promise<{ success: boolean; card: GameCardDto }> => {
const response = await adminApiClient.post('/api/v2/admin/cards', card) try {
return response.data const response = await adminApiClient.post('/api/v2/admin/cards', card)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }>
const isUpdate = card.id && card.id > 0
const operation = isUpdate ? 'update card' : 'create card'
const message = axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to ${operation}`
let fullMessage = message
if (axiosError.response?.data?.details) {
fullMessage += `. ${axiosError.response.data.details}`
}
if (axiosError.response?.data?.field) {
fullMessage += ` (Field: ${axiosError.response.data.field})`
}
throw new CardsApiError(
fullMessage,
axiosError.response?.status,
axiosError.response?.data?.field,
error
)
}
}, },
// Delete a card by ID // Delete a card by ID
deleteCard: async (cardId: number): Promise<{ success: boolean; message: string }> => { deleteCard: async (cardId: number): Promise<{ success: boolean; message: string }> => {
const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`) try {
return response.data const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string }>
throw new CardsApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to delete card ${cardId}`,
axiosError.response?.status,
undefined,
error
)
}
}, },
} }

View file

@ -1,5 +1,18 @@
import { adminApiClient } from './client' import { adminApiClient } from './client'
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models' import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
export class PacksApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public field?: string,
public originalError?: unknown
) {
super(message)
this.name = 'PacksApiError'
}
}
export const packsApi = { export const packsApi = {
// Get all packs with pagination and search // Get all packs with pagination and search
@ -9,32 +22,132 @@ export const packsApi = {
search?: string search?: string
showDisabled?: boolean showDisabled?: boolean
}): Promise<PaginatedResponse<CardPackPreviewDto>> => { }): Promise<PaginatedResponse<CardPackPreviewDto>> => {
const response = await adminApiClient.get('/api/v2/admin/packs', { try {
params: { const response = await adminApiClient.get('/api/v2/admin/packs', {
page: params?.page || 1, params: {
limit: params?.limit || 20, page: params?.page || 1,
search: params?.search, limit: params?.limit || 20,
showDisabled: params?.showDisabled, search: params?.search,
}, showDisabled: params?.showDisabled,
}) },
return response.data })
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string }>
// Check for validation errors (limit too high, etc.)
if (axiosError.response?.status === 400) {
const errorData = axiosError.response.data
if (errorData?.message?.includes('Limit')) {
throw new PacksApiError(
`Invalid limit: ${errorData.message}. Maximum allowed limit is 100.`,
axiosError.response.status,
'limit',
error
)
}
}
throw new PacksApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
'Failed to load packs',
axiosError.response?.status,
axiosError.response?.data?.field,
error
)
}
}, },
// Get pack details by ID for editing // Get pack details by ID for editing
getPack: async (packId: string): Promise<EditCardPackDto> => { getPack: async (packId: string): Promise<EditCardPackDto> => {
const response = await adminApiClient.get(`/api/v2/admin/packs/${packId}`) try {
return response.data const response = await adminApiClient.get(`/api/v2/admin/packs/${packId}`)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string }>
if (axiosError.response?.status === 404) {
throw new PacksApiError(
`Pack not found: The pack with ID "${packId}" does not exist or has been deleted.`,
axiosError.response.status,
undefined,
error
)
}
throw new PacksApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to load pack ${packId}`,
axiosError.response?.status,
undefined,
error
)
}
}, },
// Create or update pack // Create or update pack
upsertPack: async (pack: EditCardPackDto): Promise<{ success: boolean; pack: EditCardPackDto }> => { upsertPack: async (pack: EditCardPackDto): Promise<{ success: boolean; pack: EditCardPackDto }> => {
const response = await adminApiClient.post('/api/v2/admin/packs', pack) try {
return response.data const response = await adminApiClient.post('/api/v2/admin/packs', pack)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }>
const isUpdate = pack.id && pack.id.length > 0
const operation = isUpdate ? 'update pack' : 'create pack'
const baseMessage = axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to ${operation}`
let fullMessage = baseMessage
if (axiosError.response?.data?.details) {
fullMessage += `. ${axiosError.response.data.details}`
}
// Special handling for common errors
if (axiosError.response?.status === 404 && isUpdate) {
fullMessage = `Pack not found: The pack you're trying to update (ID: ${pack.id}) does not exist.`
}
if (axiosError.response?.data?.field) {
fullMessage += ` (Field: ${axiosError.response.data.field})`
}
throw new PacksApiError(
fullMessage,
axiosError.response?.status,
axiosError.response?.data?.field,
error
)
}
}, },
// Delete pack // Delete pack
deletePack: async (packId: string): Promise<{ success: boolean; message: string }> => { deletePack: async (packId: string): Promise<{ success: boolean; message: string }> => {
const response = await adminApiClient.delete(`/api/v2/admin/packs/${packId}`) try {
return response.data const response = await adminApiClient.delete(`/api/v2/admin/packs/${packId}`)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ error?: string; message?: string }>
if (axiosError.response?.status === 404) {
throw new PacksApiError(
`Pack not found: The pack with ID "${packId}" does not exist and cannot be deleted.`,
axiosError.response.status,
undefined,
error
)
}
throw new PacksApiError(
axiosError.response?.data?.message ||
axiosError.response?.data?.error ||
`Failed to delete pack ${packId}`,
axiosError.response?.status,
undefined,
error
)
}
}, },
} }

View file

@ -1,16 +1,16 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner' import { toast } from 'sonner'
import { cardsApi } from '@/api/cards' import { cardsApi, CardsApiError } from '@/api/cards'
import { packsApi, PacksApiError } from '@/api/packs'
import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils'
import type { GameCardDto } from '@/types/models' import type { GameCardDto } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from './ui/button' import { Button } from './ui/button'
import { Input } from './ui/input' import { Input } from './ui/input'
import { Label } from './ui/label' import { Label } from './ui/label'
import { Textarea } from './ui/textarea' import { Textarea } from './ui/textarea'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
import { Badge } from './ui/badge' import { Badge } from './ui/badge'
import { packsApi } from '@/api/packs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
import { ChevronLeft, ChevronRight, Save, X } from 'lucide-react' import { ChevronLeft, ChevronRight, Save, X } from 'lucide-react'
@ -69,8 +69,11 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
} }
}) })
.catch((error) => { .catch((error) => {
const errorMessage = error instanceof PacksApiError
? error.message
: formatApiError(error)
console.error('Failed to load packs:', error) console.error('Failed to load packs:', error)
toast.error('Failed to load packs') toast.error(`Failed to load packs: ${errorMessage}`)
// Initialize with empty array so component doesn't crash // Initialize with empty array so component doesn't crash
setPacksData([]) setPacksData([])
}) })
@ -123,8 +126,11 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
toast.success('Card saved successfully') toast.success('Card saved successfully')
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof CardsApiError
toast.error(axiosError.response?.data?.message || 'Failed to save card') ? error.message
: getDetailedErrorMessage(error, 'save', 'card')
toast.error(errorMessage)
console.error('Error saving card:', error)
}, },
}) })

View file

@ -0,0 +1,121 @@
import type { AxiosError } from 'axios'
export interface ApiErrorResponse {
error?: string
message?: string
details?: string
field?: string
code?: string
}
/**
* Formats an API error into a user-friendly message
*/
export function formatApiError(error: unknown): string {
const axiosError = error as AxiosError<ApiErrorResponse>
// Network error (no response from server)
if (!axiosError.response) {
if (axiosError.code === 'ECONNABORTED') {
return 'Request timeout: The server took too long to respond. Please check your connection and try again.'
}
if (axiosError.message?.includes('Network Error')) {
return 'Network error: Unable to connect to the server. Please check your internet connection.'
}
return 'Connection error: Unable to reach the server. Please check your connection and try again.'
}
const status = axiosError.response.status
const data = axiosError.response.data
// Get error message from response
let message = data?.message || data?.error || 'An unknown error occurred'
// Add more context based on status code
switch (status) {
case 400:
if (data?.field) {
return `Validation error: ${message} (field: ${data.field})`
}
if (data?.details) {
return `Bad request: ${message}. ${data.details}`
}
return `Invalid request: ${message}`
case 401:
return 'Authentication required: Please log in again.'
case 403:
return 'Access denied: You do not have permission to perform this action.'
case 404:
// Make 404 errors more specific based on the endpoint
const url = axiosError.config?.url || ''
if (url.includes('/cards/')) {
return 'Card not found: The requested card does not exist or has been deleted.'
}
if (url.includes('/packs/')) {
return 'Pack not found: The requested pack does not exist or has been deleted.'
}
if (url.includes('/users/')) {
return 'User not found: The requested user does not exist.'
}
return `Resource not found: ${message}`
case 409:
return `Conflict: ${message}. The resource may already exist or be in use.`
case 422:
if (data?.details) {
return `Validation failed: ${message}. ${data.details}`
}
return `Validation error: ${message}`
case 429:
return 'Too many requests: Please wait a moment and try again.'
case 500:
return `Server error: ${message}. Please try again later or contact support if the problem persists.`
case 502:
return 'Bad gateway: The server is temporarily unavailable. Please try again later.'
case 503:
return 'Service unavailable: The server is temporarily down for maintenance. Please try again later.'
case 504:
return 'Gateway timeout: The server took too long to respond. Please try again.'
default:
return `${message} (Error ${status})`
}
}
/**
* Gets a detailed error message with additional context for specific operations
*/
export function getDetailedErrorMessage(
error: unknown,
operation: string,
resource?: string
): string {
const baseMessage = formatApiError(error)
const axiosError = error as AxiosError<ApiErrorResponse>
// Add operation context
let contextMessage = baseMessage
if (operation && resource) {
contextMessage = `Failed to ${operation} ${resource}: ${baseMessage}`
} else if (operation) {
contextMessage = `Failed to ${operation}: ${baseMessage}`
}
// Add field-specific errors if available
if (axiosError.response?.data?.field) {
const field = axiosError.response.data.field
contextMessage += ` (Field: ${field})`
}
return contextMessage
}

View file

@ -1,9 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner' import { toast } from 'sonner'
import { cardsApi } from '@/api/cards' import { cardsApi, CardsApiError } from '@/api/cards'
import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils'
import type { GameCardDto, PaginatedResponse } from '@/types/models' import type { GameCardDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -79,6 +79,13 @@ export default function CardsPage() {
const { data, isLoading, error } = useQuery<PaginatedResponse<GameCardDto>>({ const { data, isLoading, error } = useQuery<PaginatedResponse<GameCardDto>>({
queryKey: ['cards', page, search], queryKey: ['cards', page, search],
queryFn: () => cardsApi.getCards({ page, limit, search }), queryFn: () => cardsApi.getCards({ page, limit, search }),
retry: (failureCount, error) => {
// Don't retry on client errors (4xx)
if (error instanceof CardsApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
return false
}
return failureCount < 2
},
}) })
// Mutations // Mutations
@ -90,8 +97,11 @@ export default function CardsPage() {
closeDialog() closeDialog()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof CardsApiError
toast.error(axiosError.response?.data?.message || 'Failed to create card') ? error.message
: getDetailedErrorMessage(error, 'create', 'card')
toast.error(errorMessage)
console.error('Error creating card:', error)
}, },
}) })
@ -103,8 +113,11 @@ export default function CardsPage() {
closeDialog() closeDialog()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof CardsApiError
toast.error(axiosError.response?.data?.message || 'Failed to update card') ? error.message
: getDetailedErrorMessage(error, 'update', 'card')
toast.error(errorMessage)
console.error('Error updating card:', error)
}, },
}) })
@ -117,8 +130,11 @@ export default function CardsPage() {
setCardToDelete(null) setCardToDelete(null)
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof CardsApiError
toast.error(axiosError.response?.data?.message || 'Failed to delete card') ? error.message
: getDetailedErrorMessage(error, 'delete', 'card')
toast.error(errorMessage)
console.error('Error deleting card:', error)
}, },
}) })
@ -210,6 +226,9 @@ export default function CardsPage() {
} }
if (error) { if (error) {
const errorMessage = error instanceof CardsApiError
? error.message
: formatApiError(error)
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
@ -218,7 +237,18 @@ export default function CardsPage() {
</div> </div>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-red-500">Failed to load cards. Please try again later.</p> <div className="space-y-2">
<p className="text-red-500 font-medium">Failed to load cards</p>
<p className="text-sm text-muted-foreground">{errorMessage}</p>
<Button
variant="outline"
size="sm"
onClick={() => queryClient.invalidateQueries({ queryKey: ['cards'] })}
className="mt-2"
>
Retry
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -1,9 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner' import { toast } from 'sonner'
import { packsApi } from '@/api/packs' import { packsApi, PacksApiError } from '@/api/packs'
import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils'
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models' import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -78,6 +78,13 @@ export default function PacksPage() {
const { data, isLoading, error } = useQuery<PaginatedResponse<CardPackPreviewDto>>({ const { data, isLoading, error } = useQuery<PaginatedResponse<CardPackPreviewDto>>({
queryKey: ['packs', page, search, showDisabled], queryKey: ['packs', page, search, showDisabled],
queryFn: () => packsApi.getPacks({ page, limit, search, showDisabled }), queryFn: () => packsApi.getPacks({ page, limit, search, showDisabled }),
retry: (failureCount, error) => {
// Don't retry on client errors (4xx)
if (error instanceof PacksApiError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
return false
}
return failureCount < 2
},
}) })
// Mutations // Mutations
@ -89,8 +96,11 @@ export default function PacksPage() {
closeDialog() closeDialog()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof PacksApiError
toast.error(axiosError.response?.data?.message || 'Failed to create pack') ? error.message
: getDetailedErrorMessage(error, 'create', 'pack')
toast.error(errorMessage)
console.error('Error creating pack:', error)
}, },
}) })
@ -102,8 +112,11 @@ export default function PacksPage() {
closeDialog() closeDialog()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof PacksApiError
toast.error(axiosError.response?.data?.message || 'Failed to update pack') ? error.message
: getDetailedErrorMessage(error, 'update', 'pack')
toast.error(errorMessage)
console.error('Error updating pack:', error)
}, },
}) })
@ -116,8 +129,11 @@ export default function PacksPage() {
setPackToDelete(null) setPackToDelete(null)
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }> const errorMessage = error instanceof PacksApiError
toast.error(axiosError.response?.data?.message || 'Failed to delete pack') ? error.message
: getDetailedErrorMessage(error, 'delete', 'pack')
toast.error(errorMessage)
console.error('Error deleting pack:', error)
}, },
}) })
@ -169,8 +185,12 @@ export default function PacksPage() {
setCardsToAdd([]) setCardsToAdd([])
setCardsToRemove([]) setCardsToRemove([])
setIsDialogOpen(true) setIsDialogOpen(true)
} catch { } catch (error) {
toast.error('Failed to load pack details') const errorMessage = error instanceof PacksApiError
? error.message
: getDetailedErrorMessage(error, 'load', `pack "${pack.id}"`)
toast.error(errorMessage)
console.error('Error loading pack details:', error)
} }
} }
@ -196,7 +216,8 @@ export default function PacksPage() {
} }
const packData: EditCardPackDto = { const packData: EditCardPackDto = {
id: selectedPack?.id || '', // Only include id for updates, not for new packs
...(selectedPack && { id: selectedPack.id }),
title: formData.title.trim(), title: formData.title.trim(),
subtitle: formData.subtitle.trim() || undefined, subtitle: formData.subtitle.trim() || undefined,
description: formData.description.trim() || undefined, description: formData.description.trim() || undefined,
@ -238,6 +259,9 @@ export default function PacksPage() {
} }
if (error) { if (error) {
const errorMessage = error instanceof PacksApiError
? error.message
: formatApiError(error)
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
@ -246,7 +270,18 @@ export default function PacksPage() {
</div> </div>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-red-500">Failed to load packs. Please try again later.</p> <div className="space-y-2">
<p className="text-red-500 font-medium">Failed to load packs</p>
<p className="text-sm text-muted-foreground">{errorMessage}</p>
<Button
variant="outline"
size="sm"
onClick={() => queryClient.invalidateQueries({ queryKey: ['packs'] })}
className="mt-2"
>
Retry
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -49,13 +49,23 @@ class AdminCardsApiV2 {
// Validate pagination // Validate pagination
if (page < 1) { if (page < 1) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Page must be greater than 0'}), body: json.encode({
'error': 'Invalid page parameter',
'message': 'Page must be greater than 0. Received: $page',
'field': 'page',
'details': 'Page numbers start from 1. Please provide a valid page number.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
if (limit < 1 || limit > 100) { if (limit < 1 || limit > 100) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Limit must be between 1 and 100'}), body: json.encode({
'error': 'Invalid limit parameter',
'message': 'Limit must be between 1 and 100. Received: $limit',
'field': 'limit',
'details': 'The maximum number of items per page is 100. Please use a value between 1 and 100.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -109,7 +119,11 @@ class AdminCardsApiV2 {
} catch (e, s) { } catch (e, s) {
print('Error in getAllCards: $e\n$s'); print('Error in getAllCards: $e\n$s');
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString()}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to retrieve cards',
'details': 'An unexpected error occurred while fetching cards. Please try again later or contact support if the problem persists.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -122,7 +136,12 @@ class AdminCardsApiV2 {
try { try {
if (cardId.isEmpty) { if (cardId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}), body: json.encode({
'error': 'Invalid card ID',
'message': 'Card ID cannot be empty',
'field': 'cardId',
'details': 'Please provide a valid card ID to retrieve card details.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -130,7 +149,11 @@ class AdminCardsApiV2 {
final card = await _db.packDao.getCardById(cardId); final card = await _db.packDao.getCardById(cardId);
if (card == null) { if (card == null) {
return Response.notFound( return Response.notFound(
json.encode({'error': 'Card not found'}), body: json.encode({
'error': 'Card not found',
'message': 'The requested card does not exist or has been deleted',
'details': 'Card with ID "$cardId" was not found in the database. Please verify the card ID and try again.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -152,9 +175,14 @@ class AdminCardsApiV2 {
}), }),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} catch (e) { } catch (e, s) {
print('Error in getCard: $e\n$s');
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString()}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to retrieve card',
'details': 'An unexpected error occurred while fetching card with ID "$cardId". Please try again later or contact support if the problem persists.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -168,13 +196,52 @@ class AdminCardsApiV2 {
final body = await request.readAsString(); final body = await request.readAsString();
final data = json.decode(body) as Map<String, dynamic>; final data = json.decode(body) as Map<String, dynamic>;
// Check if this is an update (has valid id) // Validate required fields for new cards
final cardIdParam = data['id']; final cardIdParam = data['id'];
if (cardIdParam != null && cardIdParam != -1) { final isUpdate = cardIdParam != null && cardIdParam != -1;
if (!isUpdate) {
// Validate required fields for new cards
if (data['original'] == null || (data['original'] as String).trim().isEmpty) {
return Response.badRequest(
body: json.encode({
'error': 'Validation error',
'message': 'Original text is required',
'field': 'original',
'details': 'Please provide the original word or phrase for the card.',
}),
headers: {'Content-Type': 'application/json'},
);
}
if (data['translation'] == null || (data['translation'] as String).trim().isEmpty) {
return Response.badRequest(
body: json.encode({
'error': 'Validation error',
'message': 'Translation is required',
'field': 'translation',
'details': 'Please provide the translation for the card.',
}),
headers: {'Content-Type': 'application/json'},
);
}
}
// Check if this is an update (has valid id)
if (isUpdate) {
final cardId = cardIdParam.toString(); final cardId = cardIdParam.toString();
final existing = await _db.packDao.getCardById(cardId); final existing = await _db.packDao.getCardById(cardId);
if (existing != null) { if (existing == null) {
// Update existing card return Response.notFound(
body: json.encode({
'error': 'Card not found',
'message': 'The card you are trying to update does not exist',
'details': 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.',
}),
headers: {'Content-Type': 'application/json'},
);
}
// Update existing card
final updated = existing.copyWith( final updated = existing.copyWith(
original: data['original'] ?? existing.original, original: data['original'] ?? existing.original,
translation: data['translation'] ?? existing.translation, translation: data['translation'] ?? existing.translation,
@ -226,7 +293,12 @@ class AdminCardsApiV2 {
final created = await _db.packDao.getCardById(cardId); final created = await _db.packDao.getCardById(cardId);
if (created == null) { if (created == null) {
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': 'Failed to retrieve created card', 'success': false}), body: json.encode({
'error': 'Database error',
'message': 'Failed to retrieve created card',
'details': 'Card was created successfully but could not be retrieved from the database. The card ID is: $cardId',
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -252,8 +324,26 @@ class AdminCardsApiV2 {
); );
} catch (e, s) { } catch (e, s) {
print('Error in createCard: $e\n$s'); print('Error in createCard: $e\n$s');
// Check for common database errors
final errorMessage = e.toString().toLowerCase();
String details = 'An unexpected error occurred while saving the card.';
if (errorMessage.contains('constraint') || errorMessage.contains('unique')) {
details = 'A card with similar data may already exist. Please check for duplicates.';
} else if (errorMessage.contains('foreign key') || errorMessage.contains('pack')) {
details = 'The specified pack ID may be invalid. Please verify that the pack exists.';
} else if (errorMessage.contains('null') || errorMessage.contains('required')) {
details = 'Required fields are missing or invalid. Please check all required fields are provided.';
}
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString(), 'success': false}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to save card',
'details': details,
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -266,7 +356,12 @@ class AdminCardsApiV2 {
try { try {
if (cardId.isEmpty) { if (cardId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}), body: json.encode({
'error': 'Invalid card ID',
'message': 'Card ID cannot be empty',
'field': 'cardId',
'details': 'Please provide a valid card ID to update the card.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -277,7 +372,11 @@ class AdminCardsApiV2 {
final existing = await _db.packDao.getCardById(cardId); final existing = await _db.packDao.getCardById(cardId);
if (existing == null) { if (existing == null) {
return Response.notFound( return Response.notFound(
json.encode({'error': 'Card not found'}), body: json.encode({
'error': 'Card not found',
'message': 'The card you are trying to update does not exist',
'details': 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -300,9 +399,15 @@ class AdminCardsApiV2 {
json.encode({'success': true}), json.encode({'success': true}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} catch (e) { } catch (e, s) {
print('Error in updateCard: $e\n$s');
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString(), 'success': false}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to update card',
'details': 'An unexpected error occurred while updating the card. Please try again later.',
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -315,7 +420,25 @@ class AdminCardsApiV2 {
try { try {
if (cardId.isEmpty) { if (cardId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}), body: json.encode({
'error': 'Invalid card ID',
'message': 'Card ID cannot be empty',
'field': 'cardId',
'details': 'Please provide a valid card ID to delete the card.',
}),
headers: {'Content-Type': 'application/json'},
);
}
// Check if card exists before attempting deletion
final existing = await _db.packDao.getCardById(cardId);
if (existing == null) {
return Response.notFound(
body: json.encode({
'error': 'Card not found',
'message': 'The card you are trying to delete does not exist',
'details': 'Card with ID "$cardId" was not found. It may have already been deleted.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -323,12 +446,29 @@ class AdminCardsApiV2 {
await _db.packDao.deleteCard(cardId); await _db.packDao.deleteCard(cardId);
return Response.ok( return Response.ok(
json.encode({'success': true}), body: json.encode({
'success': true,
'message': 'Card deleted successfully',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} catch (e) { } catch (e, s) {
print('Error in deleteCard: $e\n$s');
final errorMessage = e.toString().toLowerCase();
String details = 'An unexpected error occurred while deleting the card.';
if (errorMessage.contains('foreign key') || errorMessage.contains('constraint')) {
details = 'Cannot delete card: it is still referenced by one or more packs. Please remove the card from all packs first.';
}
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString(), 'success': false}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to delete card',
'details': details,
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -346,7 +486,25 @@ class AdminCardsApiV2 {
if (cardId.isEmpty) { if (cardId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}), body: json.encode({
'error': 'Invalid card ID',
'message': 'Card ID cannot be empty',
'field': 'cardId',
'details': 'Please provide a valid card ID to retrieve voices.',
}),
headers: {'Content-Type': 'application/json'},
);
}
// Verify card exists
final card = await _db.packDao.getCardById(cardId);
if (card == null) {
return Response.notFound(
body: json.encode({
'error': 'Card not found',
'message': 'The requested card does not exist',
'details': 'Card with ID "$cardId" was not found. Cannot retrieve voices for a non-existent card.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -368,7 +526,11 @@ class AdminCardsApiV2 {
} catch (e, s) { } catch (e, s) {
print('Error in getCardVoices: $e\n$s'); print('Error in getCardVoices: $e\n$s');
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString()}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to retrieve card voices',
'details': 'An unexpected error occurred while fetching voices for card "$cardId". Please try again later.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -386,7 +548,12 @@ class AdminCardsApiV2 {
if (cardId.isEmpty) { if (cardId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID'}), body: json.encode({
'error': 'Invalid card ID',
'message': 'Card ID cannot be empty',
'field': 'cardId',
'details': 'Please provide a valid card ID to add a voice.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -399,7 +566,12 @@ class AdminCardsApiV2 {
if (voiceUrl == null || voiceUrl.isEmpty) { if (voiceUrl == null || voiceUrl.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'voiceUrl is required'}), body: json.encode({
'error': 'Validation error',
'message': 'Voice URL is required',
'field': 'voiceUrl',
'details': 'Please provide a valid voice URL (base64 encoded audio data) to add a voice to the card.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -408,7 +580,11 @@ class AdminCardsApiV2 {
final card = await _db.packDao.getCardById(cardId); final card = await _db.packDao.getCardById(cardId);
if (card == null) { if (card == null) {
return Response.notFound( return Response.notFound(
json.encode({'error': 'Card not found'}), body: json.encode({
'error': 'Card not found',
'message': 'The card you are trying to add a voice to does not exist',
'details': 'Card with ID "$cardId" was not found. Cannot add voice to a non-existent card.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -442,8 +618,21 @@ class AdminCardsApiV2 {
); );
} catch (e, s) { } catch (e, s) {
print('Error in addCardVoice: $e\n$s'); print('Error in addCardVoice: $e\n$s');
final errorMessage = e.toString().toLowerCase();
String details = 'An unexpected error occurred while adding the voice to the card.';
if (errorMessage.contains('constraint') || errorMessage.contains('unique')) {
details = 'A voice with this URL may already exist for this card.';
}
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString(), 'success': false}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to add voice to card',
'details': details,
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -461,7 +650,25 @@ class AdminCardsApiV2 {
if (cardId.isEmpty || voiceId.isEmpty) { if (cardId.isEmpty || voiceId.isEmpty) {
return Response.badRequest( return Response.badRequest(
body: json.encode({'error': 'Invalid card ID or voice ID'}), body: json.encode({
'error': 'Invalid parameters',
'message': 'Card ID and Voice ID cannot be empty',
'field': cardId.isEmpty ? 'cardId' : 'voiceId',
'details': 'Please provide valid card ID and voice ID to remove the voice.',
}),
headers: {'Content-Type': 'application/json'},
);
}
// Verify voice exists
final voice = await _db.packDao.getVoiceById(voiceId);
if (voice == null) {
return Response.notFound(
body: json.encode({
'error': 'Voice not found',
'message': 'The voice you are trying to remove does not exist',
'details': 'Voice with ID "$voiceId" was not found. It may have already been deleted.',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }
@ -473,13 +680,21 @@ class AdminCardsApiV2 {
await _db.packDao.deleteVoice(voiceId); await _db.packDao.deleteVoice(voiceId);
return Response.ok( return Response.ok(
json.encode({'success': true}), body: json.encode({
'success': true,
'message': 'Voice removed successfully',
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} catch (e, s) { } catch (e, s) {
print('Error in removeCardVoice: $e\n$s'); print('Error in removeCardVoice: $e\n$s');
return Response.internalServerError( return Response.internalServerError(
body: json.encode({'error': e.toString(), 'success': false}), body: json.encode({
'error': 'Internal server error',
'message': 'Failed to remove voice from card',
'details': 'An unexpected error occurred while removing the voice. Please try again later.',
'success': false,
}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
} }

View file

@ -67,13 +67,23 @@ class AdminPacksApiV2 {
// Validate pagination // Validate pagination
if (page < 1) { if (page < 1) {
return _json( return _json(
{'error': 'Page must be greater than 0'}, {
'error': 'Invalid page parameter',
'message': 'Page must be greater than 0. Received: $page',
'field': 'page',
'details': 'Page numbers start from 1. Please provide a valid page number.',
},
statusCode: 400, statusCode: 400,
); );
} }
if (limit < 1 || limit > 100) { if (limit < 1 || limit > 100) {
return _json( return _json(
{'error': 'Limit must be between 1 and 100'}, {
'error': 'Invalid limit parameter',
'message': 'Limit must be between 1 and 100. Received: $limit',
'field': 'limit',
'details': 'The maximum number of items per page is 100. Please use a value between 1 and 100.',
},
statusCode: 400, statusCode: 400,
); );
} }
@ -131,6 +141,7 @@ class AdminPacksApiV2 {
{ {
'error': 'Internal server error', 'error': 'Internal server error',
'message': 'Failed to fetch packs', 'message': 'Failed to fetch packs',
'details': 'An unexpected error occurred while retrieving packs. Please try again later or contact support if the problem persists.',
}, },
statusCode: 500, statusCode: 500,
); );
@ -147,10 +158,26 @@ class AdminPacksApiV2 {
return auth; return auth;
} }
if (packId.isEmpty) {
return _json(
{
'error': 'Invalid pack ID',
'message': 'Pack ID cannot be empty',
'field': 'packId',
'details': 'Please provide a valid pack ID to retrieve pack details.',
},
statusCode: 400,
);
}
final pack = await _db.packDao.getPackById(packId); final pack = await _db.packDao.getPackById(packId);
if (pack == null) { if (pack == null) {
return _json( return _json(
{'error': 'Pack not found'}, {
'error': 'Pack not found',
'message': 'The requested pack does not exist or has been deleted',
'details': 'Pack with ID "$packId" was not found in the database. Please verify the pack ID and try again.',
},
statusCode: 404, statusCode: 404,
); );
} }
@ -195,6 +222,7 @@ class AdminPacksApiV2 {
{ {
'error': 'Internal server error', 'error': 'Internal server error',
'message': 'Failed to fetch pack', 'message': 'Failed to fetch pack',
'details': 'An unexpected error occurred while retrieving pack "$packId". Please try again later or contact support if the problem persists.',
}, },
statusCode: 500, statusCode: 500,
); );
@ -214,16 +242,33 @@ class AdminPacksApiV2 {
final body = await request.readAsString(); final body = await request.readAsString();
final data = json.decode(body) as Map<String, dynamic>; final data = json.decode(body) as Map<String, dynamic>;
// Validate required fields
if (data['title'] == null || (data['title'] as String? ?? '').trim().isEmpty) {
return _json(
{
'error': 'Validation error',
'message': 'Title is required',
'field': 'title',
'details': 'Please provide a title for the pack. The title is a required field.',
},
statusCode: 400,
);
}
final editDto = EditCardPackDto.fromJson(data); final editDto = EditCardPackDto.fromJson(data);
String packId; String packId;
if (editDto.id != null) { if (editDto.id != null && editDto.id!.isNotEmpty) {
// Update existing pack // Update existing pack
packId = editDto.id!; packId = editDto.id!;
final existing = await _db.packDao.getPackById(packId); final existing = await _db.packDao.getPackById(packId);
if (existing == null) { if (existing == null) {
return _json( return _json(
{'error': 'Pack not found'}, {
'error': 'Pack not found',
'message': 'The pack you are trying to update does not exist',
'details': 'Pack with ID "$packId" was not found. The pack may have been deleted or the ID may be incorrect.',
},
statusCode: 404, statusCode: 404,
); );
} }
@ -270,11 +315,41 @@ class AdminPacksApiV2 {
// Handle card associations if provided // Handle card associations if provided
if (editDto.addCardIds != null && editDto.addCardIds!.isNotEmpty) { if (editDto.addCardIds != null && editDto.addCardIds!.isNotEmpty) {
for (final cardId in editDto.addCardIds!) { try {
await _db.packDao.addCardToPack( for (final cardId in editDto.addCardIds!) {
packId: packId, // Verify card exists before adding
cardId: cardId, final card = await _db.packDao.getCardById(cardId);
); if (card == null) {
return _json(
{
'error': 'Card not found',
'message': 'One of the cards you are trying to add does not exist',
'field': 'addCardIds',
'details': 'Card with ID "$cardId" was not found. Please verify all card IDs before adding them to the pack.',
},
statusCode: 404,
);
}
await _db.packDao.addCardToPack(
packId: packId,
cardId: cardId,
);
}
} catch (e) {
// Handle duplicate or constraint errors
if (e.toString().toLowerCase().contains('unique') ||
e.toString().toLowerCase().contains('constraint')) {
return _json(
{
'error': 'Duplicate card',
'message': 'One or more cards are already in this pack',
'field': 'addCardIds',
'details': 'Some cards you are trying to add are already associated with this pack. Please remove duplicates and try again.',
},
statusCode: 409,
);
}
rethrow;
} }
} }
@ -300,7 +375,11 @@ class AdminPacksApiV2 {
final updatedPack = await _db.packDao.getPackById(packId); final updatedPack = await _db.packDao.getPackById(packId);
if (updatedPack == null) { if (updatedPack == null) {
return _json( return _json(
{'error': 'Failed to retrieve updated pack'}, {
'error': 'Database error',
'message': 'Failed to retrieve updated pack',
'details': 'Pack was saved successfully but could not be retrieved from the database. The pack ID is: $packId. Please try refreshing or fetching the pack again.',
},
statusCode: 500, statusCode: 500,
); );
} }
@ -339,10 +418,26 @@ class AdminPacksApiV2 {
}); });
} catch (e, s) { } catch (e, s) {
print('Error in upsertPack: $e\n$s'); print('Error in upsertPack: $e\n$s');
// Check for common database errors
final errorMessage = e.toString().toLowerCase();
String details = 'An unexpected error occurred while saving the pack.';
if (errorMessage.contains('constraint') || errorMessage.contains('unique')) {
details = 'A pack with similar data may already exist, or there is a constraint violation. Please check for duplicates or conflicting data.';
} else if (errorMessage.contains('foreign key')) {
details = 'One of the referenced resources (cards, tests, etc.) does not exist. Please verify all IDs are correct.';
} else if (errorMessage.contains('null') || errorMessage.contains('required')) {
details = 'Required fields are missing or invalid. Please check all required fields are provided.';
} else if (errorMessage.contains('json')) {
details = 'Invalid JSON format in the request. Please check the request body structure.';
}
return _json( return _json(
{ {
'error': 'Internal server error', 'error': 'Internal server error',
'message': 'Failed to save pack', 'message': 'Failed to save pack',
'details': details,
}, },
statusCode: 500, statusCode: 500,
); );
@ -359,10 +454,26 @@ class AdminPacksApiV2 {
return auth; return auth;
} }
if (packId.isEmpty) {
return _json(
{
'error': 'Invalid pack ID',
'message': 'Pack ID cannot be empty',
'field': 'packId',
'details': 'Please provide a valid pack ID to delete the pack.',
},
statusCode: 400,
);
}
final pack = await _db.packDao.getPackById(packId); final pack = await _db.packDao.getPackById(packId);
if (pack == null) { if (pack == null) {
return _json( return _json(
{'error': 'Pack not found'}, {
'error': 'Pack not found',
'message': 'The pack you are trying to delete does not exist',
'details': 'Pack with ID "$packId" was not found. It may have already been deleted or the ID may be incorrect.',
},
statusCode: 404, statusCode: 404,
); );
} }
@ -375,10 +486,19 @@ class AdminPacksApiV2 {
}); });
} catch (e, s) { } catch (e, s) {
print('Error in deletePack: $e\n$s'); print('Error in deletePack: $e\n$s');
final errorMessage = e.toString().toLowerCase();
String details = 'An unexpected error occurred while deleting the pack.';
if (errorMessage.contains('foreign key') || errorMessage.contains('constraint')) {
details = 'Cannot delete pack: it is still referenced by users or other resources. Please remove all references first.';
}
return _json( return _json(
{ {
'error': 'Internal server error', 'error': 'Internal server error',
'message': 'Failed to delete pack', 'message': 'Failed to delete pack',
'details': details,
}, },
statusCode: 500, statusCode: 500,
); );