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:
+98
-9
@@ -27,6 +27,11 @@
|
||||
let sections = []
|
||||
let nodes = []
|
||||
let todayDashboard = null
|
||||
let activityFeed = []
|
||||
let activityOffset = 0
|
||||
let activityHasMore = true
|
||||
let activityLoading = false
|
||||
let caseActivity = []
|
||||
let version = ''
|
||||
let error = ''
|
||||
let selectedSection = ''
|
||||
@@ -100,6 +105,7 @@
|
||||
sections = [
|
||||
{ id: 'today', label: 'Сегодня' },
|
||||
{ id: 'inbox', label: 'Неразобранное' },
|
||||
{ id: 'activity', label: 'Активность' },
|
||||
{ id: 'clients', label: 'Клиенты' },
|
||||
{ id: 'projects', label: 'Проекты' },
|
||||
{ id: 'recipes', label: 'Рецепты' },
|
||||
@@ -136,10 +142,17 @@
|
||||
showCreateNode = false
|
||||
error = ''
|
||||
todayDashboard = null
|
||||
activityFeed = []
|
||||
activityOffset = 0
|
||||
activityHasMore = true
|
||||
nodes = []
|
||||
try {
|
||||
if (id === 'today') {
|
||||
todayDashboard = await wailsCall('ListTodayView') || { cases: [] }
|
||||
} else if (id === 'activity') {
|
||||
activityFeed = await wailsCall('ListActivityFeed', 50, 0) || []
|
||||
activityOffset = activityFeed.length
|
||||
activityHasMore = activityFeed.length === 50
|
||||
} else {
|
||||
nodes = await wailsCall('ListNodesBySection', id) || []
|
||||
}
|
||||
@@ -147,6 +160,7 @@
|
||||
error = String(e)
|
||||
nodes = []
|
||||
todayDashboard = { cases: [] }
|
||||
activityFeed = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +185,7 @@
|
||||
showCreateNode = false
|
||||
showCreateNote = false
|
||||
error = ''
|
||||
caseActivity = []
|
||||
await loadTabData(node.id)
|
||||
}
|
||||
|
||||
@@ -179,6 +194,7 @@
|
||||
try { files = await wailsCall('ListFiles', nodeID) || [] } catch(e) {}
|
||||
try { actions = await wailsCall('ListActions', nodeID) || [] } catch(e) {}
|
||||
try { worklog = await wailsCall('ListWorklog', nodeID) || [] } catch(e) {}
|
||||
try { caseActivity = await wailsCall('ListActivityByNode', nodeID, 50, 0) || [] } catch(e) {}
|
||||
}
|
||||
|
||||
async function loadTree(nodeID) {
|
||||
@@ -831,7 +847,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if selectedSection && selectedSection !== 'today' && selectedSection !== 'inbox'}
|
||||
{#if selectedSection && selectedSection !== 'today' && selectedSection !== 'inbox' && selectedSection !== 'activity'}
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">Дела {#if nodes.length > 0}({nodes.length}){/if}</div>
|
||||
{#each nodes as node}
|
||||
@@ -1097,7 +1113,23 @@
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'activity'}
|
||||
<div class="empty-state"><p>Активность появится позже</p></div>
|
||||
<div class="activity-tab">
|
||||
{#if caseActivity.length === 0}
|
||||
<div class="empty-state"><p>Активность пока не зафиксирована</p></div>
|
||||
{:else}
|
||||
<div class="activity-events">
|
||||
{#each caseActivity as ev}
|
||||
<div class="activity-event">
|
||||
<span class="activity-event-icon">{eventIcon(ev.eventType)}</span>
|
||||
<span class="activity-event-title">{ev.title}</span>
|
||||
<span class="activity-event-type">{eventLabel(ev.eventType)}</span>
|
||||
{#if ev.targetType}<span class="activity-event-target">{ev.targetType}</span>{/if}
|
||||
<span class="activity-event-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1121,21 +1153,23 @@
|
||||
<div class="today-case-header" role="button" tabindex="0" on:click={() => openNodeById(group.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(group.nodeId)}>
|
||||
<span class="today-case-title">{group.nodeTitle}</span>
|
||||
<span class="today-case-type">{nodeKindLabel(group.nodeKind)}</span>
|
||||
{#if group.events}<span class="today-case-count">{group.events.length} {pluralize(group.events.length, 'событие', 'события', 'событий')}</span>{/if}
|
||||
<span class="today-case-time">{formatTime(group.lastActivityAt)}</span>
|
||||
</div>
|
||||
{#if group.events && group.events.length > 0}
|
||||
<div class="today-events">
|
||||
{#each group.events as ev}
|
||||
<div class="today-event">
|
||||
<div class="today-event" role="button" tabindex="0" on:click={() => openNodeById(group.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(group.nodeId)}>
|
||||
<span class="today-event-icon">{eventIcon(ev.eventType)}</span>
|
||||
<span class="today-event-title">{ev.title}</span>
|
||||
<span class="today-event-type">{eventLabel(ev.eventType)}</span>
|
||||
{#if ev.targetType}<span class="activity-event-target">{ev.targetType}</span>{/if}
|
||||
<span class="today-event-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="today-events-empty">Изменён сегодня, подробная история пока недоступна</div>
|
||||
<div class="today-events-empty">Изменён сегодня</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -1144,7 +1178,7 @@
|
||||
<div class="today-timeline">
|
||||
<h3>Лента за сегодня</h3>
|
||||
{#each todayDashboard.events as ev}
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-event" role="button" tabindex="0" on:click={() => openNodeById(ev.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(ev.nodeId)}>
|
||||
<span class="timeline-dot"></span>
|
||||
<span class="timeline-title">{ev.title}</span>
|
||||
<span class="timeline-type">{eventLabel(ev.eventType)}</span>
|
||||
@@ -1161,6 +1195,32 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if selectedSection === 'activity'}
|
||||
<div class="activity-feed">
|
||||
<div class="activity-feed-header">
|
||||
<h2>Активность</h2>
|
||||
</div>
|
||||
{#if activityFeed.length === 0}
|
||||
<div class="empty-state"><p>Активность пока не зафиксирована</p></div>
|
||||
{:else}
|
||||
<div class="activity-feed-events">
|
||||
{#each activityFeed as ev}
|
||||
<div class="activity-feed-event" role="button" tabindex="0" on:click={() => openNodeById(ev.nodeId)} on:keydown={(e) => e.key === 'Enter' && openNodeById(ev.nodeId)}>
|
||||
<span class="activity-feed-icon">{eventIcon(ev.eventType)}</span>
|
||||
<div class="activity-feed-body">
|
||||
<span class="activity-feed-title">{ev.title}</span>
|
||||
<div class="activity-feed-meta">
|
||||
<span class="activity-feed-type">{eventLabel(ev.eventType)}</span>
|
||||
{#if ev.targetType}<span class="activity-feed-target">{ev.targetType}</span>{/if}
|
||||
<span class="activity-feed-time">{formatDate(ev.createdAt)} {formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="welcome">
|
||||
<h2>Верстак</h2>
|
||||
@@ -1172,7 +1232,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !noteEditor && !selectedNode && selectedSection !== 'today' && selectedSection !== 'inbox'}
|
||||
{#if !noteEditor && !selectedNode && selectedSection !== 'today' && selectedSection !== 'inbox' && selectedSection !== 'activity'}
|
||||
<div class="fab" on:click={openCreateNode} title="Добавить дело">+</div>
|
||||
{/if}
|
||||
|
||||
@@ -1188,7 +1248,7 @@
|
||||
<div class="form-group">
|
||||
<label>Раздел</label>
|
||||
<select bind:value={newNodeSection}>
|
||||
{#each sections.filter(s => s.id !== 'today' && s.id !== 'inbox') as s}
|
||||
{#each sections.filter(s => s.id !== 'today' && s.id !== 'inbox' && s.id !== 'activity') as s}
|
||||
<option value={s.id}>{s.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -1412,9 +1472,11 @@
|
||||
.today-case-header:hover { background: #1e1e30; }
|
||||
.today-case-title { font-weight: 500; }
|
||||
.today-case-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; }
|
||||
.today-case-count { font-size: 11px; color: #6366f1; margin-left: 4px; }
|
||||
.today-case-time { font-size: 11px; color: #555; margin-left: auto; }
|
||||
.today-events { padding: 8px 16px; }
|
||||
.today-event { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; color: #b0b0c0; }
|
||||
.today-event { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; color: #b0b0c0; cursor: pointer; }
|
||||
.today-event:hover { color: #e4e4ef; }
|
||||
.today-event-icon { width: 18px; text-align: center; color: #6366f1; font-size: 13px; }
|
||||
.today-event-title { flex: 1; }
|
||||
.today-event-type { font-size: 11px; color: #666; }
|
||||
@@ -1425,9 +1487,36 @@
|
||||
.today-empty .hint { font-size: 12px; color: #555; margin-top: 8px; }
|
||||
.today-timeline { margin-top: 24px; }
|
||||
.today-timeline h3 { font-size: 13px; color: #666; text-transform: uppercase; margin-bottom: 12px; }
|
||||
.timeline-event { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; color: #b0b0c0; border-left: 2px solid #2a2a3c; padding-left: 16px; margin-left: 4px; }
|
||||
.timeline-event { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; color: #b0b0c0; border-left: 2px solid #2a2a3c; padding-left: 16px; margin-left: 4px; cursor: pointer; }
|
||||
.timeline-event:hover { color: #e4e4ef; }
|
||||
.timeline-dot { width: 6px; height: 6px; border-radius: 50%; background: #6366f1; margin-left: -19px; flex-shrink: 0; }
|
||||
.timeline-title { flex: 1; }
|
||||
.timeline-type { font-size: 11px; color: #666; }
|
||||
.timeline-time { font-size: 11px; color: #555; }
|
||||
|
||||
/* Activity tab (per-case) */
|
||||
.activity-tab { padding: 24px; }
|
||||
.activity-events { display: flex; flex-direction: column; gap: 2px; }
|
||||
.activity-event { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; font-size: 13px; color: #b0b0c0; cursor: pointer; }
|
||||
.activity-event:hover { background: #1a1a28; color: #e4e4ef; }
|
||||
.activity-event-icon { width: 18px; text-align: center; color: #6366f1; font-size: 13px; flex-shrink: 0; }
|
||||
.activity-event-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.activity-event-type { font-size: 11px; color: #666; flex-shrink: 0; }
|
||||
.activity-event-target { font-size: 10px; color: #555; background: #1e1e2e; padding: 1px 6px; border-radius: 8px; flex-shrink: 0; }
|
||||
.activity-event-time { font-size: 11px; color: #555; margin-left: 8px; flex-shrink: 0; }
|
||||
|
||||
/* Activity feed (global section) */
|
||||
.activity-feed { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.activity-feed-header { margin-bottom: 20px; }
|
||||
.activity-feed-header h2 { font-size: 24px; }
|
||||
.activity-feed-events { display: flex; flex-direction: column; gap: 2px; }
|
||||
.activity-feed-event { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
|
||||
.activity-feed-event:hover { background: #1a1a28; color: #e4e4ef; }
|
||||
.activity-feed-icon { width: 20px; text-align: center; color: #6366f1; font-size: 14px; flex-shrink: 0; margin-top: 1px; }
|
||||
.activity-feed-body { flex: 1; min-width: 0; }
|
||||
.activity-feed-title { font-size: 14px; color: #e4e4ef; }
|
||||
.activity-feed-meta { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
|
||||
.activity-feed-type { font-size: 11px; color: #666; }
|
||||
.activity-feed-target { font-size: 10px; color: #555; background: #1e1e2e; padding: 1px 6px; border-radius: 8px; }
|
||||
.activity-feed-time { font-size: 11px; color: #555; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user