223 lines
5.5 KiB
Go
223 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
|
|
"verstak/internal/core/activity"
|
|
"verstak/internal/core/nodes"
|
|
syncsvc "verstak/internal/core/sync"
|
|
"verstak/internal/i18n"
|
|
)
|
|
|
|
type SystemViewDTO struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
func (a *App) ListSystemViews() []SystemViewDTO {
|
|
return []SystemViewDTO{
|
|
{ID: "today", Label: i18n.TF("ru", "nav.today")},
|
|
{ID: "inbox", Label: i18n.TF("ru", "nav.inbox")},
|
|
{ID: "trash", Label: i18n.TF("ru", "nav.trash")},
|
|
{ID: "journal", Label: i18n.TF("ru", "nav.journal")},
|
|
{ID: "activity", Label: i18n.TF("ru", "nav.activity")},
|
|
}
|
|
}
|
|
|
|
func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return nil, err
|
|
}
|
|
aeByParent, err := a.activity.ListTodayEventsByParent()
|
|
if err != nil {
|
|
aeByParent = nil
|
|
}
|
|
todayNodes, _ := a.nodes.ListTodayNodes()
|
|
|
|
type rawEvent struct {
|
|
NodeID string
|
|
EventType string
|
|
TargetType string
|
|
TargetID string
|
|
TargetPath string
|
|
Title string
|
|
CreatedAt string
|
|
}
|
|
type caseInfo struct {
|
|
Node nodes.Node
|
|
Events []rawEvent
|
|
}
|
|
caseMap := make(map[string]*caseInfo)
|
|
|
|
ensureCase := func(caseID string) *caseInfo {
|
|
if ci, ok := caseMap[caseID]; ok {
|
|
return ci
|
|
}
|
|
ci := &caseInfo{Events: nil}
|
|
if n, err := a.nodes.GetActive(caseID); err == nil {
|
|
ci.Node = *n
|
|
}
|
|
caseMap[caseID] = ci
|
|
return ci
|
|
}
|
|
|
|
for pid, events := range aeByParent {
|
|
ci := ensureCase(pid)
|
|
for _, e := range events {
|
|
ci.Events = append(ci.Events, rawEvent{
|
|
NodeID: e.NodeID,
|
|
EventType: e.EventType,
|
|
TargetType: e.TargetType,
|
|
TargetID: e.TargetID,
|
|
TargetPath: e.TargetPath,
|
|
Title: e.Title,
|
|
CreatedAt: e.CreatedAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, n := range todayNodes {
|
|
_ = ensureCase(n.ID)
|
|
if ci := caseMap[n.ID]; ci.Node.ID == "" {
|
|
ci.Node = n
|
|
}
|
|
}
|
|
|
|
var groups []TodayGroupDTO
|
|
var flatEvents []EventDTO
|
|
summary := SummaryDTO{}
|
|
|
|
for _, ci := range caseMap {
|
|
if ci.Node.ID == "" {
|
|
continue
|
|
}
|
|
summary.ChangedCases++
|
|
|
|
dtoEvents := make([]EventDTO, 0, len(ci.Events))
|
|
for _, re := range ci.Events {
|
|
dtoEvents = append(dtoEvents, EventDTO{
|
|
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 {
|
|
case activity.TypeNoteCreated, activity.TypeNoteUpdated:
|
|
summary.Notes++
|
|
case activity.TypeFileAdded, activity.TypeFileDeleted, activity.TypeFileRenamed, activity.TypeFileCopied, activity.TypeFileMoved:
|
|
summary.Files++
|
|
}
|
|
}
|
|
|
|
last := ci.Node.UpdatedAt.Format(time.RFC3339)
|
|
for _, e := range dtoEvents {
|
|
if e.CreatedAt > last {
|
|
last = e.CreatedAt
|
|
}
|
|
}
|
|
|
|
groups = append(groups, TodayGroupDTO{
|
|
NodeID: ci.Node.ID,
|
|
NodeTitle: ci.Node.Title,
|
|
NodeKind: ci.Node.Type,
|
|
Section: ci.Node.Section,
|
|
LastActivityAt: last,
|
|
Events: dtoEvents,
|
|
})
|
|
flatEvents = append(flatEvents, dtoEvents...)
|
|
}
|
|
|
|
sort.Slice(groups, func(i, j int) bool {
|
|
return groups[i].LastActivityAt > groups[j].LastActivityAt
|
|
})
|
|
sort.Slice(flatEvents, func(i, j int) bool {
|
|
return flatEvents[i].CreatedAt > flatEvents[j].CreatedAt
|
|
})
|
|
|
|
return &TodayDashboardDTO{
|
|
Date: time.Now().Format("2006-01-02"),
|
|
Summary: summary,
|
|
Groups: groups,
|
|
Events: flatEvents,
|
|
}, nil
|
|
}
|
|
|
|
func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return nil, err
|
|
}
|
|
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] = a.eventDTOWithPath(e)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return nil, err
|
|
}
|
|
events, err := a.listActivityByNodeSubtree(nodeID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]EventDTO, len(events))
|
|
for i, e := range events {
|
|
result[i] = a.eventDTOWithPath(e)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) CountActivityByNode(nodeID string) (int, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return 0, err
|
|
}
|
|
return a.activity.CountByNode(nodeID)
|
|
}
|
|
|
|
var _ = syncsvc.EntityNode
|
|
|
|
func (a *App) listActivityByNodeSubtree(nodeID string, limit, offset int) ([]activity.Event, error) {
|
|
rows, err := a.db.Query(
|
|
`WITH RECURSIVE subtree(id) AS (
|
|
SELECT id FROM nodes WHERE id = ? AND deleted_at IS NULL
|
|
UNION ALL
|
|
SELECT n.id FROM nodes n JOIN subtree s ON n.parent_id = s.id
|
|
WHERE n.deleted_at IS NULL
|
|
)
|
|
SELECT e.id, e.node_id, e.event_type, COALESCE(e.target_type,''), COALESCE(e.target_id,''), COALESCE(e.target_path,''),
|
|
e.title, COALESCE(e.metadata,'{}'), e.created_at
|
|
FROM activity_events e
|
|
JOIN subtree s ON s.id = e.node_id
|
|
ORDER BY e.created_at DESC
|
|
LIMIT ? OFFSET ?`, nodeID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var events []activity.Event
|
|
for rows.Next() {
|
|
var e activity.Event
|
|
if err := rows.Scan(&e.ID, &e.NodeID, &e.EventType, &e.TargetType, &e.TargetID, &e.TargetPath, &e.Title, &e.DetailsJSON, &e.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, e)
|
|
}
|
|
return events, rows.Err()
|
|
}
|
|
|
|
func (a *App) eventDTOWithPath(e activity.Event) EventDTO {
|
|
dto := toEventDTO(e)
|
|
dto.NodePath = a.nodes.Path(e.NodeID)
|
|
return dto
|
|
}
|