Web UI - Infor CloudSuite-style split-panel layout (items.html rewrite): - Replace modal-based item detail with inline split-panel workspace - Horizontal mode: item list on left, tabbed detail panel on right - Vertical mode: detail panel on top, item list below - Detail tabs: Main, Properties, Revisions, BOM, Where Used - Ctrl+F opens in-page filter overlay with fuzzy search - Column config gear icon with per-layout-mode persistence - Search scope toggle pills (All / Part Number / Description) - Selected row highlight with accent border - Responsive breakpoint forces vertical below 900px - Create/Edit/Delete remain as modal dialogs Web UI - Projects page: - New projects.html template with full CRUD - Project table: Code, Name, Description, Item count, Created, Actions - Create/Edit/Delete modals - Click project code navigates to items filtered by project - 3-tab navigation in base.html: Items, Projects, Schemas Fuzzy search: - Add sahilm/fuzzy dependency for ranked text matching - New internal/api/search.go with SearchableItems fuzzy.Source - GET /api/items/search endpoint with field scope and type/project filters - Frontend routes to fuzzy endpoint when search input is non-empty Odoo ERP integration scaffold: - Migration 008: integrations and sync_log tables - internal/odoo/ package: types, client stubs, sync stubs - internal/db/integrations.go: IntegrationRepository - internal/config/config.go: OdooConfig struct - 6 API endpoints for config CRUD, sync log, test, push, pull - All sync operations return stub responses Documentation: - docs/REPOSITORY_STATUS.md: comprehensive repository state report with architecture overview, API surface, feature stubs, and potential issues analysis
186 lines
5.2 KiB
Go
186 lines
5.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/kindredsystems/silo/internal/db"
|
|
)
|
|
|
|
// OdooConfigResponse represents the Odoo configuration (API key masked).
|
|
type OdooConfigResponse struct {
|
|
Enabled bool `json:"enabled"`
|
|
URL string `json:"url"`
|
|
Database string `json:"database"`
|
|
Username string `json:"username"`
|
|
APIKey string `json:"api_key"`
|
|
}
|
|
|
|
// OdooConfigRequest represents a request to update Odoo configuration.
|
|
type OdooConfigRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
URL string `json:"url"`
|
|
Database string `json:"database"`
|
|
Username string `json:"username"`
|
|
APIKey string `json:"api_key,omitempty"`
|
|
}
|
|
|
|
// HandleGetOdooConfig returns the current Odoo integration configuration.
|
|
func (s *Server) HandleGetOdooConfig(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
repo := db.NewIntegrationRepository(s.db)
|
|
integration, err := repo.GetByName(ctx, "odoo")
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("failed to get odoo config")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get Odoo config")
|
|
return
|
|
}
|
|
|
|
if integration == nil {
|
|
writeJSON(w, http.StatusOK, OdooConfigResponse{})
|
|
return
|
|
}
|
|
|
|
resp := OdooConfigResponse{
|
|
Enabled: integration.Enabled,
|
|
URL: getString(integration.Config, "url"),
|
|
Database: getString(integration.Config, "database"),
|
|
Username: getString(integration.Config, "username"),
|
|
}
|
|
|
|
// Mask API key
|
|
apiKey := getString(integration.Config, "api_key")
|
|
if len(apiKey) > 4 {
|
|
resp.APIKey = strings.Repeat("*", len(apiKey)-4) + apiKey[len(apiKey)-4:]
|
|
} else if apiKey != "" {
|
|
resp.APIKey = "****"
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// HandleUpdateOdooConfig updates the Odoo integration configuration.
|
|
func (s *Server) HandleUpdateOdooConfig(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var req OdooConfigRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_json", err.Error())
|
|
return
|
|
}
|
|
|
|
repo := db.NewIntegrationRepository(s.db)
|
|
|
|
// If API key is masked or empty, preserve existing one
|
|
if req.APIKey == "" || strings.Contains(req.APIKey, "*") {
|
|
existing, err := repo.GetByName(ctx, "odoo")
|
|
if err == nil && existing != nil {
|
|
req.APIKey = getString(existing.Config, "api_key")
|
|
}
|
|
}
|
|
|
|
config := map[string]any{
|
|
"url": req.URL,
|
|
"database": req.Database,
|
|
"username": req.Username,
|
|
"api_key": req.APIKey,
|
|
}
|
|
|
|
if err := repo.Upsert(ctx, "odoo", req.Enabled, config); err != nil {
|
|
s.logger.Error().Err(err).Msg("failed to update odoo config")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update Odoo config")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
// HandleGetOdooSyncLog returns recent sync log entries for the Odoo integration.
|
|
func (s *Server) HandleGetOdooSyncLog(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
repo := db.NewIntegrationRepository(s.db)
|
|
integration, err := repo.GetByName(ctx, "odoo")
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("failed to get odoo integration")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get sync log")
|
|
return
|
|
}
|
|
|
|
if integration == nil {
|
|
writeJSON(w, http.StatusOK, []any{})
|
|
return
|
|
}
|
|
|
|
logs, err := repo.ListSyncLog(ctx, integration.ID, 50)
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("failed to list sync log")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list sync log")
|
|
return
|
|
}
|
|
|
|
if logs == nil {
|
|
logs = []*db.SyncLog{}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, logs)
|
|
}
|
|
|
|
// HandleTestOdooConnection tests the Odoo connection.
|
|
func (s *Server) HandleTestOdooConnection(w http.ResponseWriter, r *http.Request) {
|
|
// Stub - would use odoo.Client to test connection
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "not_implemented",
|
|
"message": "Odoo connection testing is not yet implemented",
|
|
})
|
|
}
|
|
|
|
// HandleOdooPush pushes an item to Odoo.
|
|
func (s *Server) HandleOdooPush(w http.ResponseWriter, r *http.Request) {
|
|
partNumber := chi.URLParam(r, "partNumber")
|
|
if partNumber == "" {
|
|
writeError(w, http.StatusBadRequest, "missing_param", "Part number is required")
|
|
return
|
|
}
|
|
|
|
// Stub - log the intent
|
|
s.logger.Info().Str("part_number", partNumber).Msg("odoo push requested (stub)")
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "pending",
|
|
"message": "Odoo push is not yet implemented",
|
|
"part_number": partNumber,
|
|
})
|
|
}
|
|
|
|
// HandleOdooPull pulls a product from Odoo.
|
|
func (s *Server) HandleOdooPull(w http.ResponseWriter, r *http.Request) {
|
|
odooID := chi.URLParam(r, "odooId")
|
|
if odooID == "" {
|
|
writeError(w, http.StatusBadRequest, "missing_param", "Odoo ID is required")
|
|
return
|
|
}
|
|
|
|
// Stub - log the intent
|
|
s.logger.Info().Str("odoo_id", odooID).Msg("odoo pull requested (stub)")
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "pending",
|
|
"message": "Odoo pull is not yet implemented",
|
|
"odoo_id": odooID,
|
|
})
|
|
}
|
|
|
|
// getString safely extracts a string from a map.
|
|
func getString(m map[string]any, key string) string {
|
|
if v, ok := m[key]; ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|