diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb index 28d7b6fc58..69a87af4ea 100644 --- a/app/decorators/application_decorator.rb +++ b/app/decorators/application_decorator.rb @@ -23,4 +23,29 @@ def published? def external_link? false end + + # One-line summary of the tagged sectors for a collapsed form section, primary + # first. The primary sector is bold with a ⭐; a sector leader gets a 👑 and a + # trailing "(sector leader)". Excludes the "Other" catch-all. HTML-safe. + def sectors_summary + items = object.sectorable_items_primary_first.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } + return "None selected" if items.empty? + + h.safe_join(items.map { |item| sector_summary_chip(item) }, ", ") + end + + private + + # A single sector's chip, kept on one line (whitespace-nowrap) so the icon, + # name, and "(sector leader)" never split across a wrap — breaks fall between + # sectors, at the joining comma. + def sector_summary_chip(item) + name = item.sector&.name.to_s + pieces = [] + pieces << h.content_tag(:i, "", class: "fa-solid fa-crown text-lime-600") if item.is_leader? + pieces << h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400") if item.is_primary? + pieces << (item.is_primary? ? h.content_tag(:strong, name) : name) + pieces << "(sector leader)" if item.is_leader? + h.content_tag(:span, h.safe_join(pieces, " "), class: "whitespace-nowrap") + end end diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index 34d02d9671..a2193132c4 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -56,6 +56,31 @@ def agency_type_option Organization::AGENCY_TYPE_OTHER end + # Optional Background-info fields, mapped to the short pill label shown in the + # collapsed section summary when they're filled in. + BACKGROUND_FIELD_LABELS = { + email: "Email", + website_url: "Website", + description: "Description", + mission_vision_values: "Mission/vision/values" + }.freeze + + # One-line summary for the collapsed Background Info section: the organization + # type (the specify-text when it's "Other"), followed by a pill for each filled + # optional field. FileMaker ID is intentionally left out — it's admin-only. + # HTML-safe. + def background_summary + type = agency_type_option.presence + type = "#{type}: #{object.agency_type_other}" if type == Organization::AGENCY_TYPE_OTHER && object.agency_type_other.present? + + parts = [ h.content_tag(:span, type || "Type not set", class: "text-gray-600") ] + BACKGROUND_FIELD_LABELS.each do |attr, pill_label| + next if object.public_send(attr).blank? + parts << h.content_tag(:span, pill_label, class: "text-xs font-normal px-2 py-0.5 rounded-full bg-gray-100 text-gray-600") + end + h.safe_join(parts, " ") + end + def detail(length: nil) length ? description&.truncate(length) : description end @@ -103,6 +128,29 @@ def facilitator_status_as_of(date) active ? :ongoing : :reinstated end + # Profile display toggles in form order, mapped to the noun used on each + # checkbox ("Show email" => "email"). Drives the collapsed form section's + # one-line summary. + PROFILE_DISPLAY_LABELS = { + profile_show_email: "email", + profile_show_phone: "phone", + profile_show_website: "website", + profile_show_description: "description", + profile_show_sectors: "sectors", + profile_show_workshops: "workshops", + profile_show_stories: "stories", + profile_show_events_registered: "events hosted", + profile_show_workshop_logs: "workshop logs" + }.freeze + + # One-line summary of the profile display preferences for the collapsed form + # section. Most orgs show everything, so it names only what's hidden + # ("Hide phone and website") and says "All shown" when nothing is hidden. + def profile_display_summary + hidden = PROFILE_DISPLAY_LABELS.reject { |attr, _| object.public_send(attr) }.values + hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" + end + def badges earliest = affiliations.minimum(:start_date) || start_date years = earliest ? (Time.zone.now.year - earliest.year) : nil diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 09bb4d5421..2ca2d68c30 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -76,6 +76,76 @@ def badges @badges ||= compute_badges end + # Profile display toggles in form order, mapped to the noun used on each + # checkbox ("Show email" => "email"). Drives the collapsed form section's + # one-line summary. + PROFILE_DISPLAY_LABELS = { + profile_show_credentials: "credentials", + profile_show_pronouns: "pronouns", + profile_show_email: "email", + profile_show_phone: "phone", + profile_show_social_media: "social media", + profile_show_member_since: "facilitator since", + profile_show_bio: "bio", + profile_show_affiliations: "affiliations", + profile_show_sectors: "sectors", + profile_show_workshops: "workshops", + profile_show_workshop_variations: "workshop variations", + profile_show_stories: "stories", + profile_show_resources: "resources", + profile_show_events_registered: "registrations", + profile_show_story_ideas: "story ideas", + profile_show_workshop_ideas: "workshop ideas", + profile_show_workshop_variation_ideas: "variation ideas", + profile_show_workshop_logs: "workshop logs" + }.freeze + + # One-line summary of the profile display preferences for the collapsed form + # section. Most people show everything, so it names only what's hidden + # ("Hide phone and bio") and says "All shown" when nothing is hidden. + def profile_display_summary + hidden = PROFILE_DISPLAY_LABELS.reject { |attr, _| object.public_send(attr) }.values + hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" + end + + # Social-media URL fields mapped to the platform label shown as a pill in the + # collapsed section summary when the field is filled in. + SOCIAL_MEDIA_LABELS = { + linked_in_url: "LinkedIn", + facebook_url: "Facebook", + instagram_url: "Instagram", + youtube_url: "YouTube", + twitter_url: "Twitter" + }.freeze + + # One-line summary of the social-media links for the collapsed form section: a + # grey pill for each platform with a URL on file, or "None". HTML-safe. + def social_media_summary + present = SOCIAL_MEDIA_LABELS.select { |attr, _| object.public_send(attr).present? }.values + return "None" if present.empty? + + h.safe_join(present.map { |label| + h.content_tag(:span, label, class: "text-xs font-normal px-2 py-0.5 rounded-full bg-gray-100 text-gray-600") + }, " ") + end + + # One-line summary of the tagged age ranges for a collapsed form section. The + # primary age group is bold with a ⭐ (age ranges have no leader flag). HTML-safe. + def age_ranges_summary + items = object.age_range_items_ordered + return "None selected" if items.empty? + + h.safe_join(items.map { |item| + name = item.category&.name.to_s + inner = if item.is_primary? + h.safe_join([ h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400"), h.content_tag(:strong, name) ], " ") + else + name + end + h.content_tag(:span, inner, class: "whitespace-nowrap") + }, ", ") + end + def facilitator_since_date @facilitator_since_date ||= begin facilitator_affiliations = affiliations.facilitators diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 99b790e99c..db6f05f767 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -293,7 +293,10 @@ def form_field_option_source(field) # model identity (DomainTheme color + default label + default icon + count # + default index path). Pass `params:` for filter params, or `path:` to # override entirely (e.g. nested routes). - def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_count: false, hide_icon: false, data: {}) + # wrap: renders a fixed two-line-tall button whose label wraps (rather than + # truncating on one line) — used where buttons share a row at equal width, so + # long labels stay readable and every button lines up to the same height. + def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_count: false, hide_icon: false, wrap: false, data: {}) klass = collection.klass key = klass.name.underscore.pluralize.to_sym label ||= key.to_s.humanize @@ -308,24 +311,25 @@ def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_ link_to path, data: { turbo_prefetch: false }.merge(data), class: "group flex items-center gap-3 w-full px-3 py-2 rounded-lg + #{'h-full min-h-[3.5rem]' if wrap} border #{border} #{bg} #{hover_bg} transition-colors duration-200 shadow-sm" do icon_tag = if hide_icon "".html_safe else - content_tag(:span, class: "#{text} w-5 text-center") do + content_tag(:span, class: "#{text} w-5 text-center shrink-0") do content_tag(:i, "", class: "fa-solid #{icon}") end end - label_tag = content_tag(:span, label, class: "font-medium #{text} truncate") + label_tag = content_tag(:span, label, class: "font-medium #{text} #{wrap ? 'line-clamp-2' : 'truncate'}") count_tag = if hide_count "".html_safe else content_tag(:span, number_with_delimiter(collection.count), - class: "ml-auto inline-flex items-center justify-center min-w-[2.25rem] px-2 py-0.5 text-sm font-semibold rounded-full bg-white #{text} border #{border}") + class: "ml-auto shrink-0 inline-flex items-center justify-center min-w-[2.25rem] px-2 py-0.5 text-sm font-semibold rounded-full bg-white #{text} border #{border}") end icon_tag + label_tag + count_tag diff --git a/app/views/contact_methods/_contact_method_fields.html.erb b/app/views/contact_methods/_contact_method_fields.html.erb index ee19ec2736..773d730901 100644 --- a/app/views/contact_methods/_contact_method_fields.html.erb +++ b/app/views/contact_methods/_contact_method_fields.html.erb @@ -1,8 +1,10 @@
Check every age group served, then mark the primary ones.
+ <% end %> +Check every age group served, then mark the primary ones.
- <% end %> -Auto-managed from affiliation records. Earliest affiliation start date: <%= org_earliest_aff.strftime('%b %Y') %>.
<% if f.object.start_date.present? && f.object.start_date.beginning_of_month != org_earliest_aff.beginning_of_month %> @@ -312,7 +360,7 @@ -Earliest start date from affiliations with 'facilitator' in the title. Edit affiliation records to change this date.
New — no facilitator affiliations yet. Ongoing — has an active facilitator. Reinstate — had facilitators but all have ended.
- Only - <% if allowed_to?(:edit?, f.object.user) %> - <%= link_to "editable by admins", - edit_user_path(f.object.user), - class: "underline" %> - <% else %> - editable by admins + <%# 2-up on mobile (First | Legal, Last | Pronouns), 4-across on md+. %> +
+ Only + <% if allowed_to?(:edit?, f.object.user) %> + <%= link_to "editable by admins", + edit_user_path(f.object.user), + class: "underline" %> + <% else %> + editable by admins + <% end %> +
+Earliest start date from any affiliation, regardless of title.
⚠ No affiliations have a start date. Legacy member_since value is <%= person.member_since.strftime('%b %Y') %>. Add a start date to an affiliation to use it here.
Earliest start date from affiliations with 'facilitator' somewhere in the title.
⚠ Currently showing the legacy member_since value (<%= person.member_since.strftime('%b %Y') %>) because no Facilitator affiliation has a start date. Add a start date to a Facilitator affiliation to stop using the legacy value.
Used for continuing-education credit. Once a license has CE registrations it can't be removed, and only an admin can edit it.
<%= f.simple_fields_for :professional_licenses do |license_form| %> <%= render "professional_licenses/professional_license_fields", f: license_form %> @@ -388,9 +455,18 @@ partial: "professional_licenses/professional_license_fields", class: "btn btn-secondary-outline" %>