chore: auto-commit (23 archivos)
- app.md - backend/auth.go - backend/db.go - backend/dist/assets/index-CPqSy0gZ.js - backend/dist/index.html - backend/handlers.go - backend/main.go - frontend/src/App.tsx - frontend/src/api.ts - frontend/src/components/KanbanCard.tsx - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+168
@@ -1031,3 +1031,171 @@ func (db *DB) CardHistory(cardID string) (*CardHistoryResponse, error) {
|
||||
CurrentlyLock: currently,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CardMessage struct {
|
||||
ID string `json:"id"`
|
||||
CardID string `json:"card_id"`
|
||||
AuthorID *string `json:"author_id"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func (db *DB) ListCardMessages(cardID string) ([]CardMessage, error) {
|
||||
rows, err := db.conn.Query(
|
||||
`SELECT id, card_id, author_id, body, created_at FROM card_messages WHERE card_id=? ORDER BY created_at`,
|
||||
cardID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []CardMessage{}
|
||||
for rows.Next() {
|
||||
var m CardMessage
|
||||
var author sql.NullString
|
||||
if err := rows.Scan(&m.ID, &m.CardID, &author, &m.Body, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if author.Valid && author.String != "" {
|
||||
s := author.String
|
||||
m.AuthorID = &s
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DB) CreateCardMessage(cardID, authorID, body string) (*CardMessage, error) {
|
||||
body = strings.TrimSpace(body)
|
||||
if body == "" {
|
||||
return nil, fmt.Errorf("body required")
|
||||
}
|
||||
if authorID == "" {
|
||||
return nil, fmt.Errorf("author required")
|
||||
}
|
||||
var exists int
|
||||
if err := db.conn.QueryRow(`SELECT 1 FROM cards WHERE id=?`, cardID).Scan(&exists); err != nil {
|
||||
return nil, fmt.Errorf("card not found: %w", err)
|
||||
}
|
||||
s := authorID
|
||||
m := &CardMessage{ID: newID(), CardID: cardID, AuthorID: &s, Body: body, CreatedAt: nowRFC3339()}
|
||||
if _, err := db.conn.Exec(
|
||||
`INSERT INTO card_messages (id, card_id, author_id, body, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
m.ID, m.CardID, authorID, m.Body, m.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteCardMessage(id, requesterID string) error {
|
||||
if requesterID == "" {
|
||||
return fmt.Errorf("session required")
|
||||
}
|
||||
res, err := db.conn.Exec(`DELETE FROM card_messages WHERE id=? AND author_id=?`, id, requesterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("not found or not author")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DuplicateCard clones a card into the same column at the end of the list.
|
||||
// Copies title, description, color, requester, assignee, tags, deadline, stickers.
|
||||
// Does NOT copy card_column_history, card_lock_history, card_events, card_messages.
|
||||
// Title gets " (copia)" suffix.
|
||||
func (db *DB) DuplicateCard(srcID, actorID string) (*Card, error) {
|
||||
tx, err := db.conn.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var src Card
|
||||
var assignee sql.NullString
|
||||
var deadline sql.NullString
|
||||
var tagsJSON, stickersJSON string
|
||||
if err := tx.QueryRow(
|
||||
`SELECT requester, title, description, color, column_id, assignee_id, tags, stickers, deadline
|
||||
FROM cards WHERE id=? AND deleted_at IS NULL`, srcID,
|
||||
).Scan(&src.Requester, &src.Title, &src.Description, &src.Color, &src.ColumnID, &assignee, &tagsJSON, &stickersJSON, &deadline); err != nil {
|
||||
return nil, fmt.Errorf("card not found: %w", err)
|
||||
}
|
||||
if assignee.Valid && assignee.String != "" {
|
||||
s := assignee.String
|
||||
src.AssigneeID = &s
|
||||
}
|
||||
if deadline.Valid && deadline.String != "" {
|
||||
s := deadline.String
|
||||
src.Deadline = &s
|
||||
}
|
||||
src.Tags = parseTags(tagsJSON)
|
||||
src.Stickers = parseStickers(stickersJSON)
|
||||
|
||||
var maxPos sql.NullInt64
|
||||
if err := tx.QueryRow(`SELECT MAX(position) FROM cards WHERE column_id=?`, src.ColumnID).Scan(&maxPos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pos := 0
|
||||
if maxPos.Valid {
|
||||
pos = int(maxPos.Int64) + 1
|
||||
}
|
||||
var maxSeq sql.NullInt64
|
||||
if err := tx.QueryRow(`SELECT MAX(seq_num) FROM cards`).Scan(&maxSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seqNum := 1
|
||||
if maxSeq.Valid {
|
||||
seqNum = int(maxSeq.Int64) + 1
|
||||
}
|
||||
now := nowRFC3339()
|
||||
newTitle := src.Title + " (copia)"
|
||||
c := Card{
|
||||
ID: newID(), SeqNum: seqNum, Requester: src.Requester, Title: newTitle,
|
||||
Description: src.Description, Color: src.Color, ColumnID: src.ColumnID, Position: pos,
|
||||
AssigneeID: src.AssigneeID, Tags: src.Tags, Stickers: src.Stickers, Deadline: src.Deadline,
|
||||
CreatedAt: now, UpdatedAt: now, EnteredAt: now,
|
||||
}
|
||||
var assigneeVal any
|
||||
if c.AssigneeID != nil && *c.AssigneeID != "" {
|
||||
assigneeVal = *c.AssigneeID
|
||||
}
|
||||
var deadlineVal any
|
||||
if c.Deadline != nil && *c.Deadline != "" {
|
||||
deadlineVal = *c.Deadline
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO cards (id, seq_num, requester, title, description, color, column_id, position, assignee_id, tags, stickers, deadline, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.SeqNum, c.Requester, c.Title, c.Description, c.Color, c.ColumnID, c.Position,
|
||||
assigneeVal, encodeTags(c.Tags), encodeStickers(c.Stickers), deadlineVal, c.CreatedAt, c.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`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
|
||||
}
|
||||
var destDone int
|
||||
if err := tx.QueryRow(`SELECT is_done FROM columns WHERE id=?`, c.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 := insertCardEvent(tx, c.ID, "created", actorID, map[string]any{"title": newTitle, "column_id": c.ColumnID, "duplicated_from": srcID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user