'use server' import { prisma } from './db' import { revalidatePath } from 'next/cache' // Retry utility for database operations async function retryOperation( operation: () => Promise, maxRetries: number = 3, delayMs: number = 1000 ): Promise { let lastError: Error | undefined for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await operation() } catch (error) { lastError = error as Error // Don't retry on validation errors or not found errors if ( error instanceof Error && (error.message.includes('Unique constraint') || error.message.includes('Record to update not found') || error.message.includes('Record to delete does not exist')) ) { throw error } // Wait before retrying (exponential backoff) if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, delayMs * Math.pow(2, attempt))) } } } throw lastError } export async function getLists() { try { return await retryOperation(() => prisma.list.findMany({ include: { items: true, }, orderBy: { updatedAt: 'desc', }, }) ) } catch (error) { console.error('Error fetching lists:', error) throw new Error('Failed to load lists. Please check your connection and try again.') } } export async function createList(title: string, color: string = '#3B82F6') { try { // Validate input if (!title || !title.trim()) { throw new Error('List title is required') } if (title.length > 200) { throw new Error('List title must be less than 200 characters') } const list = await retryOperation(() => prisma.list.create({ data: { title: title.trim(), color, }, }) ) revalidatePath('/') return list } catch (error) { console.error('Error creating list:', error) if (error instanceof Error && error.message.includes('required')) { throw error } throw new Error('Failed to create list. Please try again.') } } export async function getListWithItems(listId: string) { try { if (!listId) { throw new Error('List ID is required') } const list = await retryOperation(() => prisma.list.findUnique({ where: { id: listId }, include: { items: { orderBy: { createdAt: 'desc', }, }, }, }) ) if (!list) { throw new Error('List not found') } return list } catch (error) { console.error('Error fetching list:', error) if (error instanceof Error && error.message === 'List not found') { throw error } throw new Error('Failed to load list. Please try again.') } } export async function getItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId }, include: { list: true, }, }) ) if (!item) { throw new Error('Item not found') } return item } catch (error) { console.error('Error fetching item:', error) if (error instanceof Error && error.message === 'Item not found') { throw error } throw new Error('Failed to load item. Please try again.') } } export async function createItem( listId: string, title: string, description?: string, metadata?: { priority?: 'low' | 'medium' | 'high' dueDate?: Date | null tags?: string[] notes?: string | null } ) { try { // Validate input if (!listId) { throw new Error('List ID is required') } if (!title || !title.trim()) { throw new Error('Item title is required') } if (title.length > 200) { throw new Error('Item title must be less than 200 characters') } const item = await retryOperation(() => prisma.item.create({ data: { title: title.trim(), description: description?.trim() || null, listId, ...(metadata?.priority && { priority: metadata.priority }), ...(metadata?.dueDate !== undefined && { dueDate: metadata.dueDate }), ...(metadata?.tags && { tags: metadata.tags }), ...(metadata?.notes !== undefined && { notes: metadata.notes }), }, }) ) revalidatePath(`/list/${listId}`) return item } catch (error) { console.error('Error creating item:', error) if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be'))) { throw error } throw new Error('Failed to create item. Please try again.') } } export async function toggleItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId } }) ) if (!item) { throw new Error('Item not found') } const updated = await retryOperation(() => prisma.item.update({ where: { id: itemId }, data: { completed: !item.completed }, }) ) revalidatePath(`/list/${item.listId}`) return updated } catch (error) { console.error('Error toggling item:', error) if (error instanceof Error && error.message === 'Item not found') { throw error } throw new Error('Failed to update item. Please try again.') } } export async function updateItem( itemId: string, data: { title?: string description?: string | null completed?: boolean priority?: 'low' | 'medium' | 'high' dueDate?: Date | null tags?: string[] notes?: string | null } ) { try { if (!itemId) { throw new Error('Item ID is required') } // Validate title if provided if (data.title !== undefined) { if (!data.title || !data.title.trim()) { throw new Error('Item title cannot be empty') } if (data.title.length > 200) { throw new Error('Item title must be less than 200 characters') } data.title = data.title.trim() } // Trim description if provided if (data.description !== undefined && data.description !== null) { data.description = data.description.trim() || null } const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId } }) ) if (!item) { throw new Error('Item not found') } const updated = await retryOperation(() => prisma.item.update({ where: { id: itemId }, data, }) ) revalidatePath(`/list/${item.listId}`) return updated } catch (error) { console.error('Error updating item:', error) if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be'))) { throw error } throw new Error('Failed to update item. Please try again.') } } export async function deleteItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId } }) ) if (!item) { throw new Error('Item not found') } await retryOperation(() => prisma.item.delete({ where: { id: itemId }, }) ) revalidatePath(`/list/${item.listId}`) return { success: true } } catch (error) { console.error('Error deleting item:', error) if (error instanceof Error && error.message === 'Item not found') { throw error } throw new Error('Failed to delete item. Please try again.') } } export async function deleteList(listId: string) { try { if (!listId) { throw new Error('List ID is required') } // Check if list exists const list = await retryOperation(() => prisma.list.findUnique({ where: { id: listId } }) ) if (!list) { throw new Error('List not found') } await retryOperation(() => prisma.list.delete({ where: { id: listId }, }) ) revalidatePath('/') return { success: true } } catch (error) { console.error('Error deleting list:', error) if (error instanceof Error && error.message === 'List not found') { throw error } throw new Error('Failed to delete list. Please try again.') } } export async function updateList( listId: string, data: { title?: string color?: string } ) { try { if (!listId) { throw new Error('List ID is required') } // Validate title if provided if (data.title !== undefined) { if (!data.title || !data.title.trim()) { throw new Error('List title cannot be empty') } if (data.title.length > 200) { throw new Error('List title must be less than 200 characters') } data.title = data.title.trim() } const updated = await retryOperation(() => prisma.list.update({ where: { id: listId }, data, }) ) revalidatePath('/') revalidatePath(`/list/${listId}`) return updated } catch (error) { console.error('Error updating list:', error) if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be'))) { throw error } throw new Error('Failed to update list. Please try again.') } } export async function searchItems(query: string, listId?: string) { try { if (!query || !query.trim()) { return [] } const searchTerm = query.toLowerCase() // Build the where clause const whereClause: any = { OR: [ { title: { contains: query, mode: 'insensitive' as const, }, }, { description: { contains: query, mode: 'insensitive' as const, }, }, { notes: { contains: query, mode: 'insensitive' as const, }, }, ], } // Add list scope if specified if (listId) { whereClause.listId = listId } const items = await retryOperation(() => prisma.item.findMany({ where: whereClause, include: { list: true, }, orderBy: { updatedAt: 'desc', }, }) ) // Also search in tags (stored as JSON) const allItems = await retryOperation(() => listId ? prisma.item.findMany({ where: { listId }, include: { list: true }, }) : prisma.item.findMany({ include: { list: true }, }) ) const tagMatches = allItems.filter((item) => { const tags = Array.isArray(item.tags) ? item.tags : [] return tags.some((tag) => typeof tag === 'string' && tag.toLowerCase().includes(searchTerm) ) }) // Combine results and remove duplicates const combinedResults = [...items, ...tagMatches] const uniqueResults = Array.from( new Map(combinedResults.map((item) => [item.id, item])).values() ) return uniqueResults } catch (error) { console.error('Error searching items:', error) throw new Error('Failed to search items. Please try again.') } } export async function getItemsByFilter( listId: string | null, filters: { priority?: 'low' | 'medium' | 'high' | null completed?: boolean | null tags?: string[] dueDateRange?: { start: Date | null; end: Date | null } sortBy?: 'createdAt' | 'dueDate' | 'priority' | 'title' sortOrder?: 'asc' | 'desc' } = {} ) { try { // Build the where clause with compound filters (AND logic) const whereClause: any = {} // Add list scope if specified if (listId) { whereClause.listId = listId } // Filter by priority if (filters.priority) { whereClause.priority = filters.priority } // Filter by completed status if (filters.completed !== undefined && filters.completed !== null) { whereClause.completed = filters.completed } // Filter by due date range if (filters.dueDateRange) { const { start, end } = filters.dueDateRange if (start || end) { whereClause.dueDate = {} if (start) { whereClause.dueDate.gte = start } if (end) { whereClause.dueDate.lte = end } } } // Fetch items from database const items = await retryOperation(() => prisma.item.findMany({ where: whereClause, include: { list: true, }, }) ) // Filter by tags (client-side since tags are stored as JSON) let filteredItems = items if (filters.tags && filters.tags.length > 0) { filteredItems = items.filter((item) => { const itemTags = Array.isArray(item.tags) ? item.tags : [] return filters.tags!.every((filterTag) => itemTags.some((itemTag) => typeof itemTag === 'string' && itemTag === filterTag) ) }) } // Sort items const sortBy = filters.sortBy || 'createdAt' const sortOrder = filters.sortOrder || 'desc' const sortedItems = [...filteredItems].sort((a, b) => { let comparison = 0 switch (sortBy) { case 'createdAt': comparison = a.createdAt.getTime() - b.createdAt.getTime() break case 'dueDate': // Handle null due dates - place at end if (a.dueDate === null && b.dueDate === null) { comparison = 0 } else if (a.dueDate === null) { comparison = 1 // a goes after b } else if (b.dueDate === null) { comparison = -1 // b goes after a } else { comparison = a.dueDate.getTime() - b.dueDate.getTime() } break case 'priority': const priorityOrder = { low: 1, medium: 2, high: 3 } const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] || 2 const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] || 2 comparison = aPriority - bPriority break case 'title': comparison = a.title.localeCompare(b.title) break } return sortOrder === 'asc' ? comparison : -comparison }) return sortedItems } catch (error) { console.error('Error filtering items:', error) throw new Error('Failed to filter items. Please try again.') } }