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
140 lines
3.7 KiB
Go
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()
|
|
}
|