Family sharing (#1272)

* Initial account sharing changes

* Update schema.rb

* Update schema.rb

* Change sharing UI to modal

* UX fixes and sharing controls

* Scope include in finances better

* Update totals.rb

* Update totals.rb

* Scope reports to finance account scope

* Update impersonation_sessions_controller_test.rb

* Review fixes

* Update schema.rb

* Update show.html.erb

* FIX db validation

* Refine edit permissions

* Review items

* Review

* Review

* Add application level helper

* Critical review

* Address remaining review items

* Fix modals

* more scoping

* linter

* small UI fix

* Fix: Sync broadcasts push unscoped balance sheet to all users

* Update sync_complete_event.rb

 The fix removes the sidebar broadcasts (which rendered unscoped account groups using family.balance_sheet without user context)
  along with the now-unused sidebar_targets, account_group, and family_balance_sheet private methods.

  The sidebar will still update correctly — when the sync completes, Family::SyncCompleteEvent#broadcast fires family.broadcast_refresh, which triggers a
  morph-based page refresh for each user with their own authenticated session, rendering properly scoped sidebar content.
This commit is contained in:
soky srm
2026-03-25 10:50:23 +01:00
committed by GitHub
parent 6cf7d20010
commit 560c9fbff3
75 changed files with 1520 additions and 401 deletions

View File

@@ -0,0 +1,21 @@
class AddAccountSharingSupport < ActiveRecord::Migration[7.2]
def change
# Family-level default: whether new accounts are shared with all members by default
add_column :families, :default_account_sharing, :string, default: "shared", null: false
# Account ownership: who created/owns the account
add_reference :accounts, :owner, type: :uuid, foreign_key: { to_table: :users }, null: true, index: true
# Sharing join table: per-user access to accounts they don't own
create_table :account_shares, id: :uuid, default: -> { "gen_random_uuid()" } do |t|
t.references :account, type: :uuid, null: false, foreign_key: true
t.references :user, type: :uuid, null: false, foreign_key: true
t.string :permission, null: false, default: "read_only"
t.boolean :include_in_finances, null: false, default: true
t.timestamps
end
add_index :account_shares, [ :account_id, :user_id ], unique: true
add_index :account_shares, [ :user_id, :include_in_finances ]
end
end

View File

@@ -0,0 +1,34 @@
class BackfillAccountOwnersAndShares < ActiveRecord::Migration[7.2]
def up
# Existing families keep current behavior: all accounts shared
Family.update_all(default_account_sharing: "shared")
# For each family, assign all accounts to the admin (or first user)
Family.find_each do |family|
admin = family.users.find_by(role: %w[admin super_admin]) || family.users.order(:created_at).first
next unless admin
family.accounts.where(owner_id: nil).update_all(owner_id: admin.id)
# Create shares for non-owner members (preserves current full-access behavior)
member_ids = family.users.where.not(id: admin.id).pluck(:id)
account_ids = family.accounts.pluck(:id)
if member_ids.any? && account_ids.any?
records = member_ids.product(account_ids).map do |user_id, account_id|
{ user_id: user_id, account_id: account_id, permission: "full_control",
include_in_finances: true, created_at: Time.current, updated_at: Time.current }
end
AccountShare.upsert_all(records, unique_by: %i[account_id user_id])
end
end
# Owner is enforced at the model level via before_validation callback
# Keeping nullable at DB level for backward compatibility with tests/seeds
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@@ -0,0 +1,6 @@
class AddCheckConstraintsToSharingColumns < ActiveRecord::Migration[7.2]
def change
add_check_constraint :families, "default_account_sharing IN ('shared', 'private')", name: "chk_families_default_account_sharing"
add_check_constraint :account_shares, "permission IN ('full_control', 'read_write', 'read_only')", name: "chk_account_shares_permission"
end
end

View File

@@ -0,0 +1,11 @@
class ChangeAccountsOwnerFkToNullify < ActiveRecord::Migration[7.2]
def up
remove_foreign_key :accounts, :users, column: :owner_id
add_foreign_key :accounts, :users, column: :owner_id, on_delete: :nullify
end
def down
remove_foreign_key :accounts, :users, column: :owner_id
add_foreign_key :accounts, :users, column: :owner_id
end
end