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 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 = {
|
||||
// Get all cards with pagination and search
|
||||
|
|
@ -8,31 +21,91 @@ export const cardsApi = {
|
|||
limit?: number
|
||||
search?: string
|
||||
}): Promise<PaginatedResponse<GameCardDto>> => {
|
||||
const response = await adminApiClient.get('/api/v2/admin/cards', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
search: params?.search,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
try {
|
||||
const response = await adminApiClient.get('/api/v2/admin/cards', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
search: params?.search,
|
||||
},
|
||||
})
|
||||
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
|
||||
getCard: async (cardId: number): Promise<GameCardDto> => {
|
||||
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`)
|
||||
return response.data
|
||||
try {
|
||||
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
|
||||
upsertCard: async (card: GameCardDto): Promise<{ success: boolean; card: GameCardDto }> => {
|
||||
const response = await adminApiClient.post('/api/v2/admin/cards', card)
|
||||
return response.data
|
||||
try {
|
||||
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
|
||||
deleteCard: async (cardId: number): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`)
|
||||
return response.data
|
||||
try {
|
||||
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 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 = {
|
||||
// Get all packs with pagination and search
|
||||
|
|
@ -9,32 +22,132 @@ export const packsApi = {
|
|||
search?: string
|
||||
showDisabled?: boolean
|
||||
}): Promise<PaginatedResponse<CardPackPreviewDto>> => {
|
||||
const response = await adminApiClient.get('/api/v2/admin/packs', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
search: params?.search,
|
||||
showDisabled: params?.showDisabled,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
try {
|
||||
const response = await adminApiClient.get('/api/v2/admin/packs', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
search: params?.search,
|
||||
showDisabled: params?.showDisabled,
|
||||
},
|
||||
})
|
||||
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
|
||||
getPack: async (packId: string): Promise<EditCardPackDto> => {
|
||||
const response = await adminApiClient.get(`/api/v2/admin/packs/${packId}`)
|
||||
return response.data
|
||||
try {
|
||||
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
|
||||
upsertPack: async (pack: EditCardPackDto): Promise<{ success: boolean; pack: EditCardPackDto }> => {
|
||||
const response = await adminApiClient.post('/api/v2/admin/packs', pack)
|
||||
return response.data
|
||||
try {
|
||||
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
|
||||
deletePack: async (packId: string): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await adminApiClient.delete(`/api/v2/admin/packs/${packId}`)
|
||||
return response.data
|
||||
try {
|
||||
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 { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
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 { AxiosError } from 'axios'
|
||||
import { Button } from './ui/button'
|
||||
import { Input } from './ui/input'
|
||||
import { Label } from './ui/label'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
|
||||
import { Badge } from './ui/badge'
|
||||
import { packsApi } from '@/api/packs'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
|
||||
import { ChevronLeft, ChevronRight, Save, X } from 'lucide-react'
|
||||
|
||||
|
|
@ -69,8 +69,11 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error instanceof PacksApiError
|
||||
? error.message
|
||||
: formatApiError(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
|
||||
setPacksData([])
|
||||
})
|
||||
|
|
@ -123,8 +126,11 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
|
|||
toast.success('Card saved successfully')
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to save card')
|
||||
const errorMessage = error instanceof CardsApiError
|
||||
? 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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
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 { AxiosError } from 'axios'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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>>({
|
||||
queryKey: ['cards', page, 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
|
||||
|
|
@ -90,8 +97,11 @@ export default function CardsPage() {
|
|||
closeDialog()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to create card')
|
||||
const errorMessage = error instanceof CardsApiError
|
||||
? error.message
|
||||
: getDetailedErrorMessage(error, 'create', 'card')
|
||||
toast.error(errorMessage)
|
||||
console.error('Error creating card:', error)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -103,8 +113,11 @@ export default function CardsPage() {
|
|||
closeDialog()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to update card')
|
||||
const errorMessage = error instanceof CardsApiError
|
||||
? 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)
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to delete card')
|
||||
const errorMessage = error instanceof CardsApiError
|
||||
? 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) {
|
||||
const errorMessage = error instanceof CardsApiError
|
||||
? error.message
|
||||
: formatApiError(error)
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
|
@ -218,7 +237,18 @@ export default function CardsPage() {
|
|||
</div>
|
||||
<Card>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
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 { AxiosError } from 'axios'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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>>({
|
||||
queryKey: ['packs', page, 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
|
||||
|
|
@ -89,8 +96,11 @@ export default function PacksPage() {
|
|||
closeDialog()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to create pack')
|
||||
const errorMessage = error instanceof PacksApiError
|
||||
? error.message
|
||||
: getDetailedErrorMessage(error, 'create', 'pack')
|
||||
toast.error(errorMessage)
|
||||
console.error('Error creating pack:', error)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -102,8 +112,11 @@ export default function PacksPage() {
|
|||
closeDialog()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to update pack')
|
||||
const errorMessage = error instanceof PacksApiError
|
||||
? 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)
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to delete pack')
|
||||
const errorMessage = error instanceof PacksApiError
|
||||
? error.message
|
||||
: getDetailedErrorMessage(error, 'delete', 'pack')
|
||||
toast.error(errorMessage)
|
||||
console.error('Error deleting pack:', error)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -169,8 +185,12 @@ export default function PacksPage() {
|
|||
setCardsToAdd([])
|
||||
setCardsToRemove([])
|
||||
setIsDialogOpen(true)
|
||||
} catch {
|
||||
toast.error('Failed to load pack details')
|
||||
} catch (error) {
|
||||
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 = {
|
||||
id: selectedPack?.id || '',
|
||||
// Only include id for updates, not for new packs
|
||||
...(selectedPack && { id: selectedPack.id }),
|
||||
title: formData.title.trim(),
|
||||
subtitle: formData.subtitle.trim() || undefined,
|
||||
description: formData.description.trim() || undefined,
|
||||
|
|
@ -238,6 +259,9 @@ export default function PacksPage() {
|
|||
}
|
||||
|
||||
if (error) {
|
||||
const errorMessage = error instanceof PacksApiError
|
||||
? error.message
|
||||
: formatApiError(error)
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
|
@ -246,7 +270,18 @@ export default function PacksPage() {
|
|||
</div>
|
||||
<Card>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,13 +49,23 @@ class AdminCardsApiV2 {
|
|||
// Validate pagination
|
||||
if (page < 1) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
if (limit < 1 || limit > 100) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -109,7 +119,11 @@ class AdminCardsApiV2 {
|
|||
} catch (e, s) {
|
||||
print('Error in getAllCards: $e\n$s');
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -122,7 +136,12 @@ class AdminCardsApiV2 {
|
|||
try {
|
||||
if (cardId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -130,7 +149,11 @@ class AdminCardsApiV2 {
|
|||
final card = await _db.packDao.getCardById(cardId);
|
||||
if (card == null) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -152,9 +175,14 @@ class AdminCardsApiV2 {
|
|||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
print('Error in getCard: $e\n$s');
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -168,13 +196,52 @@ class AdminCardsApiV2 {
|
|||
final body = await request.readAsString();
|
||||
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'];
|
||||
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 existing = await _db.packDao.getCardById(cardId);
|
||||
if (existing != null) {
|
||||
// Update existing card
|
||||
if (existing == null) {
|
||||
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(
|
||||
original: data['original'] ?? existing.original,
|
||||
translation: data['translation'] ?? existing.translation,
|
||||
|
|
@ -226,7 +293,12 @@ class AdminCardsApiV2 {
|
|||
final created = await _db.packDao.getCardById(cardId);
|
||||
if (created == null) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -252,8 +324,26 @@ class AdminCardsApiV2 {
|
|||
);
|
||||
} catch (e, 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(
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -266,7 +356,12 @@ class AdminCardsApiV2 {
|
|||
try {
|
||||
if (cardId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -277,7 +372,11 @@ class AdminCardsApiV2 {
|
|||
final existing = await _db.packDao.getCardById(cardId);
|
||||
if (existing == null) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -300,9 +399,15 @@ class AdminCardsApiV2 {
|
|||
json.encode({'success': true}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
print('Error in updateCard: $e\n$s');
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -315,7 +420,25 @@ class AdminCardsApiV2 {
|
|||
try {
|
||||
if (cardId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -323,12 +446,29 @@ class AdminCardsApiV2 {
|
|||
await _db.packDao.deleteCard(cardId);
|
||||
|
||||
return Response.ok(
|
||||
json.encode({'success': true}),
|
||||
body: json.encode({
|
||||
'success': true,
|
||||
'message': 'Card deleted successfully',
|
||||
}),
|
||||
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(
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -346,7 +486,25 @@ class AdminCardsApiV2 {
|
|||
|
||||
if (cardId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -368,7 +526,11 @@ class AdminCardsApiV2 {
|
|||
} catch (e, s) {
|
||||
print('Error in getCardVoices: $e\n$s');
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -386,7 +548,12 @@ class AdminCardsApiV2 {
|
|||
|
||||
if (cardId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -399,7 +566,12 @@ class AdminCardsApiV2 {
|
|||
|
||||
if (voiceUrl == null || voiceUrl.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -408,7 +580,11 @@ class AdminCardsApiV2 {
|
|||
final card = await _db.packDao.getCardById(cardId);
|
||||
if (card == null) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -442,8 +618,21 @@ class AdminCardsApiV2 {
|
|||
);
|
||||
} catch (e, 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(
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -461,7 +650,25 @@ class AdminCardsApiV2 {
|
|||
|
||||
if (cardId.isEmpty || voiceId.isEmpty) {
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
@ -473,13 +680,21 @@ class AdminCardsApiV2 {
|
|||
await _db.packDao.deleteVoice(voiceId);
|
||||
|
||||
return Response.ok(
|
||||
json.encode({'success': true}),
|
||||
body: json.encode({
|
||||
'success': true,
|
||||
'message': 'Voice removed successfully',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
} catch (e, s) {
|
||||
print('Error in removeCardVoice: $e\n$s');
|
||||
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'},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,13 +67,23 @@ class AdminPacksApiV2 {
|
|||
// Validate pagination
|
||||
if (page < 1) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
if (limit < 1 || limit > 100) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -131,6 +141,7 @@ class AdminPacksApiV2 {
|
|||
{
|
||||
'error': 'Internal server error',
|
||||
'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,
|
||||
);
|
||||
|
|
@ -147,10 +158,26 @@ class AdminPacksApiV2 {
|
|||
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);
|
||||
if (pack == null) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -195,6 +222,7 @@ class AdminPacksApiV2 {
|
|||
{
|
||||
'error': 'Internal server error',
|
||||
'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,
|
||||
);
|
||||
|
|
@ -214,16 +242,33 @@ class AdminPacksApiV2 {
|
|||
final body = await request.readAsString();
|
||||
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);
|
||||
|
||||
String packId;
|
||||
if (editDto.id != null) {
|
||||
if (editDto.id != null && editDto.id!.isNotEmpty) {
|
||||
// Update existing pack
|
||||
packId = editDto.id!;
|
||||
final existing = await _db.packDao.getPackById(packId);
|
||||
if (existing == null) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -270,11 +315,41 @@ class AdminPacksApiV2 {
|
|||
|
||||
// Handle card associations if provided
|
||||
if (editDto.addCardIds != null && editDto.addCardIds!.isNotEmpty) {
|
||||
for (final cardId in editDto.addCardIds!) {
|
||||
await _db.packDao.addCardToPack(
|
||||
packId: packId,
|
||||
cardId: cardId,
|
||||
);
|
||||
try {
|
||||
for (final cardId in editDto.addCardIds!) {
|
||||
// Verify card exists before adding
|
||||
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);
|
||||
if (updatedPack == null) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -339,10 +418,26 @@ class AdminPacksApiV2 {
|
|||
});
|
||||
} catch (e, 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(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to save pack',
|
||||
'details': details,
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
|
|
@ -359,10 +454,26 @@ class AdminPacksApiV2 {
|
|||
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);
|
||||
if (pack == null) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -375,10 +486,19 @@ class AdminPacksApiV2 {
|
|||
});
|
||||
} catch (e, 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(
|
||||
{
|
||||
'error': 'Internal server error',
|
||||
'message': 'Failed to delete pack',
|
||||
'details': details,
|
||||
},
|
||||
statusCode: 500,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue