first commit

This commit is contained in:
2025-09-12 12:38:11 +02:00
commit 0db2fd0314
46 changed files with 23221 additions and 0 deletions

208
backend/api/auth.go Normal file
View File

@@ -0,0 +1,208 @@
package api
import (
"net/http"
"project-dashboard/api/utils"
"project-dashboard/middleware"
"project-dashboard/models"
"github.com/gin-gonic/gin"
)
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type RegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
}
type AuthResponse struct {
Token string `json:"token"`
User models.User `json:"user"`
}
// Register creates a new user account
func Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Check if user already exists
var existingUser models.User
if err := models.GetDB().Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
utils.ErrorResponse(c, http.StatusConflict, "User with this email already exists")
return
}
// Create new user
user := models.User{
Email: req.Email,
FirstName: req.FirstName,
LastName: req.LastName,
Role: "member", // Default role
}
// Hash password
if err := user.HashPassword(req.Password); err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to hash password")
return
}
// Save user to database
if err := models.GetDB().Create(&user).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create user")
return
}
// Generate token
token, err := middleware.GenerateToken(&user)
if err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to generate token")
return
}
utils.SuccessResponse(c, http.StatusCreated, "User created successfully", AuthResponse{
Token: token,
User: user,
})
}
// Login authenticates a user
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Find user by email
var user models.User
if err := models.GetDB().Where("email = ?", req.Email).First(&user).Error; err != nil {
utils.ErrorResponse(c, http.StatusUnauthorized, "Invalid email or password")
return
}
// Check password
if !user.CheckPassword(req.Password) {
utils.ErrorResponse(c, http.StatusUnauthorized, "Invalid email or password")
return
}
// Generate token
token, err := middleware.GenerateToken(&user)
if err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to generate token")
return
}
utils.SuccessResponse(c, http.StatusOK, "Login successful", AuthResponse{
Token: token,
User: user,
})
}
// GetProfile returns the current user's profile
func GetProfile(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
utils.SuccessResponse(c, http.StatusOK, "Profile retrieved successfully", user)
}
// UpdateProfile updates the current user's profile
func UpdateProfile(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
var updateData struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
}
if err := c.ShouldBindJSON(&updateData); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Update fields
if updateData.FirstName != "" {
userObj.FirstName = updateData.FirstName
}
if updateData.LastName != "" {
userObj.LastName = updateData.LastName
}
if updateData.Avatar != "" {
userObj.Avatar = updateData.Avatar
}
if err := models.GetDB().Save(userObj).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to update profile")
return
}
utils.SuccessResponse(c, http.StatusOK, "Profile updated successfully", userObj)
}
// ChangePassword changes the user's password
func ChangePassword(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Verify current password
if !userObj.CheckPassword(req.CurrentPassword) {
utils.ErrorResponse(c, http.StatusBadRequest, "Current password is incorrect")
return
}
// Hash new password
if err := userObj.HashPassword(req.NewPassword); err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to hash new password")
return
}
if err := models.GetDB().Save(userObj).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to update password")
return
}
utils.SuccessResponse(c, http.StatusOK, "Password changed successfully", nil)
}

243
backend/api/files.go Normal file
View File

@@ -0,0 +1,243 @@
package api
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"project-dashboard/api/utils"
"project-dashboard/models"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// UploadFile handles file uploads
func UploadFile(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
project, _ := c.Get("project")
projectObj := project.(*models.Project)
// Get task ID if provided
taskIDStr := c.PostForm("task_id")
var taskID *uint
if taskIDStr != "" {
if id, err := strconv.ParseUint(taskIDStr, 10, 32); err == nil {
taskIDUint := uint(id)
taskID = &taskIDUint
}
}
// Get file from form
file, header, err := c.Request.FormFile("file")
if err != nil {
utils.ErrorResponse(c, http.StatusBadRequest, "No file provided")
return
}
defer file.Close()
// Validate file size (10MB limit)
const maxFileSize = 10 << 20 // 10MB
if header.Size > maxFileSize {
utils.ErrorResponse(c, http.StatusBadRequest, "File size too large (max 10MB)")
return
}
// Generate unique filename
ext := filepath.Ext(header.Filename)
fileName := uuid.New().String() + ext
// Create uploads directory if it doesn't exist
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create upload directory")
return
}
// Save file
filePath := filepath.Join(uploadDir, fileName)
dst, err := os.Create(filePath)
if err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create file")
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to save file")
return
}
// Get MIME type
mimeType := header.Header.Get("Content-Type")
if mimeType == "" {
mimeType = "application/octet-stream"
}
// Create file record
fileUpload := models.FileUpload{
FileName: fileName,
OriginalName: header.Filename,
FilePath: filePath,
FileSize: header.Size,
MimeType: mimeType,
UploadedByID: userObj.ID,
ProjectID: &projectObj.ID,
TaskID: taskID,
}
if err := models.GetDB().Create(&fileUpload).Error; err != nil {
// Clean up file if database save fails
os.Remove(filePath)
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to save file record")
return
}
// Load file with relationships
models.GetDB().
Preload("UploadedBy").
Preload("Project").
Preload("Task").
First(&fileUpload, fileUpload.ID)
utils.SuccessResponse(c, http.StatusCreated, "File uploaded successfully", fileUpload)
}
// GetFiles returns all files for a project
func GetFiles(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
var files []models.FileUpload
if err := models.GetDB().
Preload("UploadedBy").
Preload("Project").
Preload("Task").
Where("project_id = ?", projectObj.ID).
Order("created_at DESC").
Find(&files).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to fetch files")
return
}
utils.SuccessResponse(c, http.StatusOK, "Files retrieved successfully", files)
}
// GetTaskFiles returns all files for a specific task
func GetTaskFiles(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
// Verify task exists and belongs to project
var task models.Task
if err := models.GetDB().
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
var files []models.FileUpload
if err := models.GetDB().
Preload("UploadedBy").
Preload("Project").
Preload("Task").
Where("task_id = ?", taskID).
Order("created_at DESC").
Find(&files).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to fetch task files")
return
}
utils.SuccessResponse(c, http.StatusOK, "Task files retrieved successfully", files)
}
// DownloadFile serves a file for download
func DownloadFile(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
fileID := c.Param("file_id")
if fileID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "File ID required")
return
}
var fileUpload models.FileUpload
if err := models.GetDB().
Where("id = ? AND project_id = ?", fileID, projectObj.ID).
First(&fileUpload).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "File not found")
return
}
// Check if file exists on disk
if _, err := os.Stat(fileUpload.FilePath); os.IsNotExist(err) {
utils.ErrorResponse(c, http.StatusNotFound, "File not found on disk")
return
}
// Set headers for file download
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileUpload.OriginalName))
c.Header("Content-Type", "application/octet-stream")
// Serve file
c.File(fileUpload.FilePath)
}
// DeleteFile deletes a file
func DeleteFile(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
fileID := c.Param("file_id")
if fileID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "File ID required")
return
}
var fileUpload models.FileUpload
if err := models.GetDB().
Where("id = ? AND project_id = ?", fileID, projectObj.ID).
First(&fileUpload).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "File not found")
return
}
// Delete file from disk
if err := os.Remove(fileUpload.FilePath); err != nil {
// Log error but continue with database deletion
fmt.Printf("Failed to delete file from disk: %v\n", err)
}
// Delete file record from database
if err := models.GetDB().Delete(&fileUpload).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to delete file record")
return
}
utils.SuccessResponse(c, http.StatusOK, "File deleted successfully", nil)
}

312
backend/api/projects.go Normal file
View File

@@ -0,0 +1,312 @@
package api
import (
"net/http"
"project-dashboard/api/utils"
"project-dashboard/models"
"strconv"
"github.com/gin-gonic/gin"
)
type CreateProjectRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Color string `json:"color"`
}
type UpdateProjectRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
Color string `json:"color"`
}
type AddMemberRequest struct {
UserID uint `json:"user_id" binding:"required"`
Role string `json:"role"`
}
// CreateProject creates a new project
func CreateProject(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
var req CreateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
project := models.Project{
Name: req.Name,
Description: req.Description,
Color: req.Color,
OwnerID: userObj.ID,
Status: "active",
}
if project.Color == "" {
project.Color = "#3b82f6"
}
if err := models.GetDB().Create(&project).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create project")
return
}
// Add owner as project member
member := models.ProjectMember{
ProjectID: project.ID,
UserID: userObj.ID,
Role: "owner",
}
models.GetDB().Create(&member)
// Load project with relationships
models.GetDB().Preload("Owner").Preload("Members").First(&project, project.ID)
utils.SuccessResponse(c, http.StatusCreated, "Project created successfully", project)
}
// GetProjects returns all projects for the current user
func GetProjects(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
var projects []models.Project
// Get projects where user is owner or member
if err := models.GetDB().
Preload("Owner").
Preload("Members").
Where("owner_id = ? OR id IN (SELECT project_id FROM project_members WHERE user_id = ?)",
userObj.ID, userObj.ID).
Find(&projects).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to fetch projects")
return
}
utils.SuccessResponse(c, http.StatusOK, "Projects retrieved successfully", projects)
}
// GetProject returns a specific project
func GetProject(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
// Load additional relationships
models.GetDB().
Preload("Owner").
Preload("Members").
Preload("Tasks").
Preload("Files").
First(projectObj, projectObj.ID)
utils.SuccessResponse(c, http.StatusOK, "Project retrieved successfully", projectObj)
}
// UpdateProject updates a project
func UpdateProject(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
user, _ := c.Get("user")
userObj := user.(*models.User)
// Check if user is owner or manager
role := projectObj.GetMemberRole(userObj.ID)
if role != "owner" && role != "manager" {
utils.ErrorResponse(c, http.StatusForbidden, "Insufficient permissions")
return
}
var req UpdateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Update fields
if req.Name != "" {
projectObj.Name = req.Name
}
if req.Description != "" {
projectObj.Description = req.Description
}
if req.Status != "" {
projectObj.Status = req.Status
}
if req.Color != "" {
projectObj.Color = req.Color
}
if err := models.GetDB().Save(projectObj).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to update project")
return
}
utils.SuccessResponse(c, http.StatusOK, "Project updated successfully", projectObj)
}
// DeleteProject deletes a project
func DeleteProject(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
user, _ := c.Get("user")
userObj := user.(*models.User)
// Only owner can delete project
if !projectObj.IsOwner(userObj.ID) {
utils.ErrorResponse(c, http.StatusForbidden, "Only project owner can delete project")
return
}
if err := models.GetDB().Delete(projectObj).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to delete project")
return
}
utils.SuccessResponse(c, http.StatusOK, "Project deleted successfully", nil)
}
// AddMember adds a member to the project
func AddMember(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
user, _ := c.Get("user")
userObj := user.(*models.User)
// Check if user has permission to add members
role := projectObj.GetMemberRole(userObj.ID)
if role != "owner" && role != "manager" {
utils.ErrorResponse(c, http.StatusForbidden, "Insufficient permissions")
return
}
var req AddMemberRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Check if user exists
var targetUser models.User
if err := models.GetDB().First(&targetUser, req.UserID).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "User not found")
return
}
// Check if user is already a member
if projectObj.HasMember(req.UserID) {
utils.ErrorResponse(c, http.StatusConflict, "User is already a member of this project")
return
}
// Set default role
if req.Role == "" {
req.Role = "member"
}
member := models.ProjectMember{
ProjectID: projectObj.ID,
UserID: req.UserID,
Role: req.Role,
}
if err := models.GetDB().Create(&member).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to add member")
return
}
// Load updated project
models.GetDB().Preload("Members").First(projectObj, projectObj.ID)
utils.SuccessResponse(c, http.StatusOK, "Member added successfully", projectObj)
}
// RemoveMember removes a member from the project
func RemoveMember(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
user, _ := c.Get("user")
userObj := user.(*models.User)
// Check if user has permission to remove members
role := projectObj.GetMemberRole(userObj.ID)
if role != "owner" && role != "manager" {
utils.ErrorResponse(c, http.StatusForbidden, "Insufficient permissions")
return
}
userIDStr := c.Param("user_id")
if userIDStr == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "User ID required")
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
utils.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID")
return
}
// Check if user is a member
if !projectObj.HasMember(uint(userID)) {
utils.ErrorResponse(c, http.StatusNotFound, "User is not a member of this project")
return
}
// Remove member
if err := models.GetDB().
Where("project_id = ? AND user_id = ?", projectObj.ID, uint(userID)).
Delete(&models.ProjectMember{}).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to remove member")
return
}
// Load updated project
models.GetDB().Preload("Members").First(projectObj, projectObj.ID)
utils.SuccessResponse(c, http.StatusOK, "Member removed successfully", projectObj)
}

359
backend/api/tasks.go Normal file
View File

@@ -0,0 +1,359 @@
package api
import (
"net/http"
"project-dashboard/api/utils"
"project-dashboard/models"
"time"
"github.com/gin-gonic/gin"
)
type CreateTaskRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Priority string `json:"priority"`
DueDate string `json:"due_date"`
AssignedToID *uint `json:"assigned_to_id"`
}
type UpdateTaskRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
DueDate string `json:"due_date"`
AssignedToID *uint `json:"assigned_to_id"`
Position *int `json:"position"`
}
type CreateSubtaskRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
AssignedToID *uint `json:"assigned_to_id"`
}
type CreateCommentRequest struct {
Content string `json:"content" binding:"required"`
}
// CreateTask creates a new task
func CreateTask(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
project, _ := c.Get("project")
projectObj := project.(*models.Project)
var req CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
task := models.Task{
Title: req.Title,
Description: req.Description,
Priority: req.Priority,
ProjectID: projectObj.ID,
CreatedByID: userObj.ID,
AssignedToID: req.AssignedToID,
Status: "todo",
}
if task.Priority == "" {
task.Priority = "medium"
}
// Parse due date if provided
if req.DueDate != "" {
if dueDate, err := time.Parse("2006-01-02T15:04:05Z07:00", req.DueDate); err == nil {
task.DueDate = &dueDate
}
}
if err := models.GetDB().Create(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create task")
return
}
// Load task with relationships
models.GetDB().
Preload("Project").
Preload("AssignedTo").
Preload("CreatedBy").
First(&task, task.ID)
utils.SuccessResponse(c, http.StatusCreated, "Task created successfully", task)
}
// GetTasks returns all tasks for a project
func GetTasks(c *gin.Context) {
project, exists := c.Get("project")
if !exists {
utils.ErrorResponse(c, http.StatusNotFound, "Project not found")
return
}
projectObj := project.(*models.Project)
var tasks []models.Task
if err := models.GetDB().
Preload("AssignedTo").
Preload("CreatedBy").
Preload("Comments").
Preload("Files").
Preload("Subtasks").
Preload("Labels").
Where("project_id = ?", projectObj.ID).
Order("position ASC, created_at DESC").
Find(&tasks).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to fetch tasks")
return
}
utils.SuccessResponse(c, http.StatusOK, "Tasks retrieved successfully", tasks)
}
// GetTask returns a specific task
func GetTask(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
var task models.Task
if err := models.GetDB().
Preload("Project").
Preload("AssignedTo").
Preload("CreatedBy").
Preload("Comments").
Preload("Comments.User").
Preload("Files").
Preload("Subtasks").
Preload("Subtasks.AssignedTo").
Preload("Labels").
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
utils.SuccessResponse(c, http.StatusOK, "Task retrieved successfully", task)
}
// UpdateTask updates a task
func UpdateTask(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
var task models.Task
if err := models.GetDB().
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
var req UpdateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
// Update fields
if req.Title != "" {
task.Title = req.Title
}
if req.Description != "" {
task.Description = req.Description
}
if req.Status != "" {
task.Status = req.Status
if req.Status == "done" {
task.MarkAsCompleted()
}
}
if req.Priority != "" {
task.Priority = req.Priority
}
if req.AssignedToID != nil {
task.AssignedToID = req.AssignedToID
}
if req.Position != nil {
task.Position = *req.Position
}
// Parse due date if provided
if req.DueDate != "" {
if dueDate, err := time.Parse("2006-01-02T15:04:05Z07:00", req.DueDate); err == nil {
task.DueDate = &dueDate
}
}
if err := models.GetDB().Save(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to update task")
return
}
// Load updated task with relationships
models.GetDB().
Preload("AssignedTo").
Preload("CreatedBy").
Preload("Comments").
Preload("Files").
Preload("Subtasks").
Preload("Labels").
First(&task, task.ID)
utils.SuccessResponse(c, http.StatusOK, "Task updated successfully", task)
}
// DeleteTask deletes a task
func DeleteTask(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
var task models.Task
if err := models.GetDB().
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
if err := models.GetDB().Delete(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to delete task")
return
}
utils.SuccessResponse(c, http.StatusOK, "Task deleted successfully", nil)
}
// CreateSubtask creates a new subtask
func CreateSubtask(c *gin.Context) {
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
// Verify task exists and belongs to project
var task models.Task
if err := models.GetDB().
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
var req CreateSubtaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
subtask := models.Subtask{
Title: req.Title,
Description: req.Description,
ParentTaskID: task.ID,
AssignedToID: req.AssignedToID,
Status: "todo",
}
if err := models.GetDB().Create(&subtask).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create subtask")
return
}
// Load subtask with relationships
models.GetDB().
Preload("AssignedTo").
First(&subtask, subtask.ID)
utils.SuccessResponse(c, http.StatusCreated, "Subtask created successfully", subtask)
}
// AddComment adds a comment to a task
func AddComment(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
utils.ErrorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
userObj := user.(*models.User)
project, _ := c.Get("project")
projectObj := project.(*models.Project)
taskID := c.Param("task_id")
if taskID == "" {
utils.ErrorResponse(c, http.StatusBadRequest, "Task ID required")
return
}
// Verify task exists and belongs to project
var task models.Task
if err := models.GetDB().
Where("id = ? AND project_id = ?", taskID, projectObj.ID).
First(&task).Error; err != nil {
utils.ErrorResponse(c, http.StatusNotFound, "Task not found")
return
}
var req CreateCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ValidationErrorResponse(c, gin.H{
"error": err.Error(),
})
return
}
comment := models.Comment{
Content: req.Content,
TaskID: task.ID,
UserID: userObj.ID,
}
if err := models.GetDB().Create(&comment).Error; err != nil {
utils.ErrorResponse(c, http.StatusInternalServerError, "Failed to create comment")
return
}
// Load comment with user
models.GetDB().
Preload("User").
First(&comment, comment.ID)
utils.SuccessResponse(c, http.StatusCreated, "Comment added successfully", comment)
}

View File

@@ -0,0 +1,64 @@
package utils
import (
"net/http"
"github.com/gin-gonic/gin"
)
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// SuccessResponse sends a successful response
func SuccessResponse(c *gin.Context, statusCode int, message string, data interface{}) {
c.JSON(statusCode, APIResponse{
Success: true,
Message: message,
Data: data,
})
}
// ErrorResponse sends an error response
func ErrorResponse(c *gin.Context, statusCode int, message string) {
c.JSON(statusCode, APIResponse{
Success: false,
Error: message,
})
}
// ValidationErrorResponse sends a validation error response
func ValidationErrorResponse(c *gin.Context, errors interface{}) {
c.JSON(http.StatusBadRequest, APIResponse{
Success: false,
Error: "Validation failed",
Data: errors,
})
}
// PaginatedResponse sends a paginated response
type PaginatedResponse struct {
Data interface{} `json:"data"`
Pagination Pagination `json:"pagination"`
}
type Pagination struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
}
func PaginatedSuccessResponse(c *gin.Context, message string, data interface{}, pagination Pagination) {
c.JSON(http.StatusOK, APIResponse{
Success: true,
Message: message,
Data: PaginatedResponse{
Data: data,
Pagination: pagination,
},
})
}