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
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:
parent
28a0062015
commit
e5c7a35ebd
8 changed files with 811 additions and 98 deletions
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
121
mnemo_cards_admin/src/lib/error-utils.ts
Normal file
121
mnemo_cards_admin/src/lib/error-utils.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue