feat(boc): v1.0 - Complete Business Operations Center
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user