chore: auto-commit (28 archivos)
- app.md - auth.go - chat.go - chat.log - db.go - frontend/package.json - frontend/pnpm-lock.yaml - frontend/src/App.tsx - frontend/src/Root.tsx - frontend/src/api.ts - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,21 +19,27 @@ type Column struct {
|
||||
Position int `json:"position"`
|
||||
Location string `json:"location"`
|
||||
Width int `json:"width"`
|
||||
WIPLimit int `json:"wip_limit"`
|
||||
IsDone bool `json:"is_done"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
ID string `json:"id"`
|
||||
Requester string `json:"requester"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
EnteredAt string `json:"entered_at"`
|
||||
TimeInColumn int64 `json:"time_in_column_ms"`
|
||||
ID string `json:"id"`
|
||||
Requester string `json:"requester"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
Locked bool `json:"locked"`
|
||||
AssigneeID *string `json:"assignee_id"`
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
EnteredAt string `json:"entered_at"`
|
||||
TimeInColumn int64 `json:"time_in_column_ms"`
|
||||
}
|
||||
|
||||
type HistoryEntry struct {
|
||||
@@ -46,6 +52,21 @@ type HistoryEntry struct {
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
type LockPeriod struct {
|
||||
ID string `json:"id"`
|
||||
CardID string `json:"card_id"`
|
||||
LockedAt string `json:"locked_at"`
|
||||
UnlockedAt *string `json:"unlocked_at"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
type CardHistoryResponse struct {
|
||||
ColumnHistory []HistoryEntry `json:"column_history"`
|
||||
LockPeriods []LockPeriod `json:"lock_periods"`
|
||||
TotalLockedMs int64 `json:"total_locked_ms"`
|
||||
CurrentlyLock bool `json:"currently_locked"`
|
||||
}
|
||||
|
||||
type DB struct{ conn *sql.DB }
|
||||
|
||||
func openDB(path string) (*DB, error) {
|
||||
@@ -73,7 +94,15 @@ func ensureColumns(conn *sql.DB) error {
|
||||
specs := []colSpec{
|
||||
{"columns", "location", "TEXT NOT NULL DEFAULT 'board'"},
|
||||
{"columns", "width", "INTEGER NOT NULL DEFAULT 300"},
|
||||
{"columns", "wip_limit", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"columns", "is_done", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"cards", "color", "TEXT NOT NULL DEFAULT ''"},
|
||||
{"cards", "locked", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"cards", "assignee_id", "TEXT"},
|
||||
{"cards", "completed_at", "TEXT"},
|
||||
{"cards", "deleted_at", "TEXT"},
|
||||
{"card_column_history", "actor_id", "TEXT"},
|
||||
{"card_lock_history", "actor_id", "TEXT"},
|
||||
}
|
||||
for _, s := range specs {
|
||||
exists, err := columnExists(conn, s.table, s.name)
|
||||
@@ -87,6 +116,9 @@ func ensureColumns(conn *sql.DB) error {
|
||||
return fmt.Errorf("add %s.%s: %w", s.table, s.name, err)
|
||||
}
|
||||
}
|
||||
if _, err := conn.Exec(`CREATE INDEX IF NOT EXISTS idx_cards_assignee ON cards(assignee_id)`); err != nil {
|
||||
return fmt.Errorf("create assignee index: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,10 +156,17 @@ func newID() string {
|
||||
|
||||
func nowRFC3339() string { return time.Now().UTC().Format(time.RFC3339Nano) }
|
||||
|
||||
func nullableActor(actorID string) any {
|
||||
if actorID == "" {
|
||||
return nil
|
||||
}
|
||||
return actorID
|
||||
}
|
||||
|
||||
// --- Columns ---
|
||||
|
||||
func (db *DB) ListColumns() ([]Column, error) {
|
||||
rows, err := db.conn.Query(`SELECT id, name, position, location, width, created_at FROM columns ORDER BY position, created_at`)
|
||||
rows, err := db.conn.Query(`SELECT id, name, position, location, width, wip_limit, is_done, created_at FROM columns ORDER BY position, created_at`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,9 +174,11 @@ func (db *DB) ListColumns() ([]Column, error) {
|
||||
out := []Column{}
|
||||
for rows.Next() {
|
||||
var c Column
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Position, &c.Location, &c.Width, &c.CreatedAt); err != nil {
|
||||
var isDone int
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Position, &c.Location, &c.Width, &c.WIPLimit, &isDone, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.IsDone = isDone != 0
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
@@ -152,10 +193,10 @@ func (db *DB) CreateColumn(name string) (*Column, error) {
|
||||
if maxPos.Valid {
|
||||
pos = int(maxPos.Int64) + 1
|
||||
}
|
||||
c := Column{ID: newID(), Name: name, Position: pos, Location: "board", Width: 300, CreatedAt: nowRFC3339()}
|
||||
c := Column{ID: newID(), Name: name, Position: pos, Location: "board", Width: 300, WIPLimit: 0, IsDone: false, CreatedAt: nowRFC3339()}
|
||||
_, err := db.conn.Exec(
|
||||
`INSERT INTO columns (id, name, position, location, width, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.Name, c.Position, c.Location, c.Width, c.CreatedAt,
|
||||
`INSERT INTO columns (id, name, position, location, width, wip_limit, is_done, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.Name, c.Position, c.Location, c.Width, c.WIPLimit, 0, c.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -168,6 +209,8 @@ type ColumnPatch struct {
|
||||
Position *int
|
||||
Location *string
|
||||
Width *int
|
||||
WIPLimit *int
|
||||
IsDone *bool
|
||||
}
|
||||
|
||||
func (db *DB) UpdateColumn(id string, patch ColumnPatch) error {
|
||||
@@ -200,6 +243,35 @@ func (db *DB) UpdateColumn(id string, patch ColumnPatch) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if patch.WIPLimit != nil {
|
||||
l := *patch.WIPLimit
|
||||
if l < 0 {
|
||||
l = 0
|
||||
}
|
||||
if _, err := db.conn.Exec(`UPDATE columns SET wip_limit=? WHERE id=?`, l, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if patch.IsDone != nil {
|
||||
v := 0
|
||||
if *patch.IsDone {
|
||||
v = 1
|
||||
}
|
||||
if _, err := db.conn.Exec(`UPDATE columns SET is_done=? WHERE id=?`, v, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// Re-evaluate completed_at for cards in this column.
|
||||
now := nowRFC3339()
|
||||
if v == 1 {
|
||||
if _, err := db.conn.Exec(`UPDATE cards SET completed_at=? WHERE column_id=? AND completed_at IS NULL`, now, id); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := db.conn.Exec(`UPDATE cards SET completed_at=NULL WHERE column_id=?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,11 +298,12 @@ func (db *DB) ReorderColumns(ids []string) error {
|
||||
|
||||
func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.created_at, c.updated_at,
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.created_at, c.updated_at,
|
||||
h.entered_at
|
||||
FROM cards c
|
||||
LEFT JOIN card_column_history h
|
||||
ON h.card_id = c.id AND h.exited_at IS NULL
|
||||
WHERE c.deleted_at IS NULL
|
||||
ORDER BY c.column_id, c.position, c.created_at
|
||||
`)
|
||||
if err != nil {
|
||||
@@ -242,9 +315,26 @@ func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
for rows.Next() {
|
||||
var c Card
|
||||
var entered sql.NullString
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &c.CreatedAt, &c.UpdatedAt, &entered); err != nil {
|
||||
var assignee sql.NullString
|
||||
var completed sql.NullString
|
||||
var deleted sql.NullString
|
||||
var locked int
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &c.CreatedAt, &c.UpdatedAt, &entered); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Locked = locked != 0
|
||||
if assignee.Valid && assignee.String != "" {
|
||||
s := assignee.String
|
||||
c.AssigneeID = &s
|
||||
}
|
||||
if completed.Valid && completed.String != "" {
|
||||
s := completed.String
|
||||
c.CompletedAt = &s
|
||||
}
|
||||
if deleted.Valid && deleted.String != "" {
|
||||
s := deleted.String
|
||||
c.DeletedAt = &s
|
||||
}
|
||||
if entered.Valid {
|
||||
c.EnteredAt = entered.String
|
||||
if t, err := time.Parse(time.RFC3339Nano, entered.String); err == nil {
|
||||
@@ -256,7 +346,7 @@ func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DB) CreateCard(columnID, requester, title, description string) (*Card, error) {
|
||||
func (db *DB) CreateCard(columnID, requester, title, description, actorID string) (*Card, error) {
|
||||
var maxPos sql.NullInt64
|
||||
if err := db.conn.QueryRow(`SELECT MAX(position) FROM cards WHERE column_id=?`, columnID).Scan(&maxPos); err != nil {
|
||||
return nil, err
|
||||
@@ -282,11 +372,22 @@ func (db *DB) CreateCard(columnID, requester, title, description string) (*Card,
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO card_column_history (id, card_id, column_id, entered_at) VALUES (?, ?, ?, ?)`,
|
||||
newID(), c.ID, c.ColumnID, now,
|
||||
`INSERT INTO card_column_history (id, card_id, column_id, entered_at, actor_id) VALUES (?, ?, ?, ?, ?)`,
|
||||
newID(), c.ID, c.ColumnID, now, nullableActor(actorID),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the destination column is_done, set completed_at.
|
||||
var destDone int
|
||||
if err := tx.QueryRow(`SELECT is_done FROM columns WHERE id=?`, columnID).Scan(&destDone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if destDone == 1 {
|
||||
if _, err := tx.Exec(`UPDATE cards SET completed_at=? WHERE id=?`, now, c.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.CompletedAt = &now
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,9 +399,16 @@ type CardPatch struct {
|
||||
Title *string
|
||||
Description *string
|
||||
Color *string
|
||||
Locked *bool
|
||||
AssigneeID *string // empty string clears assignment
|
||||
HasAssignee bool // distinguishes "set to null" from "not provided"
|
||||
}
|
||||
|
||||
func (db *DB) UpdateCard(id string, patch CardPatch) error {
|
||||
return db.UpdateCardWithActor(id, patch, "")
|
||||
}
|
||||
|
||||
func (db *DB) UpdateCardWithActor(id string, patch CardPatch, actorID string) error {
|
||||
tx, err := db.conn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -326,18 +434,114 @@ func (db *DB) UpdateCard(id string, patch CardPatch) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if patch.HasAssignee {
|
||||
if patch.AssigneeID == nil || *patch.AssigneeID == "" {
|
||||
if _, err := tx.Exec(`UPDATE cards SET assignee_id=NULL, updated_at=? WHERE id=?`, nowRFC3339(), id); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.Exec(`UPDATE cards SET assignee_id=?, updated_at=? WHERE id=?`, *patch.AssigneeID, nowRFC3339(), id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if patch.Locked != nil {
|
||||
var current int
|
||||
if err := tx.QueryRow(`SELECT locked FROM cards WHERE id=?`, id).Scan(¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
desired := 0
|
||||
if *patch.Locked {
|
||||
desired = 1
|
||||
}
|
||||
if current != desired {
|
||||
now := nowRFC3339()
|
||||
if _, err := tx.Exec(`UPDATE cards SET locked=?, updated_at=? WHERE id=?`, desired, now, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if desired == 1 {
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO card_lock_history (id, card_id, locked_at, actor_id) VALUES (?, ?, ?, ?)`,
|
||||
newID(), id, now, nullableActor(actorID),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.Exec(
|
||||
`UPDATE card_lock_history SET unlocked_at=? WHERE card_id=? AND unlocked_at IS NULL`,
|
||||
now, id,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// DeleteCard soft-deletes the card (moves it to trash).
|
||||
func (db *DB) DeleteCard(id string) error {
|
||||
_, err := db.conn.Exec(`UPDATE cards SET deleted_at=?, updated_at=? WHERE id=?`, nowRFC3339(), nowRFC3339(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
// RestoreCard removes the deleted_at flag.
|
||||
func (db *DB) RestoreCard(id string) error {
|
||||
_, err := db.conn.Exec(`UPDATE cards SET deleted_at=NULL, updated_at=? WHERE id=?`, nowRFC3339(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeCard permanently removes the card from the DB.
|
||||
func (db *DB) PurgeCard(id string) error {
|
||||
_, err := db.conn.Exec(`DELETE FROM cards WHERE id=?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListDeletedCards returns cards in the trash, newest first.
|
||||
func (db *DB) ListDeletedCards() ([]Card, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.created_at, c.updated_at
|
||||
FROM cards c
|
||||
WHERE c.deleted_at IS NOT NULL
|
||||
ORDER BY c.deleted_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []Card{}
|
||||
for rows.Next() {
|
||||
var c Card
|
||||
var assignee sql.NullString
|
||||
var completed sql.NullString
|
||||
var deleted sql.NullString
|
||||
var locked int
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Locked = locked != 0
|
||||
if assignee.Valid && assignee.String != "" {
|
||||
s := assignee.String
|
||||
c.AssigneeID = &s
|
||||
}
|
||||
if completed.Valid && completed.String != "" {
|
||||
s := completed.String
|
||||
c.CompletedAt = &s
|
||||
}
|
||||
if deleted.Valid {
|
||||
s := deleted.String
|
||||
c.DeletedAt = &s
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// MoveCard updates the card's column and/or position. If the column changes,
|
||||
// the open history entry is closed and a new one is opened.
|
||||
// orderedIDs is the new order of cards in the destination column (including this card).
|
||||
func (db *DB) MoveCard(cardID, destColumnID string, orderedIDs []string) error {
|
||||
// actorID is the user performing the move (empty string for system/anonymous).
|
||||
func (db *DB) MoveCard(cardID, destColumnID string, orderedIDs []string, actorID string) error {
|
||||
tx, err := db.conn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -345,9 +549,13 @@ func (db *DB) MoveCard(cardID, destColumnID string, orderedIDs []string) error {
|
||||
defer tx.Rollback()
|
||||
|
||||
var srcColumnID string
|
||||
if err := tx.QueryRow(`SELECT column_id FROM cards WHERE id=?`, cardID).Scan(&srcColumnID); err != nil {
|
||||
var locked int
|
||||
if err := tx.QueryRow(`SELECT column_id, locked FROM cards WHERE id=?`, cardID).Scan(&srcColumnID, &locked); err != nil {
|
||||
return fmt.Errorf("card not found: %w", err)
|
||||
}
|
||||
if locked != 0 && srcColumnID != destColumnID {
|
||||
return fmt.Errorf("card locked: cannot move between columns")
|
||||
}
|
||||
|
||||
now := nowRFC3339()
|
||||
|
||||
@@ -359,17 +567,38 @@ func (db *DB) MoveCard(cardID, destColumnID string, orderedIDs []string) error {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO card_column_history (id, card_id, column_id, entered_at) VALUES (?, ?, ?, ?)`,
|
||||
newID(), cardID, destColumnID, now,
|
||||
`INSERT INTO card_column_history (id, card_id, column_id, entered_at, actor_id) VALUES (?, ?, ?, ?, ?)`,
|
||||
newID(), cardID, destColumnID, now, nullableActor(actorID),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = actorID
|
||||
if _, err := tx.Exec(
|
||||
`UPDATE cards SET column_id=?, updated_at=? WHERE id=?`,
|
||||
destColumnID, now, cardID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// Recompute completed_at based on destination column's is_done flag.
|
||||
var destDone int
|
||||
if err := tx.QueryRow(`SELECT is_done FROM columns WHERE id=?`, destColumnID).Scan(&destDone); err != nil {
|
||||
return err
|
||||
}
|
||||
if destDone == 1 {
|
||||
if _, err := tx.Exec(`UPDATE cards SET completed_at=? WHERE id=? AND completed_at IS NULL`, now, cardID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Auto-assign: if card had no assignee and an actor is moving it, claim it.
|
||||
if actorID != "" {
|
||||
if _, err := tx.Exec(`UPDATE cards SET assignee_id=? WHERE id=? AND (assignee_id IS NULL OR assignee_id='')`, actorID, cardID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.Exec(`UPDATE cards SET completed_at=NULL WHERE id=?`, cardID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, id := range orderedIDs {
|
||||
@@ -404,7 +633,7 @@ func (db *DB) MoveCard(cardID, destColumnID string, orderedIDs []string) error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (db *DB) CardHistory(cardID string) ([]HistoryEntry, error) {
|
||||
func (db *DB) CardHistory(cardID string) (*CardHistoryResponse, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT h.id, h.card_id, h.column_id, COALESCE(c.name, ''), h.entered_at, h.exited_at
|
||||
FROM card_column_history h
|
||||
@@ -417,7 +646,7 @@ func (db *DB) CardHistory(cardID string) ([]HistoryEntry, error) {
|
||||
}
|
||||
defer rows.Close()
|
||||
now := time.Now().UTC()
|
||||
out := []HistoryEntry{}
|
||||
cols := []HistoryEntry{}
|
||||
for rows.Next() {
|
||||
var h HistoryEntry
|
||||
var exited sql.NullString
|
||||
@@ -436,7 +665,55 @@ func (db *DB) CardHistory(cardID string) ([]HistoryEntry, error) {
|
||||
end = now
|
||||
}
|
||||
h.DurationMs = end.Sub(entered).Milliseconds()
|
||||
out = append(out, h)
|
||||
cols = append(cols, h)
|
||||
}
|
||||
return out, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lockRows, err := db.conn.Query(`
|
||||
SELECT id, card_id, locked_at, unlocked_at
|
||||
FROM card_lock_history
|
||||
WHERE card_id=?
|
||||
ORDER BY locked_at
|
||||
`, cardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer lockRows.Close()
|
||||
locks := []LockPeriod{}
|
||||
var totalLocked int64
|
||||
currently := false
|
||||
for lockRows.Next() {
|
||||
var lp LockPeriod
|
||||
var unlocked sql.NullString
|
||||
if err := lockRows.Scan(&lp.ID, &lp.CardID, &lp.LockedAt, &unlocked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start, err := time.Parse(time.RFC3339Nano, lp.LockedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var end time.Time
|
||||
if unlocked.Valid {
|
||||
lp.UnlockedAt = &unlocked.String
|
||||
end, _ = time.Parse(time.RFC3339Nano, unlocked.String)
|
||||
} else {
|
||||
end = now
|
||||
currently = true
|
||||
}
|
||||
lp.DurationMs = end.Sub(start).Milliseconds()
|
||||
totalLocked += lp.DurationMs
|
||||
locks = append(locks, lp)
|
||||
}
|
||||
if err := lockRows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CardHistoryResponse{
|
||||
ColumnHistory: cols,
|
||||
LockPeriods: locks,
|
||||
TotalLockedMs: totalLocked,
|
||||
CurrentlyLock: currently,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user