diff --git a/app/controllers/fm_archives_controller.rb b/app/controllers/fm_archives_controller.rb index 2ce09502b6..901e525cc5 100644 --- a/app/controllers/fm_archives_controller.rb +++ b/app/controllers/fm_archives_controller.rb @@ -93,6 +93,8 @@ def show return end + @table_key = @record.class.table_name.delete_prefix("fm_") + @table_label = TABLES.dig(@table_key, :label) load_backlinks load_portal_records end diff --git a/app/decorators/fm_people_to_organization_decorator.rb b/app/decorators/fm_people_to_organization_decorator.rb index c27e6237e8..775f99c0b2 100644 --- a/app/decorators/fm_people_to_organization_decorator.rb +++ b/app/decorators/fm_people_to_organization_decorator.rb @@ -5,6 +5,6 @@ def kind def summary org_id = data["OrgID"] - FmOrganization.find_by(fm_id: org_id)&.data&.dig("OrgName").presence || org_id + FmOrganization.find_by(fm_id: org_id)&.name || org_id end end diff --git a/app/decorators/fm_personnel_decorator.rb b/app/decorators/fm_personnel_decorator.rb index 769218c1e1..e88a498e76 100644 --- a/app/decorators/fm_personnel_decorator.rb +++ b/app/decorators/fm_personnel_decorator.rb @@ -13,7 +13,7 @@ def program project = FmProject.find_by(fm_id: data["ProjectID"]) return data["ProjectID"] unless project - organization = FmOrganization.find_by(fm_id: project.data["OrgID"])&.data&.dig("OrgName") - [ project.data["ProjectName"].presence || project.fm_id, organization ].compact_blank.join(" at ") + organization = FmOrganization.find_by(fm_id: project.data["OrgID"])&.name + [ project.name, organization ].compact_blank.join(" at ") end end diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb index afbdfe495f..a3135b8592 100644 --- a/app/helpers/filemaker_helper.rb +++ b/app/helpers/filemaker_helper.rb @@ -41,13 +41,31 @@ def filemaker_portal_link(record) link_to text, path, class: button_classes(:primary) end - # An archive field's link to the record it names, struck through when the archive has no such record. - def fm_archive_reference_link(fm_id, table) - key = table.delete_prefix("fm_") - model = FmArchivesController::TABLES.dig(key, :model) - return tag.span(fm_id, class: "line-through opacity-60", title: "No FM record found") if model && !model.exists?(fm_id: fm_id) + # The archive record an ID field points at, or an unsaved placeholder when the archive has none. + def fm_archive_record(fm_id, table) + model = FmArchivesController::TABLES.fetch(table.delete_prefix("fm_"))[:model] + model.find_by(fm_id: fm_id) || model.new(fm_id: fm_id, data: {}) + end + + # A record's FileMaker ID, linked to its archive page, or struck through when the archive lacks it. + def fm_archive_id_tag(record) + unless record.persisted? + return tag.span(record.fm_id, class: "ml-1 font-mono text-xs font-normal text-gray-400 line-through", title: "No FM record found") + end + + link_to record.fm_id, fm_archive_record_path(record), class: "ml-1 font-mono text-xs font-normal text-primary hover:underline" + end + + # What a record is, linked to its archive page, or a not-found note when the archive lacks it. + def fm_archive_summary_tag(record) + return tag.span("Not found in FM data", class: "text-gray-500 italic") unless record.persisted? + + summary = record.class.decorator_class? ? record.decorate.try(:summary).presence : nil + link_to summary || record.name, fm_archive_record_path(record), class: "text-primary hover:underline" + end - link_to fm_id, fm_archive_path(fm_id, table: key), class: "text-primary hover:underline" + def fm_archive_record_path(record) + fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")) end # The portal's name for a related table's counterpart, when the archive's label doesn't already lead with it. diff --git a/app/models/concerns/fm_archive.rb b/app/models/concerns/fm_archive.rb index 74176bd060..ec65065e8f 100644 --- a/app/models/concerns/fm_archive.rb +++ b/app/models/concerns/fm_archive.rb @@ -8,6 +8,8 @@ module FmArchive PORTAL_COUNTERPARTS = {}.freeze PORTAL_FIELDS = {}.freeze USER_LOOKUP_FIELDS = [].freeze + CONTEXT_LINK_MAP = {}.freeze + PREFIX_LINK_MAP = {}.freeze included do before_validation :set_defaults @@ -62,8 +64,49 @@ def records_listed_under(_label) [] end + # The archive table a field's ID points at. A polymorphic field, mapped to nil in FM_LINKS, resolves + # by the record's Context or by the ID's prefix. + def reference_table(key) + value = data[key] + return if value.blank? || !self.class::FM_LINKS.key?(key) + + self.class::FM_LINKS[key] || self.class::CONTEXT_LINK_MAP[data["Context"]] || self.class::PREFIX_LINK_MAP[value.to_s[0]] + end + + # Each table words its names its own way, so records from different tables never share one. + def name + fm_id + end + private + def fm_value(*fields) + fields.lazy.filter_map { |field| data&.dig(field).to_s.squish.presence }.first + end + + def fm_join(*fields) + fm_words(*fields.map { |field| fm_value(field) }) + end + + def fm_words(*parts) + parts.compact_blank.join(" ").presence + end + + def fm_with(preposition, value) + "#{preposition} #{value}" if value.present? + end + + def fm_dollars(*fields) + amounts = fields.filter_map { |field| Float(fm_value(field)&.delete("$,"), exception: false) } + MoneyFormatter.dollars_from_cents((amounts.sum * 100).round) if amounts.any? + end + + # "Subject — detail", where the detail carries the table's own wording. + def fm_phrase(subject, detail) + return detail&.upcase_first || fm_id if subject.blank? + [ subject, detail ].compact_blank.join(" — ") + end + def set_defaults self.data ||= {} end diff --git a/app/models/fm_activity.rb b/app/models/fm_activity.rb index a6a2ecdeba..b0bd930096 100644 --- a/app/models/fm_activity.rb +++ b/app/models/fm_activity.rb @@ -39,4 +39,8 @@ class FmActivity < ApplicationRecord "ID" => "fm_rolodexes", "LinkID" => "fm_payments" }.freeze + + def name + fm_phrase(fm_value("Subject"), fm_words(fm_value("Type") || "Activity", fm_with("with", fm_value("FullName") || fm_join("FirstName", "LastName")))) + end end diff --git a/app/models/fm_address.rb b/app/models/fm_address.rb index e02402ac47..5fe6eab75c 100644 --- a/app/models/fm_address.rb +++ b/app/models/fm_address.rb @@ -30,4 +30,8 @@ class FmAddress < ApplicationRecord "RolodexID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + [ fm_value("AddrsLine1"), fm_value("AddrsCity") ].compact.join(", ").presence || fm_id + end end diff --git a/app/models/fm_allocation.rb b/app/models/fm_allocation.rb index bd3b5ea93e..f5c7a53677 100644 --- a/app/models/fm_allocation.rb +++ b/app/models/fm_allocation.rb @@ -31,4 +31,8 @@ class FmAllocation < ApplicationRecord HAS_MANY = { "fm_participants" => { via: "AllocRecID", label: "Participants" } }.freeze + + def name + fm_words(fm_value("Year"), "allocation", fm_with("of", fm_dollars("AmountGeneral", "AmountSupplies", "AmountExhibit")), fm_with("to", fm_value("ProjectID"))).upcase_first + end end diff --git a/app/models/fm_ancillary_data.rb b/app/models/fm_ancillary_data.rb index 54116ae4c2..dc2d3f2361 100644 --- a/app/models/fm_ancillary_data.rb +++ b/app/models/fm_ancillary_data.rb @@ -20,4 +20,8 @@ class FmAncillaryData < ApplicationRecord FM_LINKS = { "RolodexID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("FlagText"), fm_words(fm_value("Flag"), "flag")) + end end diff --git a/app/models/fm_art_purchase.rb b/app/models/fm_art_purchase.rb index 175a210f6b..eb7069fc99 100644 --- a/app/models/fm_art_purchase.rb +++ b/app/models/fm_art_purchase.rb @@ -22,4 +22,8 @@ class FmArtPurchase < ApplicationRecord "ArtRegistryID" => "fm_art_registries", "BuyerID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("ArtTitle"), fm_words("purchased", fm_with("for", fm_dollars("AmountPaid")))) + end end diff --git a/app/models/fm_art_registry.rb b/app/models/fm_art_registry.rb index f2a57beee6..89c7fe13c9 100644 --- a/app/models/fm_art_registry.rb +++ b/app/models/fm_art_registry.rb @@ -53,4 +53,8 @@ class FmArtRegistry < ApplicationRecord "fm_art_purchases" => { via: "ArtRegistryID", label: "Art Purchases" }, "fm_exhibited_items" => { via: "ArtRegistryItemID", label: "Exhibited Items" } }.freeze + + def name + fm_phrase(fm_value("ArtTitle"), fm_with("by", fm_join("ArtistFirstName", "ArtistLastName"))) + end end diff --git a/app/models/fm_campaign.rb b/app/models/fm_campaign.rb index bbe0d5b0ee..ee141f8581 100644 --- a/app/models/fm_campaign.rb +++ b/app/models/fm_campaign.rb @@ -26,4 +26,8 @@ class FmCampaign < ApplicationRecord "fm_donor_developers" => { via: "CampaignID", label: "Donor Developers" }, "fm_solicitations" => { via: "CampaignID", label: "Solicitations" } }.freeze + + def name + fm_value("Campaign") || fm_id + end end diff --git a/app/models/fm_committee.rb b/app/models/fm_committee.rb index c9f4b3f7f6..933bea1067 100644 --- a/app/models/fm_committee.rb +++ b/app/models/fm_committee.rb @@ -24,4 +24,8 @@ class FmCommittee < ApplicationRecord "fm_committee_meetings" => { via: "CommitteeID", label: "Meetings" }, "fm_committee_members" => { via: "CommitteeID", label: "Members" } }.freeze + + def name + fm_value("CommitteeName") || fm_id + end end diff --git a/app/models/fm_committee_meeting.rb b/app/models/fm_committee_meeting.rb index e15490d317..159a86febe 100644 --- a/app/models/fm_committee_meeting.rb +++ b/app/models/fm_committee_meeting.rb @@ -18,4 +18,8 @@ class FmCommitteeMeeting < ApplicationRecord FM_LINKS = { "CommitteeID" => "fm_committees" }.freeze + + def name + fm_words("Committee", fm_value("CommitteeID"), "meeting", fm_with("on", fm_value("MeetingDate"))) + end end diff --git a/app/models/fm_committee_member.rb b/app/models/fm_committee_member.rb index cdeb129425..d967b914d7 100644 --- a/app/models/fm_committee_member.rb +++ b/app/models/fm_committee_member.rb @@ -22,4 +22,8 @@ class FmCommitteeMember < ApplicationRecord "CommitteeID" => "fm_committees", "MemberID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("MemberID"), fm_words(fm_value("Role") || "Member", "of committee", fm_value("CommitteeID"))) + end end diff --git a/app/models/fm_contact.rb b/app/models/fm_contact.rb index 866cdc7525..4d359dd1b9 100644 --- a/app/models/fm_contact.rb +++ b/app/models/fm_contact.rb @@ -21,4 +21,8 @@ class FmContact < ApplicationRecord FM_LINKS = { "ContacteeID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("RecordID"), fm_words("contact", fm_with("for", fm_value("ContacteeID")))) + end end diff --git a/app/models/fm_donor_developer.rb b/app/models/fm_donor_developer.rb index dfcaf92c40..20a83a9578 100644 --- a/app/models/fm_donor_developer.rb +++ b/app/models/fm_donor_developer.rb @@ -26,4 +26,8 @@ class FmDonorDeveloper < ApplicationRecord HAS_MANY = { "fm_solicitations" => { via: "DonorDevRecID", label: "Solicitations" } }.freeze + + def name + fm_phrase(fm_value("NameListing"), fm_words("donor developer", fm_with("for", fm_value("CampaignID")))) + end end diff --git a/app/models/fm_email_address.rb b/app/models/fm_email_address.rb index 9b8badf616..913d4447e7 100644 --- a/app/models/fm_email_address.rb +++ b/app/models/fm_email_address.rb @@ -18,4 +18,8 @@ class FmEmailAddress < ApplicationRecord FM_LINKS = { "OrgID" => "fm_organizations" }.freeze + + def name + fm_value("EmailAddress") || fm_id + end end diff --git a/app/models/fm_event.rb b/app/models/fm_event.rb index 149a04a8a2..d6241c60d5 100644 --- a/app/models/fm_event.rb +++ b/app/models/fm_event.rb @@ -47,4 +47,8 @@ class FmEvent < ApplicationRecord "fm_volunteers" => { via: "EventID", label: "Volunteers" }, "fm_quotations" => { via: "EventID", label: "Quotations" } }.freeze + + def name + fm_value("Title") || fm_id + end end diff --git a/app/models/fm_exhibited_item.rb b/app/models/fm_exhibited_item.rb index d8daa3b492..18c5ff5657 100644 --- a/app/models/fm_exhibited_item.rb +++ b/app/models/fm_exhibited_item.rb @@ -23,4 +23,8 @@ class FmExhibitedItem < ApplicationRecord "ArtRegistryItemID" => "fm_art_registries", "EventID" => "fm_events" }.freeze + + def name + fm_phrase(fm_value("ItemName"), fm_words("exhibited", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_expenditure.rb b/app/models/fm_expenditure.rb index 752ac8357c..07d45510d1 100644 --- a/app/models/fm_expenditure.rb +++ b/app/models/fm_expenditure.rb @@ -23,4 +23,8 @@ class FmExpenditure < ApplicationRecord FM_LINKS = { "ProjectID" => "fm_projects" }.freeze + + def name + fm_phrase(fm_value("Name", "Description"), fm_words(fm_dollars("Amount"), "expense")) + end end diff --git a/app/models/fm_form_field.rb b/app/models/fm_form_field.rb index 81518402b1..e49221b840 100644 --- a/app/models/fm_form_field.rb +++ b/app/models/fm_form_field.rb @@ -18,4 +18,8 @@ class FmFormField < ApplicationRecord FM_KEY_COLUMN = "FieldNo" FM_LINKS = {}.freeze + + def name + fm_words(fm_value("FieldNameFM", "FieldNameWP"), "form field").upcase_first + end end diff --git a/app/models/fm_form_letter.rb b/app/models/fm_form_letter.rb index 5f3657e7b3..5866a37048 100644 --- a/app/models/fm_form_letter.rb +++ b/app/models/fm_form_letter.rb @@ -28,4 +28,8 @@ class FmFormLetter < ApplicationRecord FM_LINKS = { "SignatureID" => "fm_rolodexes" }.freeze + + def name + fm_value("DocumentName") || fm_id + end end diff --git a/app/models/fm_form_submission.rb b/app/models/fm_form_submission.rb index 754bd17714..cbfd9a2490 100644 --- a/app/models/fm_form_submission.rb +++ b/app/models/fm_form_submission.rb @@ -54,4 +54,8 @@ class FmFormSubmission < ApplicationRecord "PaymentID" => "fm_payments", "ParticRecID" => "fm_participants" }.freeze + + def name + fm_phrase(fm_join("FirstName", "LastName") || fm_value("OrgName"), fm_value("FormTitle") || "form submission") + end end diff --git a/app/models/fm_forms_list.rb b/app/models/fm_forms_list.rb index 4c60716c94..ad4f5230a4 100644 --- a/app/models/fm_forms_list.rb +++ b/app/models/fm_forms_list.rb @@ -21,4 +21,8 @@ class FmFormsList < ApplicationRecord HAS_MANY = { "fm_form_submissions" => { via: "FormID", label: "Form Submissions" } }.freeze + + def name + fm_value("FormTitle") || fm_id + end end diff --git a/app/models/fm_funding.rb b/app/models/fm_funding.rb index d384fc43a0..b066d42937 100644 --- a/app/models/fm_funding.rb +++ b/app/models/fm_funding.rb @@ -60,4 +60,10 @@ class FmFunding < ApplicationRecord "fm_match_donations" => { via: "FundingID", label: "Match Donations" }, "fm_reportings" => { via: "FundingRecordID", label: "Reporting Obligations" } }.freeze + + def name + granted = fm_dollars("AmountGranted") + detail = granted ? "#{granted} grant" : fm_words(fm_dollars("RequestAmount"), "funding request") + fm_phrase(fm_value("NameTemp", "Purpose"), detail) + end end diff --git a/app/models/fm_group.rb b/app/models/fm_group.rb index c0e9cd3497..e652dd9c29 100644 --- a/app/models/fm_group.rb +++ b/app/models/fm_group.rb @@ -30,4 +30,8 @@ class FmGroup < ApplicationRecord HAS_MANY = { "fm_group_members" => { via: "GroupID", label: "Members" } }.freeze + + def name + fm_value("GroupName") || fm_id + end end diff --git a/app/models/fm_group_member.rb b/app/models/fm_group_member.rb index 1f5e36a1dd..d96ae4e91f 100644 --- a/app/models/fm_group_member.rb +++ b/app/models/fm_group_member.rb @@ -23,4 +23,8 @@ class FmGroupMember < ApplicationRecord "GroupID" => "fm_groups", "ContactID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("ContactID"), fm_words("member of group", fm_value("GroupID"))) + end end diff --git a/app/models/fm_invoice.rb b/app/models/fm_invoice.rb index 08c3d450af..e94aa4124f 100644 --- a/app/models/fm_invoice.rb +++ b/app/models/fm_invoice.rb @@ -44,4 +44,8 @@ class FmInvoice < ApplicationRecord HAS_MANY = { "fm_line_items" => { via: "InvoiceNo", label: "Line Items" } }.freeze + + def name + fm_phrase(fm_words("Invoice", fm_value("InvoiceNo")), fm_value("Name", "Company")) + end end diff --git a/app/models/fm_line_item.rb b/app/models/fm_line_item.rb index 7332475822..0a1f85ab4c 100644 --- a/app/models/fm_line_item.rb +++ b/app/models/fm_line_item.rb @@ -24,4 +24,8 @@ class FmLineItem < ApplicationRecord FM_LINKS = { "InvoiceNo" => "fm_invoices" }.freeze + + def name + fm_phrase(fm_value("Description"), fm_words("line item", fm_with("on invoice", fm_value("InvoiceNo")))) + end end diff --git a/app/models/fm_match_donation.rb b/app/models/fm_match_donation.rb index 9195121972..c5b4baf86b 100644 --- a/app/models/fm_match_donation.rb +++ b/app/models/fm_match_donation.rb @@ -19,4 +19,8 @@ class FmMatchDonation < ApplicationRecord "FundingID" => "fm_fundings", "RefDonationID" => "fm_payments" }.freeze + + def name + fm_words(fm_value("Match"), "match", fm_with("of", fm_dollars("Amount")), fm_with("for", fm_value("DonationID"))).upcase_first + end end diff --git a/app/models/fm_note.rb b/app/models/fm_note.rb index 59b2593b5b..60b0c05843 100644 --- a/app/models/fm_note.rb +++ b/app/models/fm_note.rb @@ -37,4 +37,8 @@ class FmNote < ApplicationRecord "Organizations" => "fm_organizations", "Rolodex" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("Subject"), fm_words(fm_value("Context")&.singularize, "note")) + end end diff --git a/app/models/fm_organization.rb b/app/models/fm_organization.rb index 3cb475be96..bc76b9eb5a 100644 --- a/app/models/fm_organization.rb +++ b/app/models/fm_organization.rb @@ -51,4 +51,8 @@ class FmOrganization < ApplicationRecord "fm_invoices" => { via: "OrgID", label: "Invoices" }, "fm_people_to_organizations" => { via: "OrgID", label: "People Affiliations" } }.freeze + + def name + fm_value("OrgName") || fm_id + end end diff --git a/app/models/fm_participant.rb b/app/models/fm_participant.rb index de059807e9..cbcb983b07 100644 --- a/app/models/fm_participant.rb +++ b/app/models/fm_participant.rb @@ -59,4 +59,8 @@ class FmParticipant < ApplicationRecord HAS_MANY = { "fm_payments" => { via: "ParticRecID", label: "Payments" } }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Role") || "Participant", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_payment.rb b/app/models/fm_payment.rb index 8afb482abe..ac738c2584 100644 --- a/app/models/fm_payment.rb +++ b/app/models/fm_payment.rb @@ -45,10 +45,10 @@ class FmPayment < ApplicationRecord "OrgID" => "fm_organizations", "ProjectID" => "fm_projects", "ParticRecID" => "fm_participants", - "RefID" => nil # polymorphic — resolved by prefix via REFID_PREFIX_MAP + "RefID" => nil # polymorphic — resolved by prefix via PREFIX_LINK_MAP }.freeze - REFID_PREFIX_MAP = { + PREFIX_LINK_MAP = { "E" => "fm_events", "F" => "fm_fundings", "e" => "fm_events" @@ -60,4 +60,8 @@ class FmPayment < ApplicationRecord "fm_match_donations" => { via: "RefDonationID", label: "Match Donations" }, "fm_form_submissions" => { via: "PaymentID", label: "Form Submissions" } }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Type") || "Payment", fm_with("of", fm_dollars("Amount")))) + end end diff --git a/app/models/fm_people_to_organization.rb b/app/models/fm_people_to_organization.rb index 5d67cd7ef8..e228bb82a9 100644 --- a/app/models/fm_people_to_organization.rb +++ b/app/models/fm_people_to_organization.rb @@ -21,4 +21,8 @@ class FmPeopleToOrganization < ApplicationRecord "PersonID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + fm_phrase(fm_value("PersonID"), fm_words(fm_value("Title") || "Affiliated", fm_with("at", fm_value("OrgID")))) + end end diff --git a/app/models/fm_personnel.rb b/app/models/fm_personnel.rb index 6d0d3b108c..a2feccdc90 100644 --- a/app/models/fm_personnel.rb +++ b/app/models/fm_personnel.rb @@ -28,4 +28,8 @@ class FmPersonnel < ApplicationRecord "PersonID" => "fm_rolodexes", "ProjectID" => "fm_projects" }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Role") || "Personnel", fm_with("on", fm_value("ProjectID")))) + end end diff --git a/app/models/fm_phone_number.rb b/app/models/fm_phone_number.rb index 9681a34a1f..85c495833e 100644 --- a/app/models/fm_phone_number.rb +++ b/app/models/fm_phone_number.rb @@ -23,4 +23,8 @@ class FmPhoneNumber < ApplicationRecord "RolodexID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + fm_value("PhoneNumber") || fm_id + end end diff --git a/app/models/fm_postal_code.rb b/app/models/fm_postal_code.rb index a64d6b8f54..e062381093 100644 --- a/app/models/fm_postal_code.rb +++ b/app/models/fm_postal_code.rb @@ -24,4 +24,8 @@ class FmPostalCode < ApplicationRecord FM_KEY_COLUMN = "Zipcode" FM_LINKS = {}.freeze + + def name + fm_words([ fm_value("City"), fm_value("State") ].compact.join(", "), fm_id) + end end diff --git a/app/models/fm_program_sponsorship.rb b/app/models/fm_program_sponsorship.rb index 7368850a6b..0a33840f25 100644 --- a/app/models/fm_program_sponsorship.rb +++ b/app/models/fm_program_sponsorship.rb @@ -28,4 +28,8 @@ class FmProgramSponsorship < ApplicationRecord "FundingID" => "fm_fundings", "ProgramID" => "fm_projects" }.freeze + + def name + fm_words("Sponsorship", fm_with("of", fm_value("ProgramID")), fm_with("by", fm_value("FunderID"))) + end end diff --git a/app/models/fm_project.rb b/app/models/fm_project.rb index f649a7ad48..e3b879eca0 100644 --- a/app/models/fm_project.rb +++ b/app/models/fm_project.rb @@ -68,4 +68,8 @@ class FmProject < ApplicationRecord "fm_form_submissions" => { via: "ProjectID", label: "Form Submissions" }, "fm_quotations" => { via: "ProjectID", label: "Quotations" } }.freeze + + def name + fm_value("ProjectName") || fm_id + end end diff --git a/app/models/fm_quotation.rb b/app/models/fm_quotation.rb index 33a27f6a66..d75295c840 100644 --- a/app/models/fm_quotation.rb +++ b/app/models/fm_quotation.rb @@ -41,4 +41,9 @@ class FmQuotation < ApplicationRecord "ProjectID" => "fm_projects", "SourceID" => "fm_rolodexes" }.freeze + + def name + quote = fm_value("Quotation")&.truncate(60) + fm_phrase(quote && "“#{quote}”", fm_words("quote", fm_with("from", fm_value("PersonName")))) + end end diff --git a/app/models/fm_reporting.rb b/app/models/fm_reporting.rb index 234f6248d0..e2b37850ac 100644 --- a/app/models/fm_reporting.rb +++ b/app/models/fm_reporting.rb @@ -18,4 +18,8 @@ class FmReporting < ApplicationRecord FM_LINKS = { "FundingRecordID" => "fm_fundings" }.freeze + + def name + fm_phrase(fm_value("ReportDescription"), fm_words(fm_with("due", fm_value("ReportDueDate")), fm_with("for", fm_value("FundingRecordID"))) || "funding report") + end end diff --git a/app/models/fm_rolodex.rb b/app/models/fm_rolodex.rb index fb37537941..07edb40b08 100644 --- a/app/models/fm_rolodex.rb +++ b/app/models/fm_rolodex.rb @@ -198,6 +198,11 @@ class FmRolodex < ApplicationRecord "fm_ancillary_data" => { via: "RolodexID", label: "Ancillary Data" } }.freeze + def name + agency = fm_value("Organization") + fm_join("FirstName", "LastName") || (agency && "#{agency} (agency)") || fm_id + end + def phone_numbers @phone_numbers ||= contact_records(FmPhoneNumber, primary_ids: data.values_at("PrimaryPhoneID")) end diff --git a/app/models/fm_service.rb b/app/models/fm_service.rb index 7456bf23e6..78ae833a0b 100644 --- a/app/models/fm_service.rb +++ b/app/models/fm_service.rb @@ -28,4 +28,8 @@ class FmService < ApplicationRecord FM_LINKS = { "ProjectID" => "fm_projects" }.freeze + + def name + fm_words(fm_value("Month"), fm_value("Year"), "service", fm_with("at", fm_value("ProjectID"))).upcase_first + end end diff --git a/app/models/fm_solicitation.rb b/app/models/fm_solicitation.rb index 7e1f26e733..1bfb6a5cfc 100644 --- a/app/models/fm_solicitation.rb +++ b/app/models/fm_solicitation.rb @@ -36,4 +36,8 @@ class FmSolicitation < ApplicationRecord "DonorDevRecID" => "fm_donor_developers", "DonorID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("NameListing"), fm_words("solicitation", fm_with("for", fm_value("CampaignID")))) + end end diff --git a/app/models/fm_volunteer.rb b/app/models/fm_volunteer.rb index 324d5406e6..b55362f524 100644 --- a/app/models/fm_volunteer.rb +++ b/app/models/fm_volunteer.rb @@ -27,4 +27,8 @@ class FmVolunteer < ApplicationRecord "VRecID" => "fm_rolodexes", "EventID" => "fm_events" }.freeze + + def name + fm_phrase(fm_join("FirstName", "LastName"), fm_words(fm_value("Role"), "volunteer", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_workshop.rb b/app/models/fm_workshop.rb index 69d0b91c7c..0ead84f291 100644 --- a/app/models/fm_workshop.rb +++ b/app/models/fm_workshop.rb @@ -48,4 +48,8 @@ class FmWorkshop < ApplicationRecord FM_LINKS = { "AuthorID" => "fm_rolodexes" }.freeze + + def name + fm_value("WorkshopTitle") || fm_id + end end diff --git a/app/models/fm_workshop_log.rb b/app/models/fm_workshop_log.rb index 0788dc8779..f97066d2f9 100644 --- a/app/models/fm_workshop_log.rb +++ b/app/models/fm_workshop_log.rb @@ -45,4 +45,8 @@ class FmWorkshopLog < ApplicationRecord "LeaderID" => "fm_rolodexes", "AgencyID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("WorkshopTitle"), fm_words("report", fm_with("by", fm_value("LeaderName")), fm_with("on", fm_value("WorkshopDate")))) + end end diff --git a/app/views/fm_archives/_fields.html.erb b/app/views/fm_archives/_fields.html.erb index b49f4b53e1..139d945a07 100644 --- a/app/views/fm_archives/_fields.html.erb +++ b/app/views/fm_archives/_fields.html.erb @@ -1,18 +1,16 @@
<% fields.each do |key, value| %> + <% table = record.reference_table(key) %> + <% target = fm_archive_record(value, table) if table %>
-
<%= key %>
-
- <% link_target = record.class::FM_LINKS[key] %> - <% if link_target && value.present? %> - <%= fm_archive_reference_link(value, link_target) %> - <% elsif link_target.nil? && value.present? && record.class.const_defined?(:CONTEXT_LINK_MAP) && (ctx = record.data["Context"]) && (ctx_target = record.class::CONTEXT_LINK_MAP[ctx]) %> - <%= fm_archive_reference_link(value, ctx_target) %> - <% elsif link_target.nil? && value.present? && record.class.const_defined?(:PREFIX_LINK_MAP) && (prefix_target = record.class::PREFIX_LINK_MAP[value.to_s[0]]) %> - <%= fm_archive_reference_link(value, prefix_target) %> - <% else %> - <%= value %> +
+ <%= key %> + <% if target %> + <%= fm_archive_id_tag(target) %> <% end %> +
+
+ <%= target ? fm_archive_summary_tag(target) : value %>
<% end %> diff --git a/app/views/fm_archives/_listed_records.html.erb b/app/views/fm_archives/_listed_records.html.erb index 80652052ea..eeb8f79050 100644 --- a/app/views/fm_archives/_listed_records.html.erb +++ b/app/views/fm_archives/_listed_records.html.erb @@ -4,24 +4,13 @@
  • <%= record.decorate.kind %> - <% if record.persisted? %> - <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")), - class: "ml-1 font-mono text-xs font-normal text-primary hover:underline" %> - <% else %> - <%= record.fm_id %> - <% end %> + <%= fm_archive_id_tag(record) %> <% owner.decorate.contact_tags(record).each do |tag| %> <%= tag %> <% end %> - <% if record.persisted? %> - <%= link_to record.decorate.summary.presence || record.fm_id, - fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")), - class: "text-primary hover:underline" %> - <% else %> - Not found in FM data - <% end %> + <%= fm_archive_summary_tag(record) %>
  • <% end %> diff --git a/app/views/fm_archives/index.html.erb b/app/views/fm_archives/index.html.erb index 9ba54ddf41..4e7b28c14b 100644 --- a/app/views/fm_archives/index.html.erb +++ b/app/views/fm_archives/index.html.erb @@ -29,7 +29,7 @@ FM ID - Key + Name @@ -38,7 +38,7 @@ <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: @selected), class: "text-primary hover:underline" %> - <%= record.fm_key_name %> + <%= truncate(record.name, length: 100) %> <% end %> diff --git a/app/views/fm_archives/show.html.erb b/app/views/fm_archives/show.html.erb index 9cb4bd018a..7970017f47 100644 --- a/app/views/fm_archives/show.html.erb +++ b/app/views/fm_archives/show.html.erb @@ -3,10 +3,10 @@
    -

    <%= @record.class.name %>

    -

    FM ID: <%= @record.fm_id %> · Key: <%= @record.fm_key_name %>

    +

    <%= @record.name %>

    +

    <%= @table_label %> · FM ID: <%= @record.fm_id %> · Key: <%= @record.fm_key_name %>

    - <%= link_to "Back to list", fm_archives_path(table: @record.class.table_name.sub("fm_", "")), class: button_classes(:primary_outline) %> + <%= link_to "Back to list", fm_archives_path(table: @table_key), class: button_classes(:primary_outline) %>
    <% if @portal_records.any? %> @@ -65,9 +65,14 @@ <% end %>
    <% info[:records].each do |record| %> - <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: info[:table].sub("fm_", "")), - class: "text-xs font-mono text-primary hover:underline bg-gray-50 px-2 py-1 rounded", - data: { turbo_frame: "_top" } %> + <%= link_to fm_archive_path(record.fm_id, table: info[:table].sub("fm_", "")), + class: "text-xs text-primary hover:underline bg-gray-50 px-2 py-1 rounded", + title: record.name, data: { turbo_frame: "_top" } do %> + <%= record.fm_id %> + <% if record.name != record.fm_id %> + <%= truncate(record.name, length: 40) %> + <% end %> + <% end %> <% end %>
    <% if info[:records].total_pages > 1 %> diff --git a/config/features.yml b/config/features.yml index 7b479ba5b3..4449d15688 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4304,3 +4304,18 @@ - "Wide images still fill their space edge to edge, as before." - "This covers the carousels, the story cards on the home and browse pages, the related stories on each story page, and the image carousel on each story." - "Cards in the home page topic sections now all stay the same size, whatever the shape of the image." + +- name: "FileMaker archive records show their names" + area: reporting + display_status: admin_facing + released_on: 2026-10-12 + pr_number: 2664 + action_path: "/fm_archives" + summary: >- + FileMaker archive lists and record pages now show each record's name — a + person, organization, event title, or similar — instead of only its ID. + pro_tips: + - "Each table words its names its own way, so a person's payment, registration, and volunteer shift never look alike, and an art sale reads like \"After the Rain — purchased for $250\"." + - "Link rows with no name of their own, like committee members, name the FileMaker IDs they connect." + - "An ID field that points at another archive record, like a rolodex OrgID, reads like the phone and address rows: the ID sits beside the field name and the record's name links to it. A struck-through ID means the archive has no such record." + - "A payment's RefID now links to the event or funding it was for." diff --git a/spec/helpers/filemaker_helper_spec.rb b/spec/helpers/filemaker_helper_spec.rb index 68977695d8..970713661e 100644 --- a/spec/helpers/filemaker_helper_spec.rb +++ b/spec/helpers/filemaker_helper_spec.rb @@ -84,22 +84,53 @@ end end - describe "#fm_archive_reference_link" do - it "links an ID the archive holds to its record" do - FmAddress.create!(fm_id: "74001") + describe "#fm_archive_record" do + it "finds the record an ID points at" do + organization = FmOrganization.create!(fm_id: "ORG1") - expect(helper.fm_archive_reference_link("74001", "fm_addresses")) - .to include(%(href="#{fm_archive_path("74001", table: "addresses")}")) + expect(helper.fm_archive_record("ORG1", "fm_organizations")).to eq(organization) + end + + it "stands in an unsaved placeholder for an ID the archive lacks" do + record = helper.fm_archive_record("ORG9", "fm_organizations") + + expect([ record.class, record.fm_id, record.persisted? ]).to eq([ FmOrganization, "ORG9", false ]) + end + end + + describe "#fm_archive_id_tag" do + it "links a record's ID to its archive page" do + html = helper.fm_archive_id_tag(FmAddress.create!(fm_id: "74001")) + + expect(html).to match(%r{]*href="#{Regexp.escape(fm_archive_path("74001", table: "addresses"))}"[^>]*>74001}) end it "strikes through an ID the archive has no record for" do - html = helper.fm_archive_reference_link("74999", "fm_addresses") + html = helper.fm_archive_id_tag(FmAddress.new(fm_id: "74999")) expect(html).to include("line-through", %(title="No FM record found"), "74999") expect(html).not_to include("href") end end + describe "#fm_archive_summary_tag" do + it "links a record by its decorator's summary" do + address = FmAddress.create!(fm_id: "74001", data: { "AddrsLine1" => "88 Rose Ave", "AddrsCity" => "Venice" }) + + expect(helper.fm_archive_summary_tag(address)).to include(">88 Rose Ave, Venice") + end + + it "links a record without a decorator by its name" do + organization = FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + + expect(helper.fm_archive_summary_tag(organization)).to include(">Sample Shelter") + end + + it "notes a record the archive lacks as not found" do + expect(helper.fm_archive_summary_tag(FmOrganization.new(fm_id: "ORG9"))).to include("Not found in FM data") + end + end + describe "#fm_portal_counterpart_name" do let(:rolodex) { FmRolodex.new } diff --git a/spec/models/concerns/fm_archive_spec.rb b/spec/models/concerns/fm_archive_spec.rb new file mode 100644 index 0000000000..404bd9ed85 --- /dev/null +++ b/spec/models/concerns/fm_archive_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" + +RSpec.describe FmArchive do + describe "#name" do + it "names a person by their full name" do + rolodex = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "Archie", "LastName" => " Coded " }) + + expect(rolodex.name).to eq("Archie Coded") + end + + it "names an agency rolodex record by its organization" do + agency = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "", "Organization" => "Sunrise Shelter" }) + + expect(agency.name).to eq("Sunrise Shelter (agency)") + end + + it "tells an art purchase apart from the artwork by its price" do + purchase = FmArtPurchase.new(fm_id: "1", data: { "ArtTitle" => "After the Rain", "AmountPaid" => "$1,250.00" }) + artwork = FmArtRegistry.new(fm_id: "2", data: { "ArtTitle" => "After the Rain" }) + + expect(purchase.name).to eq("After the Rain — purchased for $1,250") + expect(artwork.name).to eq("After the Rain") + end + + it "words the detail alone when the subject is blank" do + expect(FmArtPurchase.new(fm_id: "1", data: { "AmountPaid" => "90" }).name).to eq("Purchased for $90") + end + + it "falls back to the FM ID when nothing names the record" do + expect(FmRolodex.new(fm_id: "00006", data: {}).name).to eq("00006") + end + + it "gives every seeded archive record its own name, though the seed reuses people and art across tables" do + expect { load Rails.root.join("db/seeds/dev/fm_archive.rb") }.to output.to_stdout + + names = FmArchivesController::TABLES.values.flat_map { |config| config[:model].all.map(&:name) } + + expect(names.tally.select { |_, count| count > 1 }.keys).to be_empty + end + end +end diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index 5a7f5ba4af..7107a9d748 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -15,6 +15,14 @@ expect(response.body).to include("Funding (1)", "table=fundings") end + + it "names each related record beside its ID" do + funding.update!(data: funding.data.merge("Purpose" => "Art supplies", "AmountGranted" => "500")) + + get fm_archive_path(rolodex.fm_id, table: "rolodexes") + + expect(response.body).to match(%r{>FND1\s*]*>Art supplies — \$500 grant}) + end end it "shows a rolodex record's fields under group headings, most important first" do @@ -162,6 +170,52 @@ expect(response.body).not_to include("Available fields") end + it "links a note's LinkID by its context, leaving its other fields unlinked" do + FmProject.create!(fm_id: "P1", data: { "ProjectName" => "Adult Windows" }) + FmNote.create!(fm_id: "N1", data: { "Context" => "Project", "LinkID" => "P1", "Subject" => "Site visit" }) + + get fm_archive_path("N1", table: "notes") + + expect(response.body).to include(fm_archive_path("P1", table: "projects")) + expect(response.body).not_to include("Site visit", "line-through") + end + + it "links a payment's RefID to the table its prefix names" do + FmEvent.create!(fm_id: "E1", data: { "Title" => "Spring Art Auction" }) + FmPayment.create!(fm_id: "D1", data: { "RefID" => "E1" }) + + get fm_archive_path("D1", table: "payments") + + expect(response.body).to include(fm_archive_path("E1", table: "events")) + end + + it "shows a linked field's ID beside its label and the record's name as the link" do + FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + FmProject.create!(fm_id: "P1", data: { "OrgID" => "ORG1" }) + + get fm_archive_path("P1", table: "projects") + + path = Regexp.escape(fm_archive_path("ORG1", table: "organizations")) + expect(response.body).to match(%r{OrgID\s*]*href="#{path}"[^>]*>ORG1\s*\s*]*>\s*]*href="#{path}"[^>]*>Sample Shelter}) + end + + it "marks a linked field whose record the archive lacks as not found" do + FmProject.create!(fm_id: "P1", data: { "OrgID" => "ORG9" }) + + get fm_archive_path("P1", table: "projects") + + expect(response.body).to match(%r{OrgID\s*]*line-through[^>]*>ORG9.*Not found in FM data}m) + end + + it "heads the page with the record's name and table" do + FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sunrise Shelter" }) + + get fm_archive_path("ORG1", table: "organizations") + + expect(response.body).to match(%r{]*>Sunrise Shelter}) + expect(response.body).to include("Organizations · FM ID: ORG1") + end + it "links back to the portal people carrying the archive code" do FmRolodex.create!(fm_id: "00006") person = create(:person, first_name: "Archie", last_name: "Coded", filemaker_code: "00012, 00006") @@ -291,6 +345,14 @@ expect(response.body).to include("People to organizations") end + it "lists each record by name" do + FmRolodex.create!(fm_id: "ROL1", data: { "FirstName" => "Archie", "LastName" => "Coded" }) + + get fm_archives_path(table: "rolodexes") + + expect(response.body).to include("Archie Coded") + end + it "marks the selected table as the current page" do get fm_archives_path(table: "rolodexes")