Skip to content

Commit 45d7f81

Browse files
committed
spruce up dashboard
1 parent 1d5a3f5 commit 45d7f81

2 files changed

Lines changed: 347 additions & 9 deletions

File tree

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,74 @@
11
module Admin
22
class DashboardController < BaseController
33
def show
4+
# --- Action items ---
5+
@draft_versions = AgendaVersion.draft
6+
.includes(:meeting)
7+
.order(created_at: :desc)
8+
.limit(5)
9+
10+
@meetings_without_minutes = Meeting.chronological
11+
.joins(:agenda_versions)
12+
.where(agenda_versions: { status: :published })
13+
.where.not(id: Meeting.joins(agenda_versions: { agenda_sections: { agenda_items: :votes } }).select(:id))
14+
.distinct
15+
.limit(5)
16+
17+
@untagged_count = AgendaItem.left_joins(:agenda_item_tags)
18+
.where(agenda_item_tags: { id: nil })
19+
.count
20+
21+
@total_items = AgendaItem.count
22+
@tagged_count = @total_items - @untagged_count
23+
24+
# --- Stats ---
25+
@meeting_count = Meeting.count
26+
@published_count = AgendaVersion.published.select(:meeting_id).distinct.count
27+
@vote_count = Vote.count
428
@user_count = User.count
5-
@admin_count = User.where(role: [ :content_admin, :site_admin ]).count
29+
30+
# --- Recent meetings ---
31+
@recent_meetings = Meeting.includes(agenda_versions: { agenda_sections: { agenda_items: :tags } })
32+
.chronological
33+
.limit(5)
34+
35+
# --- User activity ---
36+
@recent_sessions = Session.includes(:user)
37+
.order(created_at: :desc)
38+
.limit(8)
39+
40+
@new_users_7d = User.where("created_at >= ?", 7.days.ago).count
41+
@active_users_7d = Session.where("created_at >= ?", 7.days.ago)
42+
.select(:user_id).distinct.count
43+
44+
# --- Engagement ---
45+
@star_count = Star.count
46+
@stars_7d = Star.where("created_at >= ?", 7.days.ago).count
47+
48+
@most_starred = Star.select("starrable_type, starrable_id, COUNT(*) AS star_count")
49+
.group(:starrable_type, :starrable_id)
50+
.order("star_count DESC")
51+
.limit(5)
52+
.map { |s| { type: s.starrable_type, id: s.starrable_id, count: s.star_count, record: s.starrable } }
53+
.select { |s| s[:record].present? }
54+
55+
@recently_starred = Star.includes(:starrable, :user)
56+
.order(created_at: :desc)
57+
.limit(5)
58+
.select { |s| s.starrable.present? }
59+
60+
# --- Tag coverage ---
61+
@tag_count = Tag.count
62+
@rules_count = TagRule.count
63+
@top_tags = Tag.left_joins(:agenda_item_tags)
64+
.select("tags.*, COUNT(agenda_item_tags.id) AS items_count")
65+
.group("tags.id")
66+
.having("COUNT(agenda_item_tags.id) > 0")
67+
.order("items_count DESC")
68+
.limit(8)
69+
70+
# --- Blog ---
71+
@draft_posts = BlogPost.draft.chronological.limit(3)
672
end
773
end
874
end
Lines changed: 280 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,285 @@
1-
<h1 class="text-2xl font-bold text-gray-900 mb-8">Admin Dashboard</h1>
1+
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
22

3-
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
4-
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
5-
<p class="text-sm font-medium text-gray-500">Total Users</p>
6-
<p class="mt-2 text-3xl font-bold text-gray-900"><%= @user_count %></p>
3+
<%# ===== ACTION ITEMS ===== %>
4+
<% has_actions = @draft_versions.any? || @meetings_without_minutes.any? || @untagged_count > 0 || @draft_posts.any? %>
5+
<% if has_actions %>
6+
<div class="mb-8">
7+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">Needs Attention</h2>
8+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
9+
10+
<% if @draft_versions.any? %>
11+
<div class="bg-amber-50 rounded-lg border border-amber-200 p-4">
12+
<div class="flex items-center gap-2 mb-2">
13+
<span class="w-2 h-2 rounded-full bg-amber-400"></span>
14+
<span class="text-sm font-semibold text-amber-800">Unpublished Agendas</span>
15+
</div>
16+
<ul class="space-y-1">
17+
<% @draft_versions.each do |version| %>
18+
<li class="text-sm">
19+
<%= link_to version.meeting.display_name,
20+
admin_meeting_path(version.meeting),
21+
class: "text-amber-700 hover:text-amber-900 underline underline-offset-2" %>
22+
<span class="text-amber-600 text-xs">v<%= version.version_number %></span>
23+
</li>
24+
<% end %>
25+
</ul>
26+
</div>
27+
<% end %>
28+
29+
<% if @meetings_without_minutes.any? %>
30+
<div class="bg-blue-50 rounded-lg border border-blue-200 p-4">
31+
<div class="flex items-center gap-2 mb-2">
32+
<span class="w-2 h-2 rounded-full bg-blue-400"></span>
33+
<span class="text-sm font-semibold text-blue-800">Missing Minutes</span>
34+
</div>
35+
<ul class="space-y-1">
36+
<% @meetings_without_minutes.each do |meeting| %>
37+
<li class="text-sm">
38+
<%= link_to meeting.display_name,
39+
admin_meeting_path(meeting),
40+
class: "text-blue-700 hover:text-blue-900 underline underline-offset-2" %>
41+
</li>
42+
<% end %>
43+
</ul>
44+
</div>
45+
<% end %>
46+
47+
<% if @untagged_count > 0 %>
48+
<div class="bg-violet-50 rounded-lg border border-violet-200 p-4">
49+
<div class="flex items-center gap-2 mb-2">
50+
<span class="w-2 h-2 rounded-full bg-violet-400"></span>
51+
<span class="text-sm font-semibold text-violet-800">Untagged Items</span>
52+
</div>
53+
<p class="text-sm text-violet-700">
54+
<span class="text-2xl font-bold"><%= @untagged_count %></span>
55+
<span>of <%= @total_items %> items have no tags</span>
56+
</p>
57+
<div class="mt-2 w-full bg-violet-200 rounded-full h-1.5">
58+
<div class="bg-violet-500 h-1.5 rounded-full" style="width: <%= @total_items > 0 ? (@tagged_count * 100.0 / @total_items).round : 0 %>%"></div>
59+
</div>
60+
<p class="text-xs text-violet-500 mt-1"><%= (@tagged_count * 100.0 / [@total_items, 1].max).round %>% tagged</p>
61+
</div>
62+
<% end %>
63+
64+
<% if @draft_posts.any? %>
65+
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4">
66+
<div class="flex items-center gap-2 mb-2">
67+
<span class="w-2 h-2 rounded-full bg-gray-400"></span>
68+
<span class="text-sm font-semibold text-gray-700">Draft Blog Posts</span>
69+
</div>
70+
<ul class="space-y-1">
71+
<% @draft_posts.each do |post| %>
72+
<li class="text-sm">
73+
<%= link_to post.title.truncate(50),
74+
admin_blog_post_path(post),
75+
class: "text-gray-600 hover:text-gray-900 underline underline-offset-2" %>
76+
</li>
77+
<% end %>
78+
</ul>
79+
</div>
80+
<% end %>
81+
82+
</div>
83+
</div>
84+
<% end %>
85+
86+
<%# ===== STATS OVERVIEW ===== %>
87+
<div class="mb-8">
88+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">Overview</h2>
89+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
90+
<div class="bg-white rounded-lg border border-gray-200 p-4">
91+
<p class="text-xs font-medium text-gray-500 uppercase">Meetings</p>
92+
<p class="mt-1 text-2xl font-bold text-gray-900"><%= @meeting_count %></p>
93+
<p class="text-xs text-gray-400"><%= @published_count %> published</p>
94+
</div>
95+
<div class="bg-white rounded-lg border border-gray-200 p-4">
96+
<p class="text-xs font-medium text-gray-500 uppercase">Agenda Items</p>
97+
<p class="mt-1 text-2xl font-bold text-gray-900"><%= @total_items %></p>
98+
<p class="text-xs text-gray-400"><%= @tagged_count %> tagged</p>
99+
</div>
100+
<div class="bg-white rounded-lg border border-gray-200 p-4">
101+
<p class="text-xs font-medium text-gray-500 uppercase">Votes Recorded</p>
102+
<p class="mt-1 text-2xl font-bold text-gray-900"><%= @vote_count %></p>
103+
</div>
104+
<div class="bg-white rounded-lg border border-gray-200 p-4">
105+
<p class="text-xs font-medium text-gray-500 uppercase">Users</p>
106+
<p class="mt-1 text-2xl font-bold text-gray-900"><%= @user_count %></p>
107+
<% if @new_users_7d > 0 %>
108+
<p class="text-xs text-emerald-600">+<%= @new_users_7d %> this week</p>
109+
<% end %>
110+
</div>
111+
</div>
112+
</div>
113+
114+
<%# ===== TWO COLUMN: RECENT MEETINGS + ACTIVITY ===== %>
115+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
116+
117+
<%# Recent Meetings %>
118+
<div class="lg:col-span-2">
119+
<div class="flex items-center justify-between mb-3">
120+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Recent Meetings</h2>
121+
<%= link_to "View all", admin_meetings_path, class: "text-xs text-blue-600 hover:text-blue-800" %>
122+
</div>
123+
<div class="bg-white rounded-lg border border-gray-200 divide-y divide-gray-100">
124+
<% @recent_meetings.each do |meeting| %>
125+
<% version = meeting.current_version %>
126+
<div class="px-4 py-3 flex items-center justify-between gap-3">
127+
<div class="min-w-0">
128+
<%= link_to meeting.display_name,
129+
admin_meeting_path(meeting),
130+
class: "text-sm font-medium text-gray-900 hover:text-blue-600 truncate block" %>
131+
<div class="flex items-center gap-2 mt-0.5 flex-wrap">
132+
<% if version&.published? %>
133+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-50 text-emerald-700">Published</span>
134+
<% elsif version %>
135+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-50 text-amber-700">Draft</span>
136+
<% end %>
137+
<% if meeting.minutes_imported? %>
138+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-50 text-emerald-700">Minutes</span>
139+
<% end %>
140+
<% item_count = version&.agenda_items&.size || 0 %>
141+
<span class="text-xs text-gray-400"><%= item_count %> items</span>
142+
</div>
143+
</div>
144+
<span class="text-xs text-gray-400 whitespace-nowrap"><%= meeting.date.strftime("%b %-d") %></span>
145+
</div>
146+
<% end %>
147+
148+
<% if @recent_meetings.empty? %>
149+
<div class="px-4 py-8 text-center text-sm text-gray-400">No meetings yet.</div>
150+
<% end %>
151+
</div>
152+
</div>
153+
154+
<%# Activity sidebar %>
155+
<div>
156+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">User Activity</h2>
157+
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-4">
158+
<div class="grid grid-cols-2 gap-3 text-center">
159+
<div>
160+
<p class="text-xl font-bold text-gray-900"><%= @active_users_7d %></p>
161+
<p class="text-xs text-gray-500">Active (7d)</p>
162+
</div>
163+
<div>
164+
<p class="text-xl font-bold text-gray-900"><%= @star_count %></p>
165+
<p class="text-xs text-gray-500">Stars</p>
166+
<% if @stars_7d > 0 %>
167+
<p class="text-[10px] text-emerald-600">+<%= @stars_7d %> this week</p>
168+
<% end %>
169+
</div>
170+
</div>
171+
</div>
172+
173+
<h3 class="text-xs font-medium text-gray-400 uppercase mb-2">Recent Logins</h3>
174+
<div class="bg-white rounded-lg border border-gray-200 divide-y divide-gray-100">
175+
<% @recent_sessions.each do |session| %>
176+
<div class="px-3 py-2 flex items-center justify-between">
177+
<span class="text-sm text-gray-700 truncate"><%= session.user&.email_address&.split("@")&.first || "Unknown" %></span>
178+
<span class="text-[10px] text-gray-400 whitespace-nowrap"><%= time_ago_in_words(session.created_at) %> ago</span>
179+
</div>
180+
<% end %>
181+
<% if @recent_sessions.empty? %>
182+
<div class="px-3 py-4 text-center text-xs text-gray-400">No recent logins.</div>
183+
<% end %>
184+
</div>
7185
</div>
186+
</div>
187+
188+
<%# ===== STARRED CONTENT ===== %>
189+
<% if @most_starred.any? || @recently_starred.any? %>
190+
<div class="mb-8">
191+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">Starred Content</h2>
192+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
8193

9-
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
10-
<p class="text-sm font-medium text-gray-500">Admins</p>
11-
<p class="mt-2 text-3xl font-bold text-gray-900"><%= @admin_count %></p>
194+
<% if @most_starred.any? %>
195+
<div class="bg-white rounded-lg border border-gray-200">
196+
<div class="px-4 py-2.5 border-b border-gray-100">
197+
<h3 class="text-xs font-medium text-gray-500 uppercase">Most Starred</h3>
198+
</div>
199+
<div class="divide-y divide-gray-100">
200+
<% @most_starred.each do |entry| %>
201+
<div class="px-4 py-2.5 flex items-center justify-between gap-2">
202+
<div class="min-w-0 flex items-center gap-2">
203+
<% case entry[:type] %>
204+
<% when "Meeting" %>
205+
<span class="shrink-0 w-5 text-center text-xs text-gray-400">M</span>
206+
<%= link_to entry[:record].display_name.truncate(45), admin_meeting_path(entry[:record]), class: "text-sm text-gray-700 hover:text-blue-600 truncate" %>
207+
<% when "AgendaItem" %>
208+
<span class="shrink-0 w-5 text-center text-xs text-gray-400">I</span>
209+
<span class="text-sm text-gray-700 truncate" title="<%= entry[:record].title %>"><%= entry[:record].item_number %>: <%= entry[:record].title.truncate(40) %></span>
210+
<% when "CouncilMember" %>
211+
<span class="shrink-0 w-5 text-center text-xs text-gray-400">C</span>
212+
<%= link_to entry[:record].full_name.truncate(40), admin_council_member_path(entry[:record]), class: "text-sm text-gray-700 hover:text-blue-600 truncate" %>
213+
<% when "Tag" %>
214+
<span class="shrink-0 w-5 text-center text-xs text-gray-400">T</span>
215+
<span class="text-sm text-gray-700 truncate"><%= entry[:record].name %></span>
216+
<% end %>
217+
</div>
218+
<span class="shrink-0 inline-flex items-center gap-0.5 text-xs text-amber-600">
219+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
220+
<%= entry[:count] %>
221+
</span>
222+
</div>
223+
<% end %>
224+
</div>
225+
</div>
226+
<% end %>
227+
228+
<% if @recently_starred.any? %>
229+
<div class="bg-white rounded-lg border border-gray-200">
230+
<div class="px-4 py-2.5 border-b border-gray-100">
231+
<h3 class="text-xs font-medium text-gray-500 uppercase">Recently Starred</h3>
232+
</div>
233+
<div class="divide-y divide-gray-100">
234+
<% @recently_starred.each do |star| %>
235+
<div class="px-4 py-2.5 flex items-center justify-between gap-2">
236+
<div class="min-w-0 flex items-center gap-2">
237+
<span class="shrink-0 text-xs text-gray-400"><%= star.user&.email_address&.split("@")&.first&.truncate(10) || "?" %></span>
238+
<span class="text-gray-300">&rarr;</span>
239+
<% case star.starrable_type %>
240+
<% when "Meeting" %>
241+
<%= link_to star.starrable.display_name.truncate(30), admin_meeting_path(star.starrable), class: "text-sm text-gray-700 hover:text-blue-600 truncate" %>
242+
<% when "AgendaItem" %>
243+
<span class="text-sm text-gray-700 truncate"><%= star.starrable.item_number %>: <%= star.starrable.title.truncate(30) %></span>
244+
<% when "CouncilMember" %>
245+
<%= link_to star.starrable.full_name.truncate(30), admin_council_member_path(star.starrable), class: "text-sm text-gray-700 hover:text-blue-600 truncate" %>
246+
<% when "Tag" %>
247+
<span class="text-sm text-gray-700 truncate"><%= star.starrable.name %></span>
248+
<% end %>
249+
</div>
250+
<span class="shrink-0 text-[10px] text-gray-400 whitespace-nowrap"><%= time_ago_in_words(star.created_at) %></span>
251+
</div>
252+
<% end %>
253+
</div>
254+
</div>
255+
<% end %>
256+
257+
</div>
258+
</div>
259+
<% end %>
260+
261+
<%# ===== TAG COVERAGE ===== %>
262+
<div class="mb-8">
263+
<div class="flex items-center justify-between mb-3">
264+
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Top Tags</h2>
265+
<%= link_to "Manage tags", admin_tags_path, class: "text-xs text-blue-600 hover:text-blue-800" %>
266+
</div>
267+
<div class="bg-white rounded-lg border border-gray-200 p-4">
268+
<% if @top_tags.any? %>
269+
<div class="flex flex-wrap gap-2">
270+
<% @top_tags.each do |tag| %>
271+
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm bg-indigo-50 text-indigo-700 border border-indigo-100">
272+
<%= tag.name %>
273+
<span class="text-xs text-indigo-400"><%= tag.items_count %></span>
274+
</span>
275+
<% end %>
276+
</div>
277+
<div class="mt-3 flex items-center gap-4 text-xs text-gray-500">
278+
<span><%= @tag_count %> tags total</span>
279+
<span><%= @rules_count %> auto-tag rules</span>
280+
</div>
281+
<% else %>
282+
<p class="text-sm text-gray-400">No tagged items yet. <%= link_to "Set up auto-tag rules", admin_tags_path, class: "text-blue-600 hover:text-blue-800" %> to get started.</p>
283+
<% end %>
12284
</div>
13285
</div>

0 commit comments

Comments
 (0)