
- Replaced Python/FastAPI implementation with Node.js/Express/TypeScript
- Added Prisma ORM with SQLite database
- Implemented JWT authentication with bcrypt password hashing
- Created comprehensive task management API with CRUD operations
- Integrated Twilio WhatsApp Business API for notifications
- Added OpenAI integration for intelligent task scheduling
- Implemented automated background jobs with node-cron
- Added comprehensive error handling and validation
- Structured logging with Winston
- Complete API documentation and setup instructions
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
306 lines
7.5 KiB
TypeScript
306 lines
7.5 KiB
TypeScript
import { Response } from 'express';
|
|
import { validate, taskCreationSchema, taskUpdateSchema } from '@/utils/validation';
|
|
import prisma from '@/utils/database';
|
|
import logger from '@/utils/logger';
|
|
import { AuthenticatedRequest, CreateTaskData, UpdateTaskData, ApiResponse } from '@/types';
|
|
import { suggestOptimalTime } from '@/services/aiService';
|
|
import { sendTaskReminder } from '@/services/whatsappService';
|
|
|
|
export const createTask = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const taskData: CreateTaskData = validate(taskCreationSchema, req.body);
|
|
const userId = req.user!.id;
|
|
|
|
let aiSuggestedTime = null;
|
|
if (taskData.dueDate && !taskData.scheduledAt) {
|
|
aiSuggestedTime = await suggestOptimalTime(taskData.dueDate, taskData.priority || 'MEDIUM');
|
|
}
|
|
|
|
const task = await prisma.task.create({
|
|
data: {
|
|
title: taskData.title,
|
|
description: taskData.description,
|
|
priority: taskData.priority || 'MEDIUM',
|
|
dueDate: taskData.dueDate,
|
|
scheduledAt: taskData.scheduledAt,
|
|
aiSuggestedTime,
|
|
ownerId: userId,
|
|
},
|
|
include: {
|
|
owner: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
fullName: true,
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
logger.info(`Task created: ${task.title} by user ${userId}`);
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
message: 'Task created successfully',
|
|
data: task
|
|
} as ApiResponse);
|
|
|
|
} catch (error) {
|
|
logger.error('Task creation error:', error);
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Task creation failed'
|
|
} as ApiResponse);
|
|
}
|
|
};
|
|
|
|
export const getTasks = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const userId = req.user!.id;
|
|
const { status, priority, page = '1', limit = '20' } = req.query;
|
|
|
|
const pageNum = parseInt(page as string);
|
|
const limitNum = parseInt(limit as string);
|
|
const skip = (pageNum - 1) * limitNum;
|
|
|
|
const where: any = { ownerId: userId };
|
|
if (status) where.status = status;
|
|
if (priority) where.priority = priority;
|
|
|
|
const [tasks, total] = await Promise.all([
|
|
prisma.task.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip,
|
|
take: limitNum,
|
|
include: {
|
|
owner: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
fullName: true,
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
prisma.task.count({ where })
|
|
]);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Tasks retrieved successfully',
|
|
data: {
|
|
tasks,
|
|
pagination: {
|
|
page: pageNum,
|
|
limit: limitNum,
|
|
total,
|
|
pages: Math.ceil(total / limitNum)
|
|
}
|
|
}
|
|
} as ApiResponse);
|
|
|
|
} catch (error) {
|
|
logger.error('Get tasks error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to retrieve tasks'
|
|
} as ApiResponse);
|
|
}
|
|
};
|
|
|
|
export const getTask = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const taskId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
const task = await prisma.task.findFirst({
|
|
where: {
|
|
id: taskId,
|
|
ownerId: userId
|
|
},
|
|
include: {
|
|
owner: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
fullName: true,
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!task) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Task not found'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Task retrieved successfully',
|
|
data: task
|
|
} as ApiResponse);
|
|
|
|
} catch (error) {
|
|
logger.error('Get task error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to retrieve task'
|
|
} as ApiResponse);
|
|
}
|
|
};
|
|
|
|
export const updateTask = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const taskId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
const updateData: UpdateTaskData = validate(taskUpdateSchema, req.body);
|
|
|
|
const existingTask = await prisma.task.findFirst({
|
|
where: {
|
|
id: taskId,
|
|
ownerId: userId
|
|
}
|
|
});
|
|
|
|
if (!existingTask) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Task not found'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
const updatePayload: any = { ...updateData };
|
|
|
|
if (updateData.status === 'COMPLETED' && existingTask.status !== 'COMPLETED') {
|
|
updatePayload.completedAt = new Date();
|
|
}
|
|
|
|
const task = await prisma.task.update({
|
|
where: { id: taskId },
|
|
data: updatePayload,
|
|
include: {
|
|
owner: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
fullName: true,
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
logger.info(`Task updated: ${task.title} by user ${userId}`);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Task updated successfully',
|
|
data: task
|
|
} as ApiResponse);
|
|
|
|
} catch (error) {
|
|
logger.error('Task update error:', error);
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Task update failed'
|
|
} as ApiResponse);
|
|
}
|
|
};
|
|
|
|
export const deleteTask = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const taskId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
const task = await prisma.task.findFirst({
|
|
where: {
|
|
id: taskId,
|
|
ownerId: userId
|
|
}
|
|
});
|
|
|
|
if (!task) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Task not found'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
await prisma.task.delete({
|
|
where: { id: taskId }
|
|
});
|
|
|
|
logger.info(`Task deleted: ${task.title} by user ${userId}`);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Task deleted successfully'
|
|
} as ApiResponse);
|
|
|
|
} catch (error) {
|
|
logger.error('Task deletion error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to delete task'
|
|
} as ApiResponse);
|
|
}
|
|
};
|
|
|
|
export const sendReminder = async (req: AuthenticatedRequest, res: Response) => {
|
|
try {
|
|
const taskId = parseInt(req.params.id);
|
|
const userId = req.user!.id;
|
|
|
|
const task = await prisma.task.findFirst({
|
|
where: {
|
|
id: taskId,
|
|
ownerId: userId
|
|
},
|
|
include: {
|
|
owner: true
|
|
}
|
|
});
|
|
|
|
if (!task) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Task not found'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
if (!task.owner.whatsappNumber) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'WhatsApp number not configured'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
const success = await sendTaskReminder(task.owner.whatsappNumber, task);
|
|
|
|
if (success) {
|
|
await prisma.task.update({
|
|
where: { id: taskId },
|
|
data: { whatsappReminderSent: true }
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Reminder sent successfully'
|
|
} as ApiResponse);
|
|
} else {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to send reminder'
|
|
} as ApiResponse);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('Send reminder error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to send reminder'
|
|
} as ApiResponse);
|
|
}
|
|
}; |