today dashboard: activity_events, ListTodayView with events timeline, frontend TodayDashboard separated from sidebar
This commit is contained in:
+146
-2
@@ -26,6 +26,7 @@
|
||||
// ===== State =====
|
||||
let sections = []
|
||||
let nodes = []
|
||||
let todayDashboard = null
|
||||
let version = ''
|
||||
let error = ''
|
||||
let selectedSection = ''
|
||||
@@ -134,15 +135,18 @@
|
||||
worklog = []
|
||||
showCreateNode = false
|
||||
error = ''
|
||||
todayDashboard = null
|
||||
nodes = []
|
||||
try {
|
||||
if (id === 'today') {
|
||||
nodes = await wailsCall('ListTodayView') || []
|
||||
todayDashboard = await wailsCall('ListTodayView') || { cases: [] }
|
||||
} else {
|
||||
nodes = await wailsCall('ListNodesBySection', id) || []
|
||||
}
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
nodes = []
|
||||
todayDashboard = { cases: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,10 +759,59 @@
|
||||
|
||||
// ===== Helpers =====
|
||||
function tabClass(id) { return activeTab === id ? 'tab active' : 'tab' }
|
||||
function eventLabel(type) {
|
||||
const labels = {
|
||||
'note_created': 'Заметка создана',
|
||||
'note_updated': 'Заметка изменена',
|
||||
'file_added': 'Файл добавлен',
|
||||
'file_deleted': 'Файл удалён',
|
||||
'file_renamed': 'Файл переименован',
|
||||
'file_copied': 'Файл скопирован',
|
||||
'file_moved': 'Файл перемещён',
|
||||
'folder_added': 'Папка добавлена',
|
||||
'folder_deleted': 'Папка удалена',
|
||||
'folder_renamed': 'Папка переименована',
|
||||
'node_created': 'Дело создано',
|
||||
'node_updated': 'Дело изменено',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
function eventIcon(type) {
|
||||
if (type === 'note_created' || type === 'file_added' || type === 'folder_added' || type === 'node_created') return '+'
|
||||
if (type === 'file_deleted' || type === 'folder_deleted') return '×'
|
||||
if (type === 'file_renamed' || type === 'folder_renamed' || type === 'note_updated' || type === 'node_updated') return '~'
|
||||
if (type === 'file_copied') return '⧉'
|
||||
if (type === 'file_moved') return '→'
|
||||
return '•'
|
||||
}
|
||||
function formatTime(str) {
|
||||
if (!str) return ''
|
||||
try { return new Date(str).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) } catch (e) { return '' }
|
||||
}
|
||||
function formatDate(str) {
|
||||
if (!str) return ''
|
||||
try { return new Date(str).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }) } catch (e) { return str }
|
||||
}
|
||||
function nodeKindLabel(kind) {
|
||||
const labels = { 'project': 'Проект', 'client': 'Клиент', 'document': 'Документ', 'recipe': 'Рецепт', 'archive': 'Архив', 'case': 'Дело' }
|
||||
return labels[kind] || kind || 'Дело'
|
||||
}
|
||||
function pluralize(n, one, few, many) {
|
||||
n = Math.abs(n) % 100
|
||||
if (n >= 5 && n <= 20) return many
|
||||
n %= 10
|
||||
if (n === 1) return one
|
||||
if (n >= 2 && n <= 4) return few
|
||||
return many
|
||||
}
|
||||
async function openNodeById(id) {
|
||||
try {
|
||||
const node = await wailsCall('GetNodeDetail', id)
|
||||
if (node) selectNode(node)
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
@@ -778,7 +831,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if selectedSection}
|
||||
{#if selectedSection && selectedSection !== 'today' && selectedSection !== 'inbox'}
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">Дела {#if nodes.length > 0}({nodes.length}){/if}</div>
|
||||
{#each nodes as node}
|
||||
@@ -1048,6 +1101,66 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if selectedSection === 'today' && todayDashboard}
|
||||
<div class="today-dashboard">
|
||||
<div class="today-header">
|
||||
<h2>Сегодня</h2>
|
||||
<span class="today-date">{todayDashboard.date}</span>
|
||||
</div>
|
||||
{#if todayDashboard.summary}
|
||||
<div class="today-summary">
|
||||
{#if todayDashboard.summary.changedCases > 0}<span class="summary-chip">{todayDashboard.summary.changedCases} {pluralize(todayDashboard.summary.changedCases, 'дело', 'дела', 'дел')}</span>{/if}
|
||||
{#if todayDashboard.summary.notes > 0}<span class="summary-chip">{todayDashboard.summary.notes} {pluralize(todayDashboard.summary.notes, 'заметка', 'заметки', 'заметок')}</span>{/if}
|
||||
{#if todayDashboard.summary.files > 0}<span class="summary-chip">{todayDashboard.summary.files} {pluralize(todayDashboard.summary.files, 'файл', 'файла', 'файлов')}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if todayDashboard.groups && todayDashboard.groups.length > 0}
|
||||
{#each todayDashboard.groups as group}
|
||||
<div class="today-case">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
<span class="today-event-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="today-events-empty">Изменён сегодня, подробная история пока недоступна</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if todayDashboard.events && todayDashboard.events.length > 0}
|
||||
<div class="today-timeline">
|
||||
<h3>Лента за сегодня</h3>
|
||||
{#each todayDashboard.events as ev}
|
||||
<div class="timeline-event">
|
||||
<span class="timeline-dot"></span>
|
||||
<span class="timeline-title">{ev.title}</span>
|
||||
<span class="timeline-type">{eventLabel(ev.eventType)}</span>
|
||||
<span class="timeline-time">{formatTime(ev.createdAt)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="today-empty">
|
||||
<p>Сегодня пока тихо</p>
|
||||
<p class="hint">Здесь появятся дела, заметки, файлы и действия, с которыми вы работали сегодня.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="welcome">
|
||||
<h2>Верстак</h2>
|
||||
@@ -1286,4 +1399,35 @@
|
||||
.summary-warn { margin-top: 8px; padding: 8px 12px; background: #3a2a22; border-radius: 6px; color: #ffaa66; font-size: 13px; }
|
||||
|
||||
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
|
||||
|
||||
/* Today Dashboard */
|
||||
.today-dashboard { padding: 24px; overflow-y: auto; flex: 1; }
|
||||
.today-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
|
||||
.today-header h2 { font-size: 24px; }
|
||||
.today-date { font-size: 13px; color: #666; }
|
||||
.today-summary { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.summary-chip { font-size: 12px; color: #b0b0c0; background: #1a1a28; border: 1px solid #2a2a3c; padding: 4px 12px; border-radius: 16px; }
|
||||
.today-case { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
|
||||
.today-case-header { padding: 12px 16px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #2a2a3c; cursor: pointer; }
|
||||
.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-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-icon { width: 18px; text-align: center; color: #6366f1; font-size: 13px; }
|
||||
.today-event-title { flex: 1; }
|
||||
.today-event-type { font-size: 11px; color: #666; }
|
||||
.today-event-time { font-size: 11px; color: #555; margin-left: auto; }
|
||||
.today-events-empty { padding: 8px 16px; font-size: 13px; color: #666; font-style: italic; }
|
||||
.today-empty { padding: 48px 24px; text-align: center; }
|
||||
.today-empty p { color: #666; font-size: 14px; margin: 0; }
|
||||
.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-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; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user