- Add migration 013 to copy sourcing_link/standard_cost values into current revision properties JSONB and drop the columns from items table - Remove SourcingLink/StandardCost from Go Item struct and all DB queries (items.go, audit_queries.go, projects.go) - Remove from API request/response structs and handlers - Update CSV/ODS/BOM export/import to read these from revision properties - Update audit handlers to score as regular property fields - Remove from frontend Item type and hardcoded form fields - MainTab now reads sourcing_link/standard_cost from item.properties - CreateItemPane/EditItemPane no longer have dedicated fields for these; they will be rendered as schema-driven property fields
305 lines
7.7 KiB
Go
305 lines
7.7 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// Project represents a project in the database.
|
|
type Project struct {
|
|
ID string
|
|
Code string
|
|
Name string
|
|
Description string
|
|
CreatedAt time.Time
|
|
CreatedBy *string
|
|
}
|
|
|
|
// ProjectRepository provides project database operations.
|
|
type ProjectRepository struct {
|
|
db *DB
|
|
}
|
|
|
|
// NewProjectRepository creates a new project repository.
|
|
func NewProjectRepository(db *DB) *ProjectRepository {
|
|
return &ProjectRepository{db: db}
|
|
}
|
|
|
|
// List returns all projects.
|
|
func (r *ProjectRepository) List(ctx context.Context) ([]*Project, error) {
|
|
rows, err := r.db.pool.Query(ctx, `
|
|
SELECT id, code, name, description, created_at
|
|
FROM projects
|
|
ORDER BY code
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var projects []*Project
|
|
for rows.Next() {
|
|
p := &Project{}
|
|
var name, desc *string
|
|
if err := rows.Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if name != nil {
|
|
p.Name = *name
|
|
}
|
|
if desc != nil {
|
|
p.Description = *desc
|
|
}
|
|
projects = append(projects, p)
|
|
}
|
|
|
|
return projects, rows.Err()
|
|
}
|
|
|
|
// GetByCode returns a project by its code.
|
|
func (r *ProjectRepository) GetByCode(ctx context.Context, code string) (*Project, error) {
|
|
p := &Project{}
|
|
var name, desc *string
|
|
err := r.db.pool.QueryRow(ctx, `
|
|
SELECT id, code, name, description, created_at
|
|
FROM projects
|
|
WHERE code = $1
|
|
`, code).Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt)
|
|
|
|
if err == pgx.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if name != nil {
|
|
p.Name = *name
|
|
}
|
|
if desc != nil {
|
|
p.Description = *desc
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// GetByID returns a project by its ID.
|
|
func (r *ProjectRepository) GetByID(ctx context.Context, id string) (*Project, error) {
|
|
p := &Project{}
|
|
var name, desc *string
|
|
err := r.db.pool.QueryRow(ctx, `
|
|
SELECT id, code, name, description, created_at
|
|
FROM projects
|
|
WHERE id = $1
|
|
`, id).Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt)
|
|
|
|
if err == pgx.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if name != nil {
|
|
p.Name = *name
|
|
}
|
|
if desc != nil {
|
|
p.Description = *desc
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Create inserts a new project.
|
|
func (r *ProjectRepository) Create(ctx context.Context, p *Project) error {
|
|
return r.db.pool.QueryRow(ctx, `
|
|
INSERT INTO projects (code, name, description, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, created_at
|
|
`, p.Code, nullIfEmpty(p.Name), nullIfEmpty(p.Description), p.CreatedBy).Scan(&p.ID, &p.CreatedAt)
|
|
}
|
|
|
|
// Update updates a project's name and description.
|
|
func (r *ProjectRepository) Update(ctx context.Context, code string, name, description string) error {
|
|
_, err := r.db.pool.Exec(ctx, `
|
|
UPDATE projects
|
|
SET name = $2, description = $3
|
|
WHERE code = $1
|
|
`, code, nullIfEmpty(name), nullIfEmpty(description))
|
|
return err
|
|
}
|
|
|
|
// Delete removes a project (will cascade to item_projects).
|
|
func (r *ProjectRepository) Delete(ctx context.Context, code string) error {
|
|
_, err := r.db.pool.Exec(ctx, `DELETE FROM projects WHERE code = $1`, code)
|
|
return err
|
|
}
|
|
|
|
// AddItemToProject associates an item with a project.
|
|
func (r *ProjectRepository) AddItemToProject(ctx context.Context, itemID, projectID string) error {
|
|
_, err := r.db.pool.Exec(ctx, `
|
|
INSERT INTO item_projects (item_id, project_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (item_id, project_id) DO NOTHING
|
|
`, itemID, projectID)
|
|
return err
|
|
}
|
|
|
|
// AddItemToProjectByCode associates an item with a project by code.
|
|
func (r *ProjectRepository) AddItemToProjectByCode(ctx context.Context, itemID, projectCode string) error {
|
|
_, err := r.db.pool.Exec(ctx, `
|
|
INSERT INTO item_projects (item_id, project_id)
|
|
SELECT $1, id FROM projects WHERE code = $2
|
|
ON CONFLICT (item_id, project_id) DO NOTHING
|
|
`, itemID, projectCode)
|
|
return err
|
|
}
|
|
|
|
// RemoveItemFromProject removes an item's association with a project.
|
|
func (r *ProjectRepository) RemoveItemFromProject(ctx context.Context, itemID, projectID string) error {
|
|
_, err := r.db.pool.Exec(ctx, `
|
|
DELETE FROM item_projects
|
|
WHERE item_id = $1 AND project_id = $2
|
|
`, itemID, projectID)
|
|
return err
|
|
}
|
|
|
|
// RemoveItemFromProjectByCode removes an item's association with a project by code.
|
|
func (r *ProjectRepository) RemoveItemFromProjectByCode(ctx context.Context, itemID, projectCode string) error {
|
|
_, err := r.db.pool.Exec(ctx, `
|
|
DELETE FROM item_projects
|
|
WHERE item_id = $1 AND project_id = (SELECT id FROM projects WHERE code = $2)
|
|
`, itemID, projectCode)
|
|
return err
|
|
}
|
|
|
|
// GetProjectsForItem returns all projects associated with an item.
|
|
func (r *ProjectRepository) GetProjectsForItem(ctx context.Context, itemID string) ([]*Project, error) {
|
|
rows, err := r.db.pool.Query(ctx, `
|
|
SELECT p.id, p.code, p.name, p.description, p.created_at
|
|
FROM projects p
|
|
JOIN item_projects ip ON ip.project_id = p.id
|
|
WHERE ip.item_id = $1
|
|
ORDER BY p.code
|
|
`, itemID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var projects []*Project
|
|
for rows.Next() {
|
|
p := &Project{}
|
|
var name, desc *string
|
|
if err := rows.Scan(&p.ID, &p.Code, &name, &desc, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if name != nil {
|
|
p.Name = *name
|
|
}
|
|
if desc != nil {
|
|
p.Description = *desc
|
|
}
|
|
projects = append(projects, p)
|
|
}
|
|
|
|
return projects, rows.Err()
|
|
}
|
|
|
|
// GetProjectCodesForItem returns project codes for an item (convenience method).
|
|
func (r *ProjectRepository) GetProjectCodesForItem(ctx context.Context, itemID string) ([]string, error) {
|
|
rows, err := r.db.pool.Query(ctx, `
|
|
SELECT p.code
|
|
FROM projects p
|
|
JOIN item_projects ip ON ip.project_id = p.id
|
|
WHERE ip.item_id = $1
|
|
ORDER BY p.code
|
|
`, itemID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var codes []string
|
|
for rows.Next() {
|
|
var code string
|
|
if err := rows.Scan(&code); err != nil {
|
|
return nil, err
|
|
}
|
|
codes = append(codes, code)
|
|
}
|
|
|
|
return codes, rows.Err()
|
|
}
|
|
|
|
// GetItemsForProject returns all items associated with a project.
|
|
func (r *ProjectRepository) GetItemsForProject(ctx context.Context, projectID string) ([]*Item, error) {
|
|
rows, err := r.db.pool.Query(ctx, `
|
|
SELECT i.id, i.part_number, i.schema_id, i.item_type, i.description,
|
|
i.created_at, i.updated_at, i.archived_at, i.current_revision,
|
|
i.cad_synced_at, i.cad_file_path,
|
|
i.sourcing_type, i.long_description,
|
|
i.thumbnail_key
|
|
FROM items i
|
|
JOIN item_projects ip ON ip.item_id = i.id
|
|
WHERE ip.project_id = $1 AND i.archived_at IS NULL
|
|
ORDER BY i.part_number
|
|
`, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var items []*Item
|
|
for rows.Next() {
|
|
item := &Item{}
|
|
if err := rows.Scan(
|
|
&item.ID, &item.PartNumber, &item.SchemaID, &item.ItemType, &item.Description,
|
|
&item.CreatedAt, &item.UpdatedAt, &item.ArchivedAt, &item.CurrentRevision,
|
|
&item.CADSyncedAt, &item.CADFilePath,
|
|
&item.SourcingType, &item.LongDescription,
|
|
&item.ThumbnailKey,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
return items, rows.Err()
|
|
}
|
|
|
|
// SetItemProjects replaces all project associations for an item.
|
|
func (r *ProjectRepository) SetItemProjects(ctx context.Context, itemID string, projectCodes []string) error {
|
|
return r.db.Tx(ctx, func(tx pgx.Tx) error {
|
|
// Remove existing associations
|
|
_, err := tx.Exec(ctx, `DELETE FROM item_projects WHERE item_id = $1`, itemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add new associations
|
|
for _, code := range projectCodes {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO item_projects (item_id, project_id)
|
|
SELECT $1, id FROM projects WHERE code = $2
|
|
ON CONFLICT (item_id, project_id) DO NOTHING
|
|
`, itemID, code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// helper function
|
|
func nullIfEmpty(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|