Releases: BeamLabEU/phoenix_kit_crm
Release list
0.10.0 - 2026-09-02
Added
- Company-anchored interactions — schema V05 (#32). An interaction now
anchors to exactly one record:contact_uuidXORcompany_uuid, both hard
FKs, pinned by a DB CHECK (num_nonnulls(...) = 1) addedNOT VALIDand
validated separately. "Anchor" is the deliberate word —subjectis the
title column. The anchor is immutable after create:
Interaction.changeset/2is now create-only (the only changeset that casts
anchor fields) andInteraction.update_changeset/2is its edit twin, so an
edit can never move a row between feeds, activity ownership and broadcasts.
No data migration — every existing row has a contact and no company. - A dual-anchor interactions component (#32).
InteractionsComponent
serves the contact page and the company page from one module;
CompanyInteractionsComponent(a read-only member rollup) is removed. In
company mode it composes company-anchored interactions and merges the
members' own behind anAll | Company | Peoplefilter, with
Interactions.list_for_company/2combining both arms in one query so
ordering and:limitapply to the merged window. crm:company:<uuid>:interactions(#32) — a company interaction-feed
PubSub topic, the symmetric twin of the contact one, deliberately separate
fromtopic_company/1(which signals record/roster changes and triggers
heavier reloads than a feed refresh needs).- Search-independent counts on every index tab (#32).
Contacts.status_counts/0,Companies.status_counts/0and
PartyRoles.role_counts/1each answer in one grouped query, zeros included. Companies.list_companies/1:without_contacts(#32) — companies with
no membership to a live contact, using the roster's own visibility rule, so
a company listed there really does have an empty Members tab.Interactions.list_recent/1(#32) — the newest interactions across the
CRM with the anchor preloaded, excluding rows whose anchor is trashed.- Role-access changes are logged (#32).
RoleSettings.set_enabled/3takes
:actor_uuidand writescrm.role_access_enabled/crm.role_access_disabled
— this grants or revokes CRM access for everyone holding the role and was the
module's only unlogged mutation.
Changed
- The overview is a front door, not a count strip (#32). Companies and
contacts hero cards, a by-role band whose counts deep-link the pre-filtered
index, a conditional needs-attention row (companies with no live contacts →
?filter=no-contacts), the newest interactions, and a demoted Lists row.
Portal access moved to the CRM settings page, where each enabled role now
shows its user count and a link into its portal view. - Honest filter strips on both index pages (#32). The default tab is now
All — everything not trashed, exactly the scope it always was but no
longer mislabelled "Active". The Active/Inactive pair appears only once
inactive records exist, role and status tabs render only with results (or
while active), every label carries its count, the toolbar count names what
the filter shows, and a tab click resets search. - The company page starts at its tabs (#32). The in-body header band is
gone — the name lives in the layout header, Edit rides the layout's
page_actionchip, and the identity block (logo, status, role badges) opens
the Overview tab. - The project Client tab reads
list_for_company/2(#32), so "recent
interactions with this client" now includes the company's own, not only its
members'. tz_offset/1,format_local/2,current_user_uuid/1and
current_user_name/1are single definitions inInteractionHelpers(#32) —
they were verbatim copies across the show LiveViews and the overview, so an
interaction now shows the same time on every page that renders one.
Fixed
- A hard-deleted parent's interactions vanished without cleanup (#32). The
FK CASCADE bypassesdelete_interaction/2, the only path that purges an
interaction's media folder and broadcasts its deletion. Both delete contexts
now collect the doomed rows inside their transaction and hand them to
Interactions.cleanup_cascaded/1after the commit. - Trashing a company left open contact feeds stale (#32). A trashed
company's anchored rows are hidden from every involving feed;
notify_company_visibility/1now tells each affected party contact's page so
it stops showing them without a reload. Restore does the mirror. - One unknown extension cost every attachment its upload (#32).
allow_uploadrefuses any:acceptextension the mime library cannot name,
and the raise landed in a silent rescue that disabled the dropzone entirely
(.m4a/.ogg/.mkvare unknown to mime 2.0.7). The accept list is
filtered throughMIME.has_type?/1, the rescue now logs, and a rejected
upload entry shows why instead of sitting at a frozen progress bar. - A dropped attachment left no trace (#32).
attach_files/3swallowed a
missing-folder failure after the interaction had already saved and the
composer had cleared its staged list; it now logs what was lost. - Deleting an interaction from a page that does not anchor it (#32). The
feed's Delete had one gate (the row is in this feed); it now has two — the
row must also be anchored here, so a row that merely spills in via party
involvement or the member rollup is read-only and managed from its own page.
A concurrent delete'sEcto.StaleEntryErrorrefreshes instead of crashing
the LiveView into a reconnect. - Role-checkbox alignment on the contact and company forms (#32) — daisyUI's
label+fieldset-legendclasses carry their own block spacing and sat the
text visibly off the checkbox. - The migration prefix guard used
~r/^...$/, which PCRE also matches before a
trailing newline; it is now anchored with\A/\z(#32).
Hex: https://hex.pm/packages/phoenix_kit_crm/0.10.0 · Docs: https://phoenix-kit-crm.hexdocs.pm/0.10.0
0.8.1 - 2026-08-25
Added
- SchemaOwnerGuard (#28). When
PGDATABASEpoints the test suite at a shared Postgres database,Ecto.Migrator'sschema_migrationsbookkeeping is keyed only by version number with no package namespace — another package's already-applied migration can silently mask this package's migration of the same number. The guard stamps an owner marker onschema_migrationsafter a successful boot and refuses (with a clear error) any future boot that finds a marker naming a different package, instead of silently colliding.
Changed
- Dependency bump:
phoenix_kit2.13.7 → 2.13.8,phoenix_kit_comments0.4.2 → 0.4.3.
0.8.0 - 2026-08-24
0.8.0 - 2026-08-24
Live company page and tab intros (#27), plus review follow-up.
Added
- Company page follows roster, member interactions, and catalogue data without a reload
- Tab intros on contact and company show pages; Members tab links to a preselected new-contact form
Changed
- Mirror resolution rebuilds the form from the resolved record so Save cannot undo it
- CRM settings remounts after a role toggle so the sidebar follows
- Role view reloads the CRM-contact map when that column is ticked on
Fixed
- Contact Files interaction roll-up refreshes live
- List-members locale-apply preview stays in step with membership
- Comparison duplicate-email groups re-query list, count, and rows on reopen; citext keys are lowercased
- Company Catalogue tab also refreshes on category changes
- delete_contact / set_primary_company notify companies from inside the transaction
- Mirror resolution broadcasts after commit
0.7.2 - 2026-08-22
0.7.1 - 2026-08-21
Added
manufacturerparty role, alongsidesupplier/customer/partner
(#23). Granted the same way as the others; a company or contact can hold
supplierandmanufacturerat once — the case the shared role table
exists for.- Roles now carry a validity window.
valid_from/valid_toused to be
decorative — every query filtered onis_activealone, so a role stamped
with an expiredvalid_tostill resolved as live forever. Every role query
now goes throughPartyRoles.in_force/1, so an out-of-window role stops
resolving even whileis_activestays true, and re-granting a lapsed role
starts a fresh tenure instead of silently returning the stale row. - Batch party resolution:
get_suppliers/1,get_manufacturers/1, and
list_parties_with_role/2(withlist_suppliers/1,list_manufacturers/1,
list_customers/1wrappers) resolve many party uuids in one pair of
queries instead of one call per row — the N+1 a catalogue page rendering
100 items would otherwise hit. - A Catalogue tab on the company page (#23), shown when the catalogue
module is installed and enabled. Lists items this company supplies or
manufactures, with a column picker and a warning banner when items still
reference a role the company no longer holds. Soft-dependency guarded
(Code.ensure_loaded?+apply/3) — the CRM has no compile-time
dependency on the catalogue. Companygaineddescriptionandlogo_url— the fields the catalogue's
own supplier/manufacturer rows used to carry, now that those parties are
managed here.- A V04 migration adds a
CHECKconstraint on the role vocabulary and a
partial unique index (roleable_uuid,role)WHERE is_active, so a
party can hold at most one active row per role — closing a state
get_supplier/1previously had to defend against withlimit(1). Legacy
clientrows (pre-rename) are normalized tocustomerfirst, since
ADD CONSTRAINTvalidates existing rows.
Fixed
get_manufacturer/1and its batch/list counterparts now return
logo_url. V04's owndescription/logo_urladdition toCompanywas
meant to let the catalogue read a manufacturer's brand mark from CRM, but
none of the federation resolvers included the field — it was captured on
the company form and reachable by nothing.- Removed seven
priv/gettextcatalog entries ("Item","Their code",
"Unit cost","Lead time","primary","%{n} d","SKU", with real
Estonian and Russian translations) that nogettext/1call in this repo
produces. phoenix_kit_crm.import_suppliers_from_catalogue: raw-SQL uuid parameters
are now dumped to their 16-byte binary form before use (Ecto.UUID.dump/1)
and uuid columns read back from raw SQL are loaded to text before display
(Ecto.UUID.load/1) — both directions previously raised on a row that had
actually been linked.
Changed
- Dependency updates (
mix.lock):phoenix_kit2.13.4,phoenix1.8.12,
phoenix_live_view1.2.10, and routine bumps toecto,bandit,swoosh,
req,tesla, and others.
0.7.0 - 2026-08-14
Manual two-way mirror between CRM records and system users, and module-owned migrations.
Added
- Company ↔ organization-user and Contact ↔ person-user mirroring, as explicit admin actions (Create mirror / Link existing / Unlink) from both sides. Nothing is automatic — no PubSub, no background sync. The form you act from is the master, and a diverging field raises a per-field conflict modal rather than silently overwriting. The diff is recomputed fresh at submit time, so a stale selection or an out-of-band edit cannot drive a bad write, and every two-record write runs in one transaction.
- Module-owned migrations (
migration_module/0), following thephoenix_kit_legalprecedent. V01 idempotently adopts the 10phoenix_kit_crm_*tables the core chain historically created and addscompanies.user_uuidwith a partial unique index — so any number of companies may be unlinked while a linked user maps to at most one.down/1never drops a table, and a test pins that.
user_uuid is never cast from form params on either schema — only through a dedicated link_user_changeset/2.
Note for hosts: because companies.user_uuid is outside core's manifest, mix phoenix_kit.repair reports it as an info-level extra_object. Repair takes no action on it and will not drop it.
586 tests, 0 failures; mix precommit exit 0. Lockfile moves to phoenix_kit 2.4.0; the ~> 2.0 pin is unchanged.
0.6.1 - 2026-08-11
Fixed
- The remaining untranslated Estonian and Russian strings are filled in (#21). A large block of msgids had empty
msgstrvalues, so those parts of the CRM rendered in English inside an otherwise translated UI.
Changed
- The CRM page subtitle now spells out what the module is: "Customer relationship management — companies, contacts and the roles that can reach them".
- Dependency updates (
phoenix_kit2.2.0,phoenix_kit_comments0.4.0,phoenix1.8.10,hackney4.7.3).
0.6.0 - 2026-08-10
⚠️ Requires phoenix_kit ~> 2.0
Core 2.0.0 refuses to migrate a database below its squashed V135 floor — check mix phoenix_kit.status before upgrading. A host below V135 must install phoenix_kit 1.7.236 (the migration bridge) and migrate to at least V135 first.
Sibling pins were raised in step. Each sibling's new minor is the first release of that package requiring core 2.0, so the previous pins could only have resolved versions still requiring core 1.7 — an unsatisfiable set. See CHANGELOG.md for the full entry.
0.3.2 - 2026-07-20
Fixed
Lists.recount_list/1raised if the list row it was recounting was deleted concurrently between the caller loading it and the recount's ownUPDATE— a narrower race left open by 0.3.1's fix. It now returns:missinginstead of raising, andContacts.delete_contact/1's recount step tolerates it: a moot counter on an already-deleted list no longer rolls back the whole contact deletion (PR #15).
0.3.0 - 2026-07-19
Stage 3 of the restructuring plan (PR #13): CRM contact lists, a CSV/text account importer, per-list locale with bulk-apply, contact opt-out/consent, and a duplicate-email/list-overlap comparison screen.
Requires phoenix_kit >= 1.7.203.
Post-merge review + fixes (mount/3 database-query timing on 3 new LiveViews, and a dependency-floor correction) are documented in dev_docs/pull_requests/2026/13-crm-contact-lists/CLAUDE_REVIEW.md.
See CHANGELOG.md for the full list.