activity: global feed, per-case log, sidebar section, today UX
- migration 009: target_type, target_id, target_path columns - new Event fields: TargetType, TargetID, TargetPath - ListActivityFeed (paginated global), ListActivityByNode (per-case) - all Record() callsites pass target info - frontend: Активность sidebar section with chronological feed - per-case Активность tab with real data (was placeholder) - today events: clickable, target-type badges, event counts
This commit is contained in:
+87
-31
@@ -120,13 +120,15 @@ type SearchResultDTO struct {
|
||||
}
|
||||
|
||||
type EventDTO struct {
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"nodeId"`
|
||||
ParentID string `json:"parentId"`
|
||||
EventType string `json:"eventType"`
|
||||
Title string `json:"title"`
|
||||
Metadata string `json:"metadata"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
NodeID string `json:"nodeId"`
|
||||
EventType string `json:"eventType"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetID string `json:"targetId"`
|
||||
TargetPath string `json:"targetPath"`
|
||||
Title string `json:"title"`
|
||||
DetailsJSON string `json:"detailsJson"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CaseActivityDTO struct {
|
||||
@@ -166,6 +168,7 @@ func (a *App) ListSections() []SectionDTO {
|
||||
return []SectionDTO{
|
||||
{ID: "today", Label: "Сегодня"},
|
||||
{ID: "inbox", Label: "Неразобранное"},
|
||||
{ID: "activity", Label: "Активность"},
|
||||
{ID: "clients", Label: "Клиенты"},
|
||||
{ID: "projects", Label: "Проекты"},
|
||||
{ID: "recipes", Label: "Рецепты"},
|
||||
@@ -200,10 +203,13 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
||||
todayNodes, _ := a.nodes.ListTodayNodes()
|
||||
|
||||
type rawEvent struct {
|
||||
NodeID string
|
||||
EventType string
|
||||
Title string
|
||||
CreatedAt string
|
||||
NodeID string
|
||||
EventType string
|
||||
TargetType string
|
||||
TargetID string
|
||||
TargetPath string
|
||||
Title string
|
||||
CreatedAt string
|
||||
}
|
||||
type caseInfo struct {
|
||||
Node nodes.Node
|
||||
@@ -228,10 +234,13 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
||||
ci := ensureCase(pid)
|
||||
for _, e := range events {
|
||||
ci.Events = append(ci.Events, rawEvent{
|
||||
NodeID: e.NodeID,
|
||||
EventType: e.EventType,
|
||||
Title: e.Title,
|
||||
CreatedAt: e.CreatedAt,
|
||||
NodeID: e.NodeID,
|
||||
EventType: e.EventType,
|
||||
TargetType: e.TargetType,
|
||||
TargetID: e.TargetID,
|
||||
TargetPath: e.TargetPath,
|
||||
Title: e.Title,
|
||||
CreatedAt: e.CreatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -257,12 +266,13 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
||||
dtoEvents := make([]EventDTO, 0, len(ci.Events))
|
||||
for _, re := range ci.Events {
|
||||
dtoEvents = append(dtoEvents, EventDTO{
|
||||
ID: ci.Node.ID + "/" + re.NodeID,
|
||||
NodeID: re.NodeID,
|
||||
ParentID: ci.Node.ID,
|
||||
EventType: re.EventType,
|
||||
Title: re.Title,
|
||||
Metadata: "{}",
|
||||
ID: ci.Node.ID + "/" + re.NodeID + "/" + re.CreatedAt,
|
||||
NodeID: re.NodeID,
|
||||
EventType: re.EventType,
|
||||
TargetType: re.TargetType,
|
||||
TargetID: re.TargetID,
|
||||
TargetPath: re.TargetPath,
|
||||
Title: re.Title,
|
||||
CreatedAt: re.CreatedAt,
|
||||
})
|
||||
switch re.EventType {
|
||||
@@ -306,6 +316,48 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toEventDTO(e activity.Event) EventDTO {
|
||||
return EventDTO{
|
||||
ID: e.ID,
|
||||
NodeID: e.NodeID,
|
||||
EventType: e.EventType,
|
||||
TargetType: e.TargetType,
|
||||
TargetID: e.TargetID,
|
||||
TargetPath: e.TargetPath,
|
||||
Title: e.Title,
|
||||
DetailsJSON: e.DetailsJSON,
|
||||
CreatedAt: e.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) {
|
||||
events, err := a.activity.ListRecent(limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]EventDTO, len(events))
|
||||
for i, e := range events {
|
||||
result[i] = toEventDTO(e)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error) {
|
||||
events, err := a.activity.ListByNode(nodeID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]EventDTO, len(events))
|
||||
for i, e := range events {
|
||||
result[i] = toEventDTO(e)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) CountActivityByNode(nodeID string) (int, error) {
|
||||
return a.activity.CountByNode(nodeID)
|
||||
}
|
||||
|
||||
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
|
||||
list, err := a.nodes.ListChildren(parentID, false)
|
||||
if err != nil {
|
||||
@@ -331,7 +383,7 @@ func (a *App) CreateNode(parentID, nodeType, title, section string) (*NodeDTO, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = a.activity.Record(n.ID, parentID, activity.TypeNodeCreated, title, "")
|
||||
_ = a.activity.Record(n.ID, activity.TargetNode, n.ID, "", activity.TypeNodeCreated, title, "")
|
||||
dto := toNodeDTO(n)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -365,7 +417,7 @@ func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = a.activity.Record(node.ID, parentID, activity.TypeNoteCreated, title, "")
|
||||
_ = a.activity.Record(parentID, activity.TargetNote, node.ID, "", activity.TypeNoteCreated, title, "")
|
||||
dto := toNodeDTO(node)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -386,7 +438,7 @@ func (a *App) SaveNote(noteID, content string) error {
|
||||
if n.ParentID != nil {
|
||||
pid = *n.ParentID
|
||||
}
|
||||
_ = a.activity.Record(noteID, pid, activity.TypeNoteUpdated, n.Title, "")
|
||||
_ = a.activity.Record(pid, activity.TargetNote, noteID, "", activity.TypeNoteUpdated, n.Title, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -458,7 +510,7 @@ func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
_ = a.activity.Record(n.ID, nodeID, activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`)
|
||||
_ = a.activity.Record(nodeID, activity.TargetFile, n.ID, "", activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`)
|
||||
}
|
||||
return toNodeDTOs(nodes), nil
|
||||
}
|
||||
@@ -469,7 +521,7 @@ func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
_ = a.activity.Record(n.ID, nodeID, activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`)
|
||||
_ = a.activity.Record(nodeID, activity.TargetFile, n.ID, "", activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`)
|
||||
}
|
||||
return toNodeDTOs(nodes), nil
|
||||
}
|
||||
@@ -482,10 +534,12 @@ func (a *App) DeleteFileOrFolder(nodeID string) error {
|
||||
pid = *n.ParentID
|
||||
}
|
||||
evType := activity.TypeFileDeleted
|
||||
targetType := activity.TargetFile
|
||||
if n.Type == nodes.TypeFolder {
|
||||
evType = activity.TypeFolderDeleted
|
||||
targetType = activity.TargetFolder
|
||||
}
|
||||
_ = a.activity.Record(nodeID, pid, evType, n.Title, "")
|
||||
_ = a.activity.Record(pid, targetType, nodeID, "", evType, n.Title, "")
|
||||
}
|
||||
return a.files.DeleteNodeAndChildren(nodeID)
|
||||
}
|
||||
@@ -495,7 +549,7 @@ func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = a.activity.Record(node.ID, parentID, activity.TypeFileAdded, filename, "")
|
||||
_ = a.activity.Record(parentID, activity.TargetFile, node.ID, "", activity.TypeFileAdded, filename, "")
|
||||
dto := toNodeDTO(node)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -511,7 +565,7 @@ func (a *App) DuplicateNode(nodeID string) (*NodeDTO, error) {
|
||||
if err2 == nil && n.ParentID != nil {
|
||||
pid = *n.ParentID
|
||||
}
|
||||
_ = a.activity.Record(node.ID, pid, activity.TypeFileCopied, node.Title, "")
|
||||
_ = a.activity.Record(pid, activity.TargetFile, node.ID, "", activity.TypeFileCopied, node.Title, "")
|
||||
dto := toNodeDTO(node)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -530,10 +584,12 @@ func (a *App) RenameNode(nodeID, newTitle string) error {
|
||||
pid = *n.ParentID
|
||||
}
|
||||
evType := activity.TypeFileRenamed
|
||||
targetType := activity.TargetFile
|
||||
if n.Type == nodes.TypeFolder {
|
||||
evType = activity.TypeFolderRenamed
|
||||
targetType = activity.TargetFolder
|
||||
}
|
||||
_ = a.activity.Record(nodeID, pid, evType, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`)
|
||||
_ = a.activity.Record(pid, targetType, nodeID, "", evType, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -568,7 +624,7 @@ func (a *App) MoveNode(nodeID, newParentID string) error {
|
||||
if node.ParentID != nil {
|
||||
pid = *node.ParentID
|
||||
}
|
||||
_ = a.activity.Record(nodeID, pid, activity.TypeFileMoved, node.Title, `{"to":"`+newParentID+`"}`)
|
||||
_ = a.activity.Record(pid, activity.TargetFile, nodeID, "", activity.TypeFileMoved, node.Title, `{"to":"`+newParentID+`"}`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user