feat(timeline): fix chronology, status resolution, and component history UI
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"reanimator/internal/domain"
|
||||
)
|
||||
@@ -17,6 +18,12 @@ type ComponentObservationMeta struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
type ComponentInstallationHistoryItem struct {
|
||||
AssetID string
|
||||
InstalledAt time.Time
|
||||
RemovedAt *time.Time
|
||||
}
|
||||
|
||||
func NewInstallationRepository(db *sql.DB) *InstallationRepository {
|
||||
return &InstallationRepository{db: db}
|
||||
}
|
||||
@@ -58,6 +65,56 @@ func (r *InstallationRepository) ListCurrentComponentsByAsset(ctx context.Contex
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *InstallationRepository) ListPreviousComponentsByAsset(ctx context.Context, assetID string) ([]domain.Component, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT c.id, c.vendor, c.model, c.vendor_serial, c.first_seen_at, c.created_at, c.updated_at
|
||||
FROM installations i
|
||||
JOIN parts c ON c.id = i.part_id
|
||||
WHERE i.machine_id = ?
|
||||
AND i.removed_at IS NOT NULL
|
||||
AND i.id = (
|
||||
SELECT i2.id
|
||||
FROM installations i2
|
||||
WHERE i2.machine_id = i.machine_id AND i2.part_id = i.part_id
|
||||
ORDER BY
|
||||
CASE WHEN i2.removed_at IS NULL THEN 1 ELSE 0 END DESC,
|
||||
i2.removed_at DESC,
|
||||
i2.installed_at DESC,
|
||||
i2.created_at DESC,
|
||||
i2.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
ORDER BY c.created_at DESC`,
|
||||
assetID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]domain.Component, 0)
|
||||
for rows.Next() {
|
||||
var component domain.Component
|
||||
var vendor sql.NullString
|
||||
var model sql.NullString
|
||||
var firstSeenAt sql.NullTime
|
||||
|
||||
if err := rows.Scan(&component.ID, &vendor, &model, &component.VendorSerial, &firstSeenAt, &component.CreatedAt, &component.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
component.Vendor = nullStringToPtr(vendor)
|
||||
component.Model = nullStringToPtr(model)
|
||||
component.FirstSeenAt = nullTimeToPtr(firstSeenAt)
|
||||
items = append(items, component)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *InstallationRepository) ListCurrentComponentIDsByAssets(ctx context.Context, assetIDs []string) (map[string][]string, error) {
|
||||
result := make(map[string][]string, len(assetIDs))
|
||||
if len(assetIDs) == 0 {
|
||||
@@ -223,3 +280,33 @@ func (r *InstallationRepository) GetCurrentAssetIDByComponent(ctx context.Contex
|
||||
}
|
||||
return &assetID, nil
|
||||
}
|
||||
|
||||
func (r *InstallationRepository) ListInstallationHistoryByComponent(ctx context.Context, componentID string) ([]ComponentInstallationHistoryItem, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT machine_id, installed_at, removed_at
|
||||
FROM installations
|
||||
WHERE part_id = ?
|
||||
ORDER BY installed_at DESC, created_at DESC, id DESC`,
|
||||
componentID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]ComponentInstallationHistoryItem, 0)
|
||||
for rows.Next() {
|
||||
var item ComponentInstallationHistoryItem
|
||||
var removedAt sql.NullTime
|
||||
if err := rows.Scan(&item.AssetID, &item.InstalledAt, &removedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.RemovedAt = nullTimeToPtr(removedAt)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -104,28 +104,54 @@ func (r *EventRepository) List(ctx context.Context, subjectType string, subjectI
|
||||
}
|
||||
|
||||
func (r *EventRepository) ListLatestBySubjects(ctx context.Context, subjectType string, ids []string) (map[string]domain.TimelineEvent, error) {
|
||||
return r.listLatestBySubjects(ctx, subjectType, ids, nil)
|
||||
}
|
||||
|
||||
func (r *EventRepository) ListLatestBySubjectsAndEventTypes(ctx context.Context, subjectType string, ids []string, eventTypes []string) (map[string]domain.TimelineEvent, error) {
|
||||
return r.listLatestBySubjects(ctx, subjectType, ids, eventTypes)
|
||||
}
|
||||
|
||||
func (r *EventRepository) listLatestBySubjects(ctx context.Context, subjectType string, ids []string, eventTypes []string) (map[string]domain.TimelineEvent, error) {
|
||||
result := make(map[string]domain.TimelineEvent, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
placeholders := make([]string, len(ids))
|
||||
args := make([]any, 0, len(ids)+1)
|
||||
args := make([]any, 0, len(ids)+len(eventTypes)+2)
|
||||
args = append(args, subjectType)
|
||||
for i, id := range ids {
|
||||
placeholders[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
eventFilter := ""
|
||||
eventFilterInner := ""
|
||||
if len(eventTypes) > 0 {
|
||||
eventPlaceholdersOuter := make([]string, len(eventTypes))
|
||||
for i, eventType := range eventTypes {
|
||||
eventPlaceholdersOuter[i] = "?"
|
||||
args = append(args, eventType)
|
||||
}
|
||||
eventPlaceholdersInner := make([]string, len(eventTypes))
|
||||
for i, eventType := range eventTypes {
|
||||
eventPlaceholdersInner[i] = "?"
|
||||
args = append(args, eventType)
|
||||
}
|
||||
eventFilter = " AND t.event_type IN (" + strings.Join(eventPlaceholdersOuter, ",") + ")"
|
||||
eventFilterInner = " AND t2.event_type IN (" + strings.Join(eventPlaceholdersInner, ",") + ")"
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT t.id, t.subject_id, t.event_type, t.event_time, t.machine_id, t.part_id, t.firmware_version, t.created_at
|
||||
FROM timeline_events t
|
||||
WHERE t.subject_type = ? AND t.subject_id IN (` + strings.Join(placeholders, ",") + `)
|
||||
WHERE t.subject_type = ? AND t.subject_id IN (` + strings.Join(placeholders, ",") + `)` + eventFilter + `
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM timeline_events t2
|
||||
WHERE t2.subject_type = t.subject_type
|
||||
AND t2.subject_id = t.subject_id
|
||||
` + eventFilterInner + `
|
||||
AND (
|
||||
t2.event_time > t.event_time
|
||||
OR (t2.event_time = t.event_time AND t2.id > t.id)
|
||||
|
||||
Reference in New Issue
Block a user