6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
150 lines
5.2 KiB
Go
150 lines
5.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"aamos-admin/models"
|
|
)
|
|
|
|
var moduleOrder = []string{
|
|
"academy", "finance", "compliance", "people", "operations",
|
|
"commerce", "identity", "connect", "governance", "analytics",
|
|
}
|
|
|
|
var moduleSeed = []models.Module{
|
|
{ID: "academy", Name: "Academy", Description: "Learning & Training Platform", Icon: "graduation-cap", Status: models.ModuleStatusStopped},
|
|
{ID: "finance", Name: "Finance", Description: "Financial Management & Reporting", Icon: "chart-line", Status: models.ModuleStatusStopped},
|
|
{ID: "compliance", Name: "Compliance", Description: "Regulatory Compliance & Auditing", Icon: "shield-check", Status: models.ModuleStatusStopped},
|
|
{ID: "people", Name: "People", Description: "HR & People Management", Icon: "users", Status: models.ModuleStatusStopped},
|
|
{ID: "operations", Name: "Operations", Description: "Operations & Process Management", Icon: "cogs", Status: models.ModuleStatusStopped},
|
|
{ID: "commerce", Name: "Commerce", Description: "Sales & Commerce Platform", Icon: "shopping-cart", Status: models.ModuleStatusStopped},
|
|
{ID: "identity", Name: "Identity", Description: "Identity & Access Management", Icon: "fingerprint", Status: models.ModuleStatusStopped},
|
|
{ID: "connect", Name: "Connect", Description: "Communications & Integration Hub", Icon: "network-wired", Status: models.ModuleStatusStopped},
|
|
{ID: "governance", Name: "Governance", Description: "Corporate Governance & Policy", Icon: "landmark", Status: models.ModuleStatusStopped},
|
|
{ID: "analytics", Name: "Analytics", Description: "Data Analytics & Business Intelligence", Icon: "chart-bar", Status: models.ModuleStatusStopped},
|
|
}
|
|
|
|
// ModulesHandler handles GET /api/v1/modules and PUT /api/v1/modules/{id}/toggle.
|
|
type ModulesHandler struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewModulesHandler seeds the 10 Ouroboros modules (idempotent) and returns the handler.
|
|
func NewModulesHandler(db *sql.DB) (*ModulesHandler, error) {
|
|
h := &ModulesHandler{db: db}
|
|
if err := h.seed(); err != nil {
|
|
return nil, fmt.Errorf("modules seed: %w", err)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
func (h *ModulesHandler) seed() error {
|
|
for _, m := range moduleSeed {
|
|
_, err := h.db.Exec(
|
|
`INSERT INTO modules (id, name, enabled, description, icon, status)
|
|
VALUES ($1, $2, FALSE, $3, $4, $5)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
m.ID, m.Name, m.Description, m.Icon, string(m.Status),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("seed %s: %w", m.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List handles GET /api/v1/modules — returns all 10 modules in canonical Ouroboros order.
|
|
func (h *ModulesHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
rows, err := h.db.Query(
|
|
`SELECT id, name, enabled, description, icon, status FROM modules`,
|
|
)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("db query: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
byID := make(map[string]models.Module, len(moduleOrder))
|
|
for rows.Next() {
|
|
var m models.Module
|
|
var status string
|
|
if err := rows.Scan(&m.ID, &m.Name, &m.Enabled, &m.Description, &m.Icon, &status); err != nil {
|
|
http.Error(w, fmt.Sprintf("db scan: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
m.Status = models.ModuleStatus(status)
|
|
byID[m.ID] = m
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
http.Error(w, fmt.Sprintf("db rows: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
result := make([]models.Module, 0, len(moduleOrder))
|
|
for _, id := range moduleOrder {
|
|
if m, ok := byID[id]; ok {
|
|
result = append(result, m)
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// Toggle handles PUT /api/v1/modules/{id}/toggle — flips enabled and syncs status.
|
|
func (h *ModulesHandler) Toggle(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Extract {id} from /api/v1/modules/{id}/toggle
|
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
|
if len(parts) < 2 || parts[len(parts)-1] != "toggle" {
|
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id := parts[len(parts)-2]
|
|
if id == "" || id == "modules" {
|
|
http.Error(w, "missing module id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// SET expressions use original row values, so NOT enabled correctly toggles:
|
|
// false → true + 'running', true → false + 'stopped'.
|
|
var m models.Module
|
|
var status string
|
|
err := h.db.QueryRow(
|
|
`UPDATE modules
|
|
SET enabled = NOT enabled,
|
|
status = CASE WHEN NOT enabled THEN $2 ELSE $3 END
|
|
WHERE id = $1
|
|
RETURNING id, name, enabled, description, icon, status`,
|
|
id,
|
|
string(models.ModuleStatusRunning),
|
|
string(models.ModuleStatusStopped),
|
|
).Scan(&m.ID, &m.Name, &m.Enabled, &m.Description, &m.Icon, &status)
|
|
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "module not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("db update: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
m.Status = models.ModuleStatus(status)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(m)
|
|
}
|