Files
boc/aamos-admin-upgrade/backend/handlers/modules.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- 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
2026-07-08 19:56:03 +00:00

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)
}