effective_committees 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/admin/committee_agenda_items_controller.rb +16 -0
  3. data/app/controllers/effective/committee_agenda_items_controller.rb +11 -0
  4. data/app/controllers/effective/committee_folders_controller.rb +5 -0
  5. data/app/datatables/admin/effective_committee_agenda_items_datatable.rb +29 -0
  6. data/app/datatables/admin/effective_committee_folders_datatable.rb +10 -8
  7. data/app/helpers/effective_committees_helper.rb +11 -1
  8. data/app/models/effective/committee.rb +14 -2
  9. data/app/models/effective/committee_agenda_item.rb +43 -0
  10. data/app/models/effective/committee_folder.rb +59 -1
  11. data/app/views/admin/committee_agenda_items/_form.html.haml +3 -0
  12. data/app/views/admin/committee_agenda_items/_form_committee_agenda_item.html.haml +37 -0
  13. data/app/views/admin/committee_folders/_form.html.haml +21 -11
  14. data/app/views/admin/committee_folders/_form_committee_folder.html.haml +12 -2
  15. data/app/views/admin/committee_members/_form_committee_member.html.haml +8 -0
  16. data/app/views/admin/committees/_fields.html.haml +3 -0
  17. data/app/views/effective/committee_agenda_items/_card.html.haml +30 -0
  18. data/app/views/effective/committee_agenda_items/_committee_agenda_item.html.haml +14 -0
  19. data/app/views/effective/committee_agenda_items/show.html.haml +10 -0
  20. data/app/views/effective/committee_folders/_agenda.html.haml +42 -0
  21. data/app/views/effective/committee_folders/_breadcrumb.html.haml +13 -0
  22. data/app/views/effective/committee_folders/_committee_folder.html.haml +42 -20
  23. data/app/views/effective/committee_folders/agenda.html.haml +2 -0
  24. data/app/views/effective/committee_folders/show.html.haml +1 -1
  25. data/app/views/effective/committees/_dashboard_agendas.html.haml +18 -0
  26. data/app/views/effective/committees/index.html.haml +3 -0
  27. data/config/routes.rb +5 -1
  28. data/db/migrate/101_create_effective_committees.rb +25 -0
  29. data/lib/effective_committees/version.rb +1 -1
  30. data/lib/effective_committees.rb +1 -0
  31. metadata +15 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d78fcecb80be8b05b2a3e02921358f8183575caccd4e259b27ca856d98abc869
4
- data.tar.gz: '07478672b7ba0c0474d8928041e1d16ac32199110244bc50998cfd9b1b8e918a'
3
+ metadata.gz: b64f9b12ecd4b0b3b70dd361f5fbe9c25b37009f167c68c92b7d0a0501bdbb7d
4
+ data.tar.gz: b3e5bbcadbd792f9f98d0bc5cb65aad229ae24fea3bae2aa7a0063f8e1535295
5
5
  SHA512:
6
- metadata.gz: 6ed635067e98101e0e8e33f7ef3ba0365cac457fc58d6fda4bc099c1043e4d20b690eea8bfb9068a0d9f8b1e6f99003c9c14ef206ba93f216adfee3c4bb7533c
7
- data.tar.gz: b358afac722343b105466631e35ae62c49a34b1322d73c16ff58c551b35bc55326a75292f6268410f734091329424b14ee3f495a078722994e1a563783f53400
6
+ metadata.gz: 340c2c724dfab35ab4b3b901cd5035805496b4a7e6029abfaf3f490fbe957c1fdc99312c0fb72712577c2983083182baaf0c7a075baa08a5d9aae8c82b38a7fe
7
+ data.tar.gz: af0d65a2e6b320347dfd169ce5710712cda0eab4c33d80da5e386b8a2262e03b431d32ec922f8fa9a7579ab370a901811192bee4254c98e719856341b518e8b5
@@ -0,0 +1,16 @@
1
+ module Admin
2
+ class CommitteeAgendaItemsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_committees) }
5
+
6
+ include Effective::CrudController
7
+
8
+ private
9
+
10
+ def permitted_params
11
+ model = (params.key?(:effective_committee_agenda_item) ? :effective_committee_agenda_item : :committee_agenda_item)
12
+ params.require(model).permit!
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Effective
2
+ class CommitteeAgendaItemsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ include Effective::CrudController
6
+ log_page_views(if: -> { EffectiveCommittees.log_page_views? })
7
+
8
+ resource_scope -> { Effective::CommitteeAgendaItem.deep }
9
+
10
+ end
11
+ end
@@ -7,5 +7,10 @@ module Effective
7
7
 
8
8
  resource_scope -> { Effective::CommitteeFolder.deep }
9
9
 
10
+ def agenda
11
+ @committee_folder = resource_scope.find(params[:id])
12
+ EffectiveResources.authorize!(self, :agenda, @committee_folder)
13
+ end
14
+
10
15
  end
11
16
  end
@@ -0,0 +1,29 @@
1
+ module Admin
2
+ class EffectiveCommitteeAgendaItemsDatatable < Effective::Datatable
3
+ datatable do
4
+ reorder :position
5
+
6
+ col :updated_at, visible: false
7
+ col :created_at, visible: false
8
+
9
+ col :id, visible: false
10
+
11
+ col :code, label: 'Code'
12
+ col :title
13
+ col :presenter
14
+ col :timed_at, label: 'Timed'
15
+
16
+ actions_col
17
+ end
18
+
19
+ collection do
20
+ scope = Effective::CommitteeAgendaItem.deep.sorted
21
+
22
+ if attributes[:committee_folder_id].present?
23
+ scope = scope.where(committee_folder_id: attributes[:committee_folder_id])
24
+ end
25
+
26
+ scope
27
+ end
28
+ end
29
+ end
@@ -10,8 +10,8 @@ module Admin
10
10
 
11
11
  col :committee
12
12
 
13
- col :parents, label: "Folders" do |committee_folder|
14
- admin_committees_parents(committee_folder)
13
+ col :title do |folder|
14
+ link_to(folder.title, effective_committees.edit_admin_committee_folder_path(folder))
15
15
  end
16
16
 
17
17
  col :slug, visible: false
@@ -24,18 +24,20 @@ module Admin
24
24
  end
25
25
 
26
26
  collection do
27
- folders = Effective::CommitteeFolder.deep.sorted.all
27
+ folders = Effective::CommitteeFolder.deep.sorted
28
28
 
29
29
  if attributes[:committee_folder_id].present?
30
- folders = folders.where(id: committee_folder.children)
30
+ folders = folders.where(committee_folder_id: attributes[:committee_folder_id])
31
+ elsif attributes[:committee_id].present?
32
+ folders = folders.where(
33
+ committee_id: attributes[:committee_id],
34
+ committee_type: attributes[:committee_type],
35
+ committee_folder_id: nil
36
+ )
31
37
  end
32
38
 
33
39
  folders
34
40
  end
35
41
 
36
- def committee_folder
37
- @committee_folder ||= Effective::CommitteeFolder.find_by_id(attributes[:committee_folder_id])
38
- end
39
-
40
42
  end
41
43
  end
@@ -37,8 +37,18 @@ module EffectiveCommitteesHelper
37
37
  end
38
38
 
39
39
  def admin_committees_parents(resource)
40
- parents = resource.parents + [resource]
40
+ parents = []
41
+ parents << resource.committee if resource.respond_to?(:committee) && resource.committee.present?
42
+ parents += resource.parents
43
+ parents << resource
44
+
41
45
  render(partial: 'admin/committees/parents', locals: { parents: parents }, formats: [:html])
42
46
  end
43
47
 
48
+ def committee_file_link(committee_file, label: nil)
49
+ label = label.presence || committee_file.to_s
50
+ return label unless committee_file.file.attached?
51
+ link_to(label, url_for(committee_file.file), target: '_blank')
52
+ end
53
+
44
54
  end
@@ -18,6 +18,8 @@ module Effective
18
18
  has_many :committee_files, -> { Effective::CommitteeFile.sorted }, class_name: 'Effective::CommitteeFile', inverse_of: :committee, dependent: :delete_all
19
19
  accepts_nested_attributes_for :committee_files, allow_destroy: true
20
20
 
21
+ has_many :committee_agenda_items, -> { Effective::CommitteeAgendaItem.sorted }, class_name: 'Effective::CommitteeAgendaItem', inverse_of: :committee, dependent: :delete_all
22
+
21
23
  effective_resource do
22
24
  title :string
23
25
  slug :string
@@ -28,6 +30,8 @@ module Effective
28
30
  committee_folders_count :integer # Counter Cache
29
31
  committee_files_count :integer # Counter Cache
30
32
 
33
+ agenda_mode :boolean
34
+
31
35
  timestamps
32
36
  end
33
37
 
@@ -48,11 +52,19 @@ module Effective
48
52
  title.presence || 'New Committee'
49
53
  end
50
54
 
55
+ # Returns the user's currently-active term on this committee, or nil.
56
+ # For the full history including expired terms, use committee_members_for(user:).
51
57
  def committee_member(user:)
52
- committee_members.find { |member| member.user_id == user.id }
58
+ committee_members.find { |member| member.user_id == user.id && member.active? }
59
+ end
60
+
61
+ # All terms (active and expired) a user has served on this committee.
62
+ def committee_members_for(user:)
63
+ committee_members.select { |member| member.user_id == user.id }
53
64
  end
54
65
 
55
- # Find or build
66
+ # Find-active-or-build-new. If the user has no active term, build a fresh row
67
+ # (expired terms are history and are not edited in place through this helper).
56
68
  def build_committee_member(user:)
57
69
  committee_member(user: user) || committee_members.build(user: user)
58
70
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Effective
4
+ class CommitteeAgendaItem < ActiveRecord::Base
5
+ self.table_name = (EffectiveCommittees.committee_agenda_items_table_name || :committee_agenda_items).to_s
6
+
7
+ log_changes(to: :committee) if respond_to?(:log_changes)
8
+
9
+ belongs_to :committee, polymorphic: true
10
+ belongs_to :committee_folder
11
+
12
+ has_rich_text :body
13
+
14
+ effective_resource do
15
+ code :string
16
+ title :string
17
+ presenter :string
18
+ timed_at :string
19
+
20
+ position :integer
21
+
22
+ timestamps
23
+ end
24
+
25
+ before_validation(if: -> { committee_folder.present? }) do
26
+ self.committee ||= committee_folder.committee
27
+ self.position ||= (committee_folder.committee_agenda_items.maximum(:position) || -1) + 1
28
+ end
29
+
30
+ scope :sorted, -> { order(:position) }
31
+ scope :deep, -> { includes(:rich_text_body) }
32
+
33
+ validates :title, presence: true, length: { maximum: 250 }
34
+ validates :position, presence: true
35
+ validates :code, length: { maximum: 50 }
36
+ validates :presenter, length: { maximum: 250 }
37
+ validates :timed_at, length: { maximum: 250 }
38
+
39
+ def to_s
40
+ [code, title].compact_blank.join(' ').presence || model_name.human
41
+ end
42
+ end
43
+ end
@@ -14,6 +14,8 @@ module Effective
14
14
  has_many :committee_folders, -> { Effective::CommitteeFolder.sorted.deep }, dependent: :destroy, inverse_of: :committee_folder
15
15
  has_many :committee_files, -> { Effective::CommitteeFile.sorted.deep }, dependent: :destroy, inverse_of: :committee_folder
16
16
 
17
+ has_many :committee_agenda_items, -> { Effective::CommitteeAgendaItem.sorted }, dependent: :destroy, inverse_of: :committee_folder
18
+
17
19
  has_many_attached :files
18
20
 
19
21
  effective_resource do
@@ -23,6 +25,8 @@ module Effective
23
25
  position :integer
24
26
  committee_files_count :integer # Counter Cache
25
27
 
28
+ meeting_date :datetime
29
+
26
30
  timestamps
27
31
  end
28
32
 
@@ -33,12 +37,52 @@ module Effective
33
37
  scope :deep, -> { includes(:rich_text_body, :committee, :committee_files) }
34
38
  scope :sorted, -> { order(:position) }
35
39
  scope :top_level, -> { where(committee_folder_id: nil) }
40
+ scope :upcoming_meetings, -> { where.not(meeting_date: nil).where('meeting_date >= ?', 1.week.ago.beginning_of_day).order(:meeting_date) }
41
+ scope :past_meetings, -> { where.not(meeting_date: nil).where('meeting_date < ?', 1.week.ago.beginning_of_day).order(meeting_date: :desc) }
42
+
43
+ # Hierarchical alphabetical sort: roots A→Z, then each root's children A→Z, etc.
44
+ # Returns an Array so it can't be chained further; intended for select dropdowns.
45
+ scope :sorted_for_dropdowns, -> {
46
+ folders = order(:title).to_a
47
+ by_parent = folders.group_by(&:committee_folder_id)
48
+ visible_ids = folders.map(&:id).to_set
49
+
50
+ result = []
51
+ walk = ->(parent_id) {
52
+ Array(by_parent[parent_id]).each do |folder|
53
+ result << folder
54
+ walk.call(folder.id)
55
+ end
56
+ }
57
+ walk.call(nil)
58
+
59
+ # Any folders whose parent isn't in the current scope (e.g. the relation
60
+ # was filtered) should still appear, attached at the top in alphabetical order.
61
+ orphans = folders.reject { |folder| result.include?(folder) }
62
+ .reject { |folder| visible_ids.include?(folder.committee_folder_id) }
63
+ orphans.each do |folder|
64
+ result << folder
65
+ walk.call(folder.id)
66
+ end
67
+
68
+ result
69
+ }
36
70
 
37
71
  validates :title, presence: true, length: { maximum: 250 },
38
- uniqueness: { scope: [:committee_id], message: 'already exists for this committee'}
72
+ uniqueness: { scope: [:committee_id, :committee_folder_id], message: 'already exists in this folder' }
39
73
 
40
74
  validates :position, presence: true
41
75
 
76
+ validate(if: -> { meeting_date.present? }) do
77
+ if parents.any?(&:meeting?)
78
+ errors.add(:meeting_date, "can't be set — a parent folder is already an agenda")
79
+ end
80
+
81
+ if persisted? && committee_folders.any? { |child| child.meeting_date.present? || child.send(:any_descendant_meeting?) }
82
+ errors.add(:meeting_date, "can't be set — a child folder is already an agenda")
83
+ end
84
+ end
85
+
42
86
  def to_s
43
87
  (parents + [self]).map { |folder| (folder.title || 'folder') }.join(' / ')
44
88
  end
@@ -52,6 +96,14 @@ module Effective
52
96
  committee_folder.blank?
53
97
  end
54
98
 
99
+ def meeting?
100
+ meeting_date.present?
101
+ end
102
+
103
+ def agenda_section?
104
+ committee_folder&.meeting?
105
+ end
106
+
55
107
  def parent
56
108
  committee_folder || committee
57
109
  end
@@ -72,5 +124,11 @@ module Effective
72
124
  committee_folders.flat_map { |folder| [folder] + folder.children }
73
125
  end
74
126
 
127
+ protected
128
+
129
+ def any_descendant_meeting?
130
+ committee_folders.any? { |child| child.meeting_date.present? || child.any_descendant_meeting? }
131
+ end
132
+
75
133
  end
76
134
  end
@@ -0,0 +1,3 @@
1
+ = tabs do
2
+ = tab 'Agenda Item' do
3
+ = render 'admin/committee_agenda_items/form_committee_agenda_item', committee_agenda_item: committee_agenda_item
@@ -0,0 +1,37 @@
1
+ = effective_form_with(model: [:admin, committee_agenda_item], engine: true) do |f|
2
+ - f.object.committee_type ||= Effective::Committee.name
3
+
4
+ = f.hidden_field :committee_type
5
+ = f.hidden_field :committee_id
6
+
7
+ - if inline_datatable? && inline_datatable.attributes[:committee_folder_id].present?
8
+ = f.hidden_field :committee_folder_id
9
+ - elsif f.object.persisted?
10
+ = f.static_field :committee
11
+ = f.select :committee_folder_id, Effective::CommitteeFolder.sorted.where(committee: f.object.committee), label: "Section"
12
+ - else
13
+ = f.select :committee_id, Effective::Committee.sorted.all,
14
+ 'data-load-ajax-url': effective_committees.new_admin_committee_agenda_item_path,
15
+ 'data-load-ajax-div': '#effective-committees-ajax'
16
+
17
+ #effective-committees-ajax
18
+ = f.select :committee_folder_id, Effective::CommitteeFolder.sorted.where(committee: f.object.committee), label: "Section"
19
+
20
+ .row
21
+ .col-md-3
22
+ = f.text_field :code, hint: "e.g. A-1"
23
+ .col-md-9
24
+ = f.text_field :title
25
+
26
+ .row
27
+ .col-md-6
28
+ = f.text_field :presenter
29
+ .col-md-6
30
+ = f.text_field :timed_at, label: "Time", hint: "e.g. 3:00pm"
31
+
32
+ - if defined?(EffectiveArticleEditor)
33
+ = f.article_editor :body
34
+ - else
35
+ = f.rich_text_area :body, hint: "Rich text shown under the item title."
36
+
37
+ = f.submit
@@ -1,24 +1,34 @@
1
1
  - if committee_folder.persisted?
2
2
  %p= admin_committees_parents(committee_folder)
3
3
 
4
+ - agenda_section = committee_folder.committee.agenda_mode? && committee_folder.agenda_section?
5
+
4
6
  = tabs do
5
7
  - if committee_folder.persisted?
6
8
  = tab 'Folder' do
7
- %h3 Folders
8
- %p.text-muted
9
- %small Refresh the page after creating a new folder to move files to it.
9
+ - if agenda_section
10
+ %h3 Agenda Items
11
+ %p.text-muted
12
+ %small Structured items rendered on the agenda page. Drag the reorder handle to change position.
13
+
14
+ - datatable = Admin::EffectiveCommitteeAgendaItemsDatatable.new(committee_folder_id: committee_folder.id, committee_id: committee_folder.committee_id, committee_type: committee_folder.committee_type)
15
+ = render_datatable(datatable, inline: true)
16
+ - else
17
+ %h3 Folders
18
+ %p.text-muted
19
+ %small Refresh the page after creating a new folder to move files to it.
10
20
 
11
- - datatable = Admin::EffectiveCommitteeFoldersDatatable.new(committee_folder: committee_folder, committee: committee_folder.committee)
12
- = render_datatable(datatable, inline: true, simple: true)
21
+ - datatable = Admin::EffectiveCommitteeFoldersDatatable.new(committee_folder: committee_folder, committee: committee_folder.committee)
22
+ = render_datatable(datatable, inline: true, simple: true)
13
23
 
14
- %h3 Files
15
- %p.text-muted
16
- %small Select the files, then use the Bulk Actions dropdown to move them to another folder
24
+ %h3 Files
25
+ %p.text-muted
26
+ %small Select the files, then use the Bulk Actions dropdown to move them to another folder
17
27
 
18
- .mt-4= render('admin/committee_folders/form_bulk_upload', committee_folder: committee_folder)
28
+ .mt-4= render('admin/committee_folders/form_bulk_upload', committee_folder: committee_folder)
19
29
 
20
- - datatable = Admin::EffectiveCommitteeFilesDatatable.new(committee_folder: committee_folder, committee: committee_folder.committee)
21
- = render_datatable(datatable, inline: true)
30
+ - datatable = Admin::EffectiveCommitteeFilesDatatable.new(committee_folder: committee_folder, committee: committee_folder.committee)
31
+ = render_datatable(datatable, inline: true)
22
32
 
23
33
  = tab 'Folder Details' do
24
34
  = render 'admin/committee_folders/form_committee_folder', committee_folder: committee_folder
@@ -8,22 +8,32 @@
8
8
  = f.hidden_field :committee_folder_id
9
9
  - elsif f.object.persisted?
10
10
  = f.static_field :committee
11
- = f.select :committee_folder_id, Effective::CommitteeFolder.sorted.where(committee: f.object.committee), label: "Parent Folder", hint: "Leave blank to create a top-level folder"
11
+ = f.select :committee_folder_id, Effective::CommitteeFolder.where(committee: f.object.committee).sorted_for_dropdowns, label: "Parent Folder", hint: "Leave blank to create a top-level folder"
12
12
  - else
13
13
  = f.select :committee_id, Effective::Committee.sorted.all,
14
14
  'data-load-ajax-url': effective_committees.new_admin_committee_folder_path,
15
15
  'data-load-ajax-div': '#effective-committees-ajax'
16
16
 
17
17
  #effective-committees-ajax
18
- = f.select :committee_folder_id, Effective::CommitteeFolder.sorted.where(committee: f.object.committee), label: "Parent Folder", hint: "Leave blank to create a top-level folder"
18
+ = f.select :committee_folder_id, Effective::CommitteeFolder.where(committee: f.object.committee).sorted_for_dropdowns, label: "Parent Folder", hint: "Leave blank to create a top-level folder"
19
19
 
20
20
  = f.text_field :title
21
21
 
22
22
  = acts_as_slugged_fields(f, url: (effective_committees.commitee_url(f.object) rescue nil))
23
23
 
24
+ - if f.object.committee&.agenda_mode?
25
+ - meeting_ancestor = f.object.parents.detect(&:meeting?)
26
+ - if meeting_ancestor.present?
27
+ .alert.alert-info.mb-3
28
+ This folder is inside the &ldquo;#{meeting_ancestor.title}&rdquo; agenda, so it can&rsquo;t be turned into its own agenda. It should be treated as a section of the parent agenda.
29
+ - else
30
+ = f.datetime_field :meeting_date, label: "Meeting date & time",
31
+ hint: "Turns this folder into an agenda. Child folders become the agenda's sections (A, B, C, etc.), and within each section folder you can add agenda items. Details added to the Body below will appear at the top of the agenda page.".html_safe
32
+
24
33
  - if defined?(EffectiveArticleEditor)
25
34
  = f.article_editor :body, hint: "Displayed on the #{committee_folder_label} page"
26
35
  - else
27
36
  = f.rich_text_area :body, hint: "Displayed on the #{committee_folder_label} page"
28
37
 
38
+
29
39
  = f.submit
@@ -25,6 +25,14 @@
25
25
  = f.static_field :committee
26
26
  = f.static_field :user, label: committee_member_label
27
27
 
28
+ - other_terms = f.object.committee.committee_members_for(user: f.object.user).reject { |m| m.id == f.object.id }
29
+ - if other_terms.any?
30
+ .mb-3
31
+ %strong Other terms for this user on this committee
32
+ %ul
33
+ - other_terms.each do |term|
34
+ %li= [term.start_on || '(open start)', term.end_on || '(open end)'].join(' to ')
35
+
28
36
  = f.text_field :category
29
37
 
30
38
  .row
@@ -5,6 +5,9 @@
5
5
  = f.check_box :display_on_index, label: "Show on the member-only #{link_to "Volunteers and Committees", effective_committees.volunteers_and_committees_path, target: '_blank'} page"
6
6
  = f.check_box :display_on_dashboard, label: "Show on the #{link_to "My #{committee_label.pluralize}", effective_committees.committees_path, target: '_blank'} page".html_safe
7
7
 
8
+ = f.check_box :agenda_mode, label: "Enable structured meeting agendas for this #{committee_label}",
9
+ hint: "When checked, any subfolder with a meeting date has the meeting agenda view enabled.".html_safe
10
+
8
11
  - if defined?(EffectiveArticleEditor)
9
12
  = f.article_editor :body, hint: "Shown on the #{committee_label} page."
10
13
  - else
@@ -0,0 +1,30 @@
1
+ -# Renders a single agenda item's content (left column + right metadata column).
2
+ -# Locals:
3
+ -# item: CommitteeAgendaItem
4
+ -# compact: true when rendered in the long agenda list (smaller heading)
5
+
6
+ - compact = local_assigns.fetch(:compact, false)
7
+
8
+ .row
9
+ .col-md-9
10
+ - if compact
11
+ %h4.mb-2
12
+ - if item.code.present?
13
+ %span.text-muted.me-2= item.code
14
+ = item.title
15
+ - else
16
+ %h1.card-title.mb-2
17
+ - if item.code.present?
18
+ %span.text-muted.me-2= item.code
19
+ = item.title
20
+
21
+ - if item.body.present?
22
+ .agenda-item-body= item.body.to_s
23
+
24
+ .col-md-3.text-md-end
25
+ - if item.presenter.present?
26
+ %p.mb-1
27
+ %strong= item.presenter
28
+ - if item.timed_at.present?
29
+ %p.mb-0.text-muted
30
+ %em= item.timed_at
@@ -0,0 +1,14 @@
1
+ - committee = committee_agenda_item.committee
2
+ - section = committee_agenda_item.committee_folder
3
+
4
+ - trail = section.parents.map { |parent| [parent.title, effective_committees.committee_committee_folder_path(committee, parent)] }
5
+ - trail << [section.title, effective_committees.committee_committee_folder_path(committee, section)]
6
+ = render 'effective/committee_folders/breadcrumb', committee: committee, trail: trail, current: committee_agenda_item.to_s
7
+
8
+ .effective-committee-agenda-item
9
+ .card.mb-4
10
+ .card-body
11
+ = render 'effective/committee_agenda_items/card', item: committee_agenda_item
12
+
13
+ %p
14
+ = link_to "← Back", effective_committees.committee_committee_folder_path(committee, section), class: 'btn btn-link p-0'
@@ -0,0 +1,10 @@
1
+ - resource = (@_effective_resource || Effective::Resource.new(controller_path))
2
+ - @resource = instance_variable_get('@' + resource.name) if resource.name
3
+
4
+ - if @resource
5
+ .resource-buttons
6
+ = render_resource_buttons(@resource, show: false)
7
+
8
+ = render_resource_partial(@resource)
9
+
10
+ .my-4
@@ -0,0 +1,42 @@
1
+ - committee = committee_folder.committee
2
+ - sections = committee_folder.committee_folders.sorted.includes(:rich_text_body, committee_agenda_items: :rich_text_body)
3
+
4
+ - trail = committee_folder.parents.map { |parent| [parent.title, effective_committees.committee_committee_folder_path(committee, parent)] }
5
+ = render 'effective/committee_folders/breadcrumb', committee: committee, trail: trail, current: committee_folder.title
6
+
7
+ .effective-committee-agenda
8
+ %h1.mb-1= committee.to_s
9
+ %h2.text-muted.mb-3= committee_folder.title
10
+
11
+ .card.mb-4
12
+ .card-body
13
+
14
+ - if committee_folder.meeting_date.present?
15
+ %p.mb-3
16
+ %strong Date:
17
+ = committee_folder.meeting_date.strftime('%A, %B %-d, %Y at %-l:%M %p')
18
+
19
+ - if committee_folder.rich_text_body.present?
20
+ = committee_folder.rich_text_body.to_s
21
+
22
+ - sections.each do |section|
23
+ %section.mb-5{id: "section-#{section.id}"}
24
+ %h2.mb-3= section.title
25
+ - if section.rich_text_body.present?
26
+ .mb-3= section.rich_text_body.to_s
27
+
28
+ - items = section.committee_agenda_items.sorted
29
+ - if items.any?
30
+ .card
31
+ .list-group.list-group-flush
32
+ - items.each do |item|
33
+ .list-group-item{id: "item-#{item.id}"}
34
+ = render 'effective/committee_agenda_items/card', item: item, compact: true
35
+
36
+ - meeting_files = committee_folder.committee_files.sorted
37
+ - if meeting_files.any?
38
+ %hr
39
+ %h3 Meeting Files
40
+ %ul
41
+ - meeting_files.each do |committee_file|
42
+ %li= committee_file_link(committee_file)
@@ -0,0 +1,13 @@
1
+ -# Locals:
2
+ -# committee: the Committee
3
+ -# trail: [[title, path], ...] — non-active links between the committee and the active page
4
+ -# current: title string of the active leaf
5
+
6
+ %nav{'aria-label': 'breadcrumb'}
7
+ %ol.breadcrumb
8
+ %li.breadcrumb-item= link_to("My Dashboard", return_to_dashboard_path)
9
+ %li.breadcrumb-item= link_to("My #{committees_label}", effective_committees.committees_path)
10
+ %li.breadcrumb-item= link_to(committee, effective_committees.committee_path(committee))
11
+ - trail.each do |title, path|
12
+ %li.breadcrumb-item= link_to(title, path)
13
+ %li.breadcrumb-item.active{'aria-current': 'page'}= current
@@ -1,31 +1,54 @@
1
1
  = render('layout') do
2
2
  - committee = committee_folder.committee
3
3
 
4
- %nav{'aria-label': 'breadcrumb'}
5
- %ol.breadcrumb
6
- %li.breadcrumb-item= link_to("My Dashboard", return_to_dashboard_path)
7
- %li.breadcrumb-item= link_to("My #{committees_label}", effective_committees.committees_path)
8
- %li.breadcrumb-item= link_to(committee, effective_committees.committee_path(committee))
9
-
10
- - committee_folder.parents.each do |parent|
11
- %li.breadcrumb-item= link_to(parent.title, effective_committees.committee_committee_folder_path(committee, parent))
12
-
13
- %li.breadcrumb-item.active{'aria-current': 'page'}= committee_folder.title
4
+ - trail = committee_folder.parents.map { |parent| [parent.title, effective_committees.committee_committee_folder_path(committee, parent)] }
5
+ = render 'effective/committee_folders/breadcrumb', committee: committee, trail: trail, current: committee_folder.title
14
6
 
15
7
  .effective-committee-folder
16
- %h1= committee_folder.title
8
+ .d-flex.justify-content-between.align-items-start.mb-3
9
+ %h1.mb-0= committee_folder.title
10
+ - if committee.agenda_mode? && committee_folder.meeting?
11
+ = link_to "View Agenda", effective_committees.agenda_committee_committee_folder_path(committee, committee_folder), class: 'btn btn-primary'
17
12
 
18
13
  - if committee_folder.rich_text_body.present?
19
14
  .mb-4= committee_folder.rich_text_body.to_s
20
15
 
21
16
  .my-4
22
17
 
18
+ - agenda_items = committee_folder.committee_agenda_items.sorted if committee.agenda_mode?
19
+
20
+ - if committee.agenda_mode? && agenda_items.present?
21
+ %h3 Agenda Items
22
+ .card.mb-4
23
+ .list-group.list-group-flush
24
+ - agenda_items.each do |agenda_item|
25
+ = link_to(effective_committees.committee_committee_folder_committee_agenda_item_path(committee, committee_folder, agenda_item), class: 'list-group-item list-group-action') do
26
+ .row
27
+ .col-md-9
28
+ - if agenda_item.code.present?
29
+ %span.text-muted.me-2= agenda_item.code
30
+ = agenda_item.title
31
+ .col-md-3.text-md-end
32
+ - if agenda_item.presenter.present?
33
+ %small.text-muted= agenda_item.presenter
34
+ - if agenda_item.timed_at.present?
35
+ %small.text-muted.ms-2= agenda_item.timed_at
36
+
37
+ - meeting_children = committee_folder.committee_folders.select(&:meeting?) if committee.agenda_mode?
38
+
39
+ - if committee.agenda_mode? && meeting_children.present?
40
+ %h3 Agendas
41
+ .card.mb-4
42
+ .list-group.list-group-flush
43
+ - meeting_children.each do |meeting_folder|
44
+ = link_to(meeting_folder.title, effective_committees.agenda_committee_committee_folder_path(committee, meeting_folder), class: 'list-group-item list-group-action')
45
+
23
46
  - if committee_folder.committee_folders.present?
24
47
  %h3 Folders
25
- .card
48
+ .card.mb-4
26
49
  .list-group.list-group-flush
27
- - committee_folder.committee_folders.each do |committee_folder|
28
- = link_to(committee_folder.title, effective_committees.committee_committee_folder_path(committee, committee_folder), class: 'list-group-item list-group-action')
50
+ - committee_folder.committee_folders.each do |child_folder|
51
+ = link_to(child_folder.title, effective_committees.committee_committee_folder_path(committee, child_folder), class: 'list-group-item list-group-action')
29
52
 
30
53
  %h3 Files
31
54
  .card
@@ -43,14 +66,13 @@
43
66
  - committee_folder.committee_files.each_with_index do |file, index|
44
67
  %tr
45
68
  %td= index+1
46
- %td
47
- - if file.file.attached?
48
- = link_to(file.to_s, url_for(file.file), target: '_blank')
49
- - else
50
- = file.to_s
51
-
69
+ %td= committee_file_link(file)
52
70
  %td.d-none.d-lg-table-cell= file.created_at.strftime('%F')
53
71
  %td.d-none.d-lg-table-cell= number_to_human_size(file.file.byte_size)
54
72
  %td.d-none.d-lg-table-cell= file.notes
55
73
  - else
56
74
  %p No files.
75
+
76
+ - back_path = committee_folder.committee_folder.present? ? effective_committees.committee_committee_folder_path(committee, committee_folder.committee_folder) : effective_committees.committee_path(committee)
77
+ %p.mt-4
78
+ = link_to "← Back", back_path, class: 'btn btn-link p-0'
@@ -0,0 +1,2 @@
1
+ = render('layout') do
2
+ = render 'effective/committee_folders/agenda', committee_folder: @committee_folder
@@ -3,7 +3,7 @@
3
3
 
4
4
  - if @resource
5
5
  .resource-buttons
6
- = render_resource_buttons(@resource, show: false)
6
+ = render_resource_buttons(@resource, show: false, agenda: false)
7
7
 
8
8
  = render_resource_partial(@resource)
9
9
 
@@ -0,0 +1,18 @@
1
+ - agenda_committees = current_user.committees.select(&:agenda_mode?)
2
+ - upcoming = agenda_committees.flat_map { |c| Effective::CommitteeFolder.where(committee: c).upcoming_meetings.limit(5) }.sort_by(&:meeting_date).first(5)
3
+
4
+ - if upcoming.any? || recent.any?
5
+ %h3 Upcoming Meetings
6
+
7
+ - if upcoming.any?
8
+ .card
9
+ .list-group.list-group-flush
10
+ - upcoming.each do |meeting|
11
+ = link_to effective_committees.agenda_committee_committee_folder_path(meeting.committee, meeting), class: 'list-group-item list-group-action' do
12
+ %strong= meeting.committee.to_s
13
+ %br
14
+ = meeting.title
15
+ - else
16
+ %p.text-muted No upcoming meetings.
17
+
18
+ .mb-5
@@ -33,4 +33,7 @@
33
33
  %p No files or folders.
34
34
 
35
35
  .col-lg
36
+ - if current_user.committees.any?(&:agenda_mode?)
37
+ = render 'effective/committees/dashboard_agendas'
38
+
36
39
  = render 'effective/committees/dashboard_activity'
data/config/routes.rb CHANGED
@@ -10,7 +10,10 @@ EffectiveCommittees::Engine.routes.draw do
10
10
  resources :committees, only: [:index, :show] do
11
11
  get 'activity', on: :collection
12
12
 
13
- resources :committee_folders, only: [:show]
13
+ resources :committee_folders, only: [:show], path: 'folders' do
14
+ member { get :agenda }
15
+ resources :committee_agenda_items, only: [:show]
16
+ end
14
17
  end
15
18
 
16
19
  get 'volunteers-and-committees', to: 'committees#volunteers_and_committees', as: 'volunteers_and_committees'
@@ -26,6 +29,7 @@ EffectiveCommittees::Engine.routes.draw do
26
29
  resources :committee_files, except: [:show] do
27
30
  post :bulk_move, on: :collection
28
31
  end
32
+ resources :committee_agenda_items, except: [:show, :index]
29
33
  end
30
34
 
31
35
  end
@@ -13,6 +13,8 @@ class CreateEffectiveCommittees < ActiveRecord::Migration[6.0]
13
13
  t.boolean :display_on_index, default: true
14
14
  t.boolean :display_on_dashboard, default: true
15
15
 
16
+ t.boolean :agenda_mode, default: false, null: false
17
+
16
18
  t.datetime :updated_at
17
19
  t.datetime :created_at
18
20
  end
@@ -57,6 +59,8 @@ class CreateEffectiveCommittees < ActiveRecord::Migration[6.0]
57
59
  t.integer :position
58
60
  t.integer :committee_files_count, default: 0
59
61
 
62
+ t.datetime :meeting_date
63
+
60
64
  t.datetime :updated_at
61
65
  t.datetime :created_at
62
66
  end
@@ -64,6 +68,7 @@ class CreateEffectiveCommittees < ActiveRecord::Migration[6.0]
64
68
  add_index :committee_folders, [:committee_id, :committee_type]
65
69
  add_index :committee_folders, [:position]
66
70
  add_index :committee_folders, :committee_id, if_not_exists: true
71
+ add_index :committee_folders, [:committee_id, :meeting_date]
67
72
 
68
73
  create_table :committee_files do |t|
69
74
  t.integer :committee_id
@@ -85,5 +90,25 @@ class CreateEffectiveCommittees < ActiveRecord::Migration[6.0]
85
90
 
86
91
  add_index :committee_files, :committee_id, if_not_exists: true
87
92
  add_index :committee_files, :title, if_not_exists: true
93
+
94
+ create_table :committee_agenda_items do |t|
95
+ t.integer :committee_id
96
+ t.string :committee_type
97
+
98
+ t.integer :committee_folder_id
99
+
100
+ t.string :code
101
+ t.string :title
102
+ t.string :presenter
103
+ t.string :timed_at
104
+
105
+ t.integer :position
106
+
107
+ t.datetime :updated_at
108
+ t.datetime :created_at
109
+ end
110
+
111
+ add_index :committee_agenda_items, :committee_folder_id
112
+ add_index :committee_agenda_items, [:committee_id, :committee_type]
88
113
  end
89
114
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveCommittees
2
- VERSION = '0.9.5'.freeze
2
+ VERSION = '0.10.0'.freeze
3
3
  end
@@ -9,6 +9,7 @@ module EffectiveCommittees
9
9
  def self.config_keys
10
10
  [
11
11
  :committees_table_name, :committee_members_table_name, :committee_folders_table_name, :committee_files_table_name,
12
+ :committee_agenda_items_table_name,
12
13
  :layout, :use_effective_roles, :log_page_views
13
14
  ]
14
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_committees
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-30 00:00:00.000000000 Z
11
+ date: 2026-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -207,13 +207,16 @@ files:
207
207
  - app/assets/javascripts/effective_committees/base.js
208
208
  - app/assets/stylesheets/effective_committees.scss
209
209
  - app/assets/stylesheets/effective_committees/base.scss
210
+ - app/controllers/admin/committee_agenda_items_controller.rb
210
211
  - app/controllers/admin/committee_files_controller.rb
211
212
  - app/controllers/admin/committee_folders_controller.rb
212
213
  - app/controllers/admin/committee_members_controller.rb
213
214
  - app/controllers/admin/committees_controller.rb
215
+ - app/controllers/effective/committee_agenda_items_controller.rb
214
216
  - app/controllers/effective/committee_folders_controller.rb
215
217
  - app/controllers/effective/committee_members_controller.rb
216
218
  - app/controllers/effective/committees_controller.rb
219
+ - app/datatables/admin/effective_committee_agenda_items_datatable.rb
217
220
  - app/datatables/admin/effective_committee_files_datatable.rb
218
221
  - app/datatables/admin/effective_committee_folders_datatable.rb
219
222
  - app/datatables/admin/effective_committee_members_datatable.rb
@@ -224,9 +227,12 @@ files:
224
227
  - app/helpers/effective_committees_helper.rb
225
228
  - app/models/concerns/effective_committees_user.rb
226
229
  - app/models/effective/committee.rb
230
+ - app/models/effective/committee_agenda_item.rb
227
231
  - app/models/effective/committee_file.rb
228
232
  - app/models/effective/committee_folder.rb
229
233
  - app/models/effective/committee_member.rb
234
+ - app/views/admin/committee_agenda_items/_form.html.haml
235
+ - app/views/admin/committee_agenda_items/_form_committee_agenda_item.html.haml
230
236
  - app/views/admin/committee_files/_form.html.haml
231
237
  - app/views/admin/committee_files/_form_committee_file.html.haml
232
238
  - app/views/admin/committee_folders/_form.html.haml
@@ -239,14 +245,21 @@ files:
239
245
  - app/views/admin/committees/_form.html.haml
240
246
  - app/views/admin/committees/_form_committee.html.haml
241
247
  - app/views/admin/committees/_parents.html.haml
248
+ - app/views/effective/committee_agenda_items/_card.html.haml
249
+ - app/views/effective/committee_agenda_items/_committee_agenda_item.html.haml
250
+ - app/views/effective/committee_agenda_items/show.html.haml
251
+ - app/views/effective/committee_folders/_agenda.html.haml
252
+ - app/views/effective/committee_folders/_breadcrumb.html.haml
242
253
  - app/views/effective/committee_folders/_committee_folder.html.haml
243
254
  - app/views/effective/committee_folders/_layout.html.haml
255
+ - app/views/effective/committee_folders/agenda.html.haml
244
256
  - app/views/effective/committee_folders/show.html.haml
245
257
  - app/views/effective/committee_members/_form.html.haml
246
258
  - app/views/effective/committee_members/_user_fields.html.haml
247
259
  - app/views/effective/committees/_committee.html.haml
248
260
  - app/views/effective/committees/_dashboard.html.haml
249
261
  - app/views/effective/committees/_dashboard_activity.html.haml
262
+ - app/views/effective/committees/_dashboard_agendas.html.haml
250
263
  - app/views/effective/committees/_fields.html.haml
251
264
  - app/views/effective/committees/_form.html.haml
252
265
  - app/views/effective/committees/_form_committee.html.haml