Files
silo/internal/db/integrations.go
Forbes 7550b78740 feat: Infor-style split-panel layout, projects page, fuzzy search, Odoo scaffold
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
2026-01-31 09:20:27 -06:00

140 lines
3.7 KiB
Go

package db
import (
"context"
"encoding/json"
"time"
"github.com/jackc/pgx/v5"
)
// Integration represents an ERP integration configuration.
type Integration struct {
ID string
Name string
Enabled bool
Config map[string]any
CreatedAt time.Time
UpdatedAt time.Time
}
// SyncLog represents a sync log entry.
type SyncLog struct {
ID string
IntegrationID string
ItemID *string
Direction string
Status string
ExternalID string
ExternalModel string
RequestPayload json.RawMessage
ResponsePayload json.RawMessage
ErrorMessage string
StartedAt *time.Time
CompletedAt *time.Time
CreatedAt time.Time
}
// IntegrationRepository provides integration database operations.
type IntegrationRepository struct {
db *DB
}
// NewIntegrationRepository creates a new integration repository.
func NewIntegrationRepository(db *DB) *IntegrationRepository {
return &IntegrationRepository{db: db}
}
// GetByName returns an integration by name.
func (r *IntegrationRepository) GetByName(ctx context.Context, name string) (*Integration, error) {
row := r.db.pool.QueryRow(ctx, `
SELECT id, name, enabled, config, created_at, updated_at
FROM integrations
WHERE name = $1
`, name)
var i Integration
var configJSON []byte
err := row.Scan(&i.ID, &i.Name, &i.Enabled, &configJSON, &i.CreatedAt, &i.UpdatedAt)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if len(configJSON) > 0 {
if err := json.Unmarshal(configJSON, &i.Config); err != nil {
return nil, err
}
}
return &i, nil
}
// Upsert creates or updates an integration by name.
func (r *IntegrationRepository) Upsert(ctx context.Context, name string, enabled bool, config map[string]any) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
_, err = r.db.pool.Exec(ctx, `
INSERT INTO integrations (name, enabled, config)
VALUES ($1, $2, $3)
ON CONFLICT (name) DO UPDATE SET
enabled = EXCLUDED.enabled,
config = EXCLUDED.config,
updated_at = now()
`, name, enabled, configJSON)
return err
}
// CreateSyncLog inserts a new sync log entry.
func (r *IntegrationRepository) CreateSyncLog(ctx context.Context, entry *SyncLog) error {
_, err := r.db.pool.Exec(ctx, `
INSERT INTO sync_log (integration_id, item_id, direction, status, external_id, external_model, request_payload, response_payload, error_message, started_at, completed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, entry.IntegrationID, entry.ItemID, entry.Direction, entry.Status,
entry.ExternalID, entry.ExternalModel, entry.RequestPayload, entry.ResponsePayload,
entry.ErrorMessage, entry.StartedAt, entry.CompletedAt)
return err
}
// ListSyncLog returns recent sync log entries for an integration.
func (r *IntegrationRepository) ListSyncLog(ctx context.Context, integrationID string, limit int) ([]*SyncLog, error) {
if limit <= 0 {
limit = 50
}
rows, err := r.db.pool.Query(ctx, `
SELECT id, integration_id, item_id, direction, status,
external_id, external_model, request_payload, response_payload,
error_message, started_at, completed_at, created_at
FROM sync_log
WHERE integration_id = $1
ORDER BY created_at DESC
LIMIT $2
`, integrationID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []*SyncLog
for rows.Next() {
var l SyncLog
err := rows.Scan(
&l.ID, &l.IntegrationID, &l.ItemID, &l.Direction, &l.Status,
&l.ExternalID, &l.ExternalModel, &l.RequestPayload, &l.ResponsePayload,
&l.ErrorMessage, &l.StartedAt, &l.CompletedAt, &l.CreatedAt,
)
if err != nil {
return nil, err
}
logs = append(logs, &l)
}
return logs, rows.Err()
}