package handlers import ( "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" ) type ProjectHandler struct { DB *sql.DB } func NewProjectHandler(db *sql.DB) *ProjectHandler { return &ProjectHandler{DB: db} } type Project struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` CustomerID *string `json:"customer_id"` Status string `json:"status"` Budget float64 `json:"budget"` Spent float64 `json:"spent"` Currency string `json:"currency"` StartDate *time.Time `json:"start_date"` EndDate *time.Time `json:"end_date"` ManagerID *string `json:"manager_id"` Progress float64 `json:"progress"` CreatedAt time.Time `json:"created_at"` } type ProjectTime struct { ID string `json:"id"` ProjectID string `json:"project_id"` EmployeeID string `json:"employee_id"` EmployeeName string `json:"employee_name"` Date time.Time `json:"date"` Hours float64 `json:"hours"` Description string `json:"description"` Billable bool `json:"billable"` HourlyRate float64 `json:"hourly_rate"` } func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) { status := r.URL.Query().Get("status") if status == "" { status = "all" } var query string var args []interface{} if status == "all" { query = `SELECT id, name, description, customer_id, status, budget, spent, currency, start_date, end_date, manager_id, created_at FROM boc_projects ORDER BY created_at DESC LIMIT 100` } else { query = `SELECT id, name, description, customer_id, status, budget, spent, currency, start_date, end_date, manager_id, created_at FROM boc_projects WHERE status = $1 ORDER BY created_at DESC LIMIT 100` args = append(args, status) } rows, err := h.DB.Query(query, args...) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() projects := []Project{} for rows.Next() { var p Project if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.CustomerID, &p.Status, &p.Budget, &p.Spent, &p.Currency, &p.StartDate, &p.EndDate, &p.ManagerID, &p.CreatedAt); err != nil { continue } if p.Budget > 0 { p.Progress = (p.Spent / p.Budget) * 100 } projects = append(projects, p) } writeJSON(w, http.StatusOK, map[string]interface{}{ "projects": projects, "total": len(projects), }) } func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { var req Project if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } var id string err := h.DB.QueryRow(` INSERT INTO boc_projects (name, description, customer_id, status, budget, currency, start_date, end_date, manager_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id `, req.Name, req.Description, req.CustomerID, req.Status, req.Budget, req.Currency, req.StartDate, req.EndDate, req.ManagerID).Scan(&id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create project") return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": id, "message": "Project created", }) } func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var p Project err := h.DB.QueryRow(` SELECT id, name, description, customer_id, status, budget, spent, currency, start_date, end_date, manager_id, created_at FROM boc_projects WHERE id = $1 `, id).Scan(&p.ID, &p.Name, &p.Description, &p.CustomerID, &p.Status, &p.Budget, &p.Spent, &p.Currency, &p.StartDate, &p.EndDate, &p.ManagerID, &p.CreatedAt) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "project not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } if p.Budget > 0 { p.Progress = (p.Spent / p.Budget) * 100 } // Get time entries rows, err := h.DB.Query(` SELECT pt.id, pt.project_id, pt.employee_id, e.first_name || ' ' || e.last_name, pt.date, pt.hours, pt.description, pt.billable, pt.hourly_rate FROM boc_project_times pt JOIN boc_employees e ON pt.employee_id = e.id WHERE pt.project_id = $1 ORDER BY pt.date DESC `, id) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() times := []ProjectTime{} for rows.Next() { var t ProjectTime if err := rows.Scan(&t.ID, &t.ProjectID, &t.EmployeeID, &t.EmployeeName, &t.Date, &t.Hours, &t.Description, &t.Billable, &t.HourlyRate); err != nil { continue } times = append(times, t) } // Get expenses expenseRows, err := h.DB.Query(` SELECT e.id, e.category, e.description, e.amount, e.created_at FROM boc_project_expenses pe JOIN boc_expenses e ON pe.expense_id = e.id WHERE pe.project_id = $1 `, id) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer expenseRows.Close() expenses := []map[string]interface{}{} for expenseRows.Next() { var eID, category, description string var amount float64 var createdAt time.Time if err := expenseRows.Scan(&eID, &category, &description, &amount, &createdAt); err != nil { continue } expenses = append(expenses, map[string]interface{}{ "id": eID, "category": category, "description": description, "amount": amount, "created_at": createdAt, }) } writeJSON(w, http.StatusOK, map[string]interface{}{ "project": p, "times": times, "expenses": expenses, }) } func (h *ProjectHandler) AddTime(w http.ResponseWriter, r *http.Request) { projectID := chi.URLParam(r, "id") var req ProjectTime if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } var id string err := h.DB.QueryRow(` INSERT INTO boc_project_times (project_id, employee_id, date, hours, description, billable, hourly_rate) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id `, projectID, req.EmployeeID, req.Date, req.Hours, req.Description, req.Billable, req.HourlyRate).Scan(&id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to add time") return } // Update project spent h.DB.Exec(` UPDATE boc_projects SET spent = ( SELECT COALESCE(SUM(pt.hours * pt.hourly_rate), 0) + COALESCE(SUM(pe.amount), 0) FROM boc_project_times pt LEFT JOIN boc_project_expenses pe ON pe.project_id = pt.project_id WHERE pt.project_id = $1 ) WHERE id = $1 `, projectID) writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": id, "message": "Time entry added", }) } func (h *ProjectHandler) GetProjectSummary(w http.ResponseWriter, r *http.Request) { // Summary across all projects var totalBudget, totalSpent float64 err := h.DB.QueryRow(` SELECT COALESCE(SUM(budget), 0), COALESCE(SUM(spent), 0) FROM boc_projects WHERE status = 'active' `).Scan(&totalBudget, &totalSpent) if err != nil { totalBudget, totalSpent = 0, 0 } var totalHours float64 err = h.DB.QueryRow(` SELECT COALESCE(SUM(hours), 0) FROM boc_project_times pt JOIN boc_projects p ON pt.project_id = p.id WHERE p.status = 'active' `).Scan(&totalHours) if err != nil { totalHours = 0 } writeJSON(w, http.StatusOK, map[string]interface{}{ "total_budget": totalBudget, "total_spent": totalSpent, "remaining": totalBudget - totalSpent, "utilization": map[string]interface{}{ "percentage": map[bool]float64{true: (totalSpent / totalBudget) * 100, false: 0}[totalBudget > 0], }, "total_hours": totalHours, }) }