mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* Add invited users with delete button to admin users page Shows pending invitations per family below active users in /admin/users/. Each invitation row has a red Delete button aligned with the role column. Alt/option-clicking any Delete button changes all invitation button labels to "Delete All" and destroys all pending invitations for that family. - Add admin routes: DELETE /admin/invitations/:id and DELETE /admin/families/:id/invitations - Add Admin::InvitationsController with destroy and destroy_all actions - Load pending invitations grouped by family in users controller index - Render invitation rows in a dashed-border tbody below active user rows - Add admin-invitation-delete Stimulus controller for alt-click behavior - Add i18n strings for invitation UI and flash messages https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Fix destroy_all using params[:id] from member route The member route /admin/families/:id/invitations sets params[:id], not params[:family_id], so Family.find was always receiving nil. https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Fix translation key in destroy_all to match locale t(".success_all") looked up a nonexistent key; the locale defines admin.invitations.destroy_all.success, so t(".success") is correct. https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Scope bulk delete to pending invitations and allow re-inviting emails - destroy_all now uses family.invitations.pending.destroy_all so accepted and expired invitation history is preserved - Replace blanket email uniqueness validation with a custom check scoped to pending invitations only, so the same email can be invited again after an invitation is deleted or expires https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Drop unconditional unique DB index on invitations(email, family_id) The model-level uniqueness check was already scoped to pending invitations, but the blanket unique index on (email, family_id) still caused ActiveRecord::RecordNotUnique when re-inviting an email that had any historical invitation record in the same family (e.g. after an accepted invite or after an account deletion). Replace it with no DB-level unique constraint — the no_duplicate_pending_invitation_in_family model validation is the sole enforcer and correctly scopes uniqueness to pending rows only. https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Replace blanket unique index with partial unique index on pending invitations Instead of dropping the DB-level uniqueness constraint entirely, replace the unconditional unique index on (email, family_id) with a partial unique index scoped to WHERE accepted_at IS NULL. This enforces the invariant at the DB layer (no two non-accepted invitations for the same email in a family) while allowing re-invites once a prior invitation has been accepted. https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm * Fix migration version and make remove_index reversible - Change Migration[8.0] to Migration[7.2] to match the rest of the codebase - Pass column names to remove_index so Rails can reconstruct the old index on rollback https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
87 lines
2.6 KiB
Ruby
87 lines
2.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Admin
|
|
class UsersController < Admin::BaseController
|
|
before_action :set_user, only: %i[update]
|
|
|
|
def index
|
|
authorize User
|
|
scope = policy_scope(User)
|
|
.left_joins(family: :subscription)
|
|
.includes(family: :subscription)
|
|
|
|
scope = scope.where(role: params[:role]) if params[:role].present?
|
|
scope = apply_trial_filter(scope) if params[:trial_status].present?
|
|
|
|
users = scope.order(
|
|
Arel.sql(
|
|
"CASE " \
|
|
"WHEN subscriptions.status = 'trialing' THEN 0 " \
|
|
"WHEN subscriptions.id IS NULL THEN 1 " \
|
|
"ELSE 2 END, " \
|
|
"subscriptions.trial_ends_at ASC NULLS LAST, users.email ASC"
|
|
)
|
|
)
|
|
|
|
family_ids = users.map(&:family_id).uniq
|
|
@accounts_count_by_family = Account.where(family_id: family_ids).group(:family_id).count
|
|
@entries_count_by_family = Entry.joins(:account).where(accounts: { family_id: family_ids }).group("accounts.family_id").count
|
|
|
|
user_ids = users.map(&:id).uniq
|
|
@last_login_by_user = Session.where(user_id: user_ids).group(:user_id).maximum(:created_at)
|
|
@sessions_count_by_user = Session.where(user_id: user_ids).group(:user_id).count
|
|
|
|
@families_with_users = users.group_by(&:family).sort_by do |family, _users|
|
|
-(@entries_count_by_family[family.id] || 0)
|
|
end
|
|
|
|
@invitations_by_family = Invitation.pending
|
|
.where(family_id: family_ids)
|
|
.group_by(&:family_id)
|
|
|
|
@trials_expiring_in_7_days = Subscription
|
|
.where(status: :trialing)
|
|
.where(trial_ends_at: Time.current..7.days.from_now)
|
|
.count
|
|
end
|
|
|
|
def update
|
|
authorize @user
|
|
|
|
if @user.update(user_params)
|
|
Rails.logger.info(
|
|
"[Admin::Users] Role changed - " \
|
|
"by_user_id=#{Current.user.id} " \
|
|
"target_user_id=#{@user.id} " \
|
|
"new_role=#{@user.role}"
|
|
)
|
|
redirect_to admin_users_path, notice: t(".success")
|
|
else
|
|
redirect_to admin_users_path, alert: t(".failure")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_user
|
|
@user = User.find(params[:id])
|
|
end
|
|
|
|
def user_params
|
|
params.require(:user).permit(:role)
|
|
end
|
|
|
|
def apply_trial_filter(scope)
|
|
case params[:trial_status]
|
|
when "expiring_soon"
|
|
scope.where(subscriptions: { status: :trialing })
|
|
.where(subscriptions: { trial_ends_at: Time.current..7.days.from_now })
|
|
when "trialing"
|
|
scope.where(subscriptions: { status: :trialing })
|
|
else
|
|
scope
|
|
end
|
|
end
|
|
end
|
|
end
|