Remove memberships page

This commit is contained in:
Eugene Burmakin 2025-10-04 16:17:26 +02:00
parent 2a85735aee
commit c6fc4328d7
7 changed files with 4 additions and 475 deletions

View file

@ -4,17 +4,7 @@ class FamilyMembershipsController < ApplicationController
before_action :authenticate_user!
before_action :ensure_family_feature_enabled!
before_action :set_family
before_action :set_membership, only: %i[show destroy]
def index
authorize @family, :show?
@members = @family.members.includes(:family_membership)
end
def show
authorize @membership, :show?
end
before_action :set_membership, only: %i[destroy]
def destroy
authorize @membership

View file

@ -51,10 +51,6 @@
<%= t('families.show.members_title', default: 'Family Members') %>
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">(<%= @members.count %>)</span>
</h2>
<%= link_to family_members_path(@family),
class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium" do %>
<%= t('families.show.manage_members', default: 'Manage') %>
<% end %>
</div>
<div class="space-y-3">

View file

@ -1,168 +0,0 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<!-- Header -->
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6 mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
Family Members
</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">
Manage members of <%= @family.name %>
</p>
</div>
<%= link_to family_path(@family),
class: "bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
← Back to Family
<% end %>
</div>
</div>
<!-- Members List -->
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
All Members (<%= @members.count %>)
</h2>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<% @members.each do |member| %>
<div class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700">
<div class="flex items-center justify-between">
<!-- Member Info -->
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">
<%= member.email.first.upcase %>
</span>
</div>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
<%= member.email %>
</p>
<% if member.family_membership.role == 'owner' %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
Owner
</span>
<% else %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
<%= member.family_membership.role.humanize %>
</span>
<% end %>
<% if member == current_user %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
You
</span>
<% end %>
</div>
<div class="flex items-center space-x-4 mt-1">
<p class="text-sm text-gray-500 dark:text-gray-400">
Joined <%= member.family_membership.created_at.strftime('%B %d, %Y') %>
</p>
<% if member.family_membership.role == 'owner' %>
<p class="text-sm text-gray-500 dark:text-gray-400">
Created this family
</p>
<% end %>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center space-x-3">
<%= link_to family_member_path(@family, member.family_membership),
class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium" do %>
View
<% end %>
<% if policy(member.family_membership).destroy? %>
<% if !member.family_membership.owner? %>
<%= link_to family_member_path(@family, member.family_membership),
method: :delete,
confirm: "Are you sure you want to remove #{member.email} from the family?",
class: "text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium" do %>
Remove
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Family Statistics -->
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Total Members
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= @members.count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Active Members
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= @members.count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-yellow-600 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Family Age
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= time_ago_in_words(@family.created_at) %>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,202 +0,0 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto">
<!-- Header -->
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="h-16 w-16 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center">
<span class="text-xl font-medium text-gray-700 dark:text-gray-200">
<%= @membership.user.email.first.upcase %>
</span>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
<%= @membership.user.email %>
</h1>
<div class="flex items-center space-x-2 mt-1">
<% if @membership.role == 'owner' %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
Family Owner
</span>
<% else %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
<%= @membership.role.humanize %>
</span>
<% end %>
<% if @membership.user == current_user %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
You
</span>
<% end %>
</div>
</div>
</div>
<div class="flex space-x-3">
<%= link_to family_members_path(@family),
class: "bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
← All Members
<% end %>
<% if policy(@membership).destroy? && !@membership.owner? %>
<%= link_to family_member_path(@family, @membership),
method: :delete,
confirm: "Are you sure you want to remove #{@membership.user.email} from the family?",
class: "bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
Remove Member
<% end %>
<% end %>
</div>
</div>
</div>
<!-- Member Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Basic Information -->
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
Basic Information
</h2>
<dl class="space-y-4">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Email Address
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100"><%= @membership.user.email %></dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Family Role
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= @membership.role.humanize %>
<% if @membership.role == 'owner' %>
<span class="text-gray-500 dark:text-gray-400">
- Can manage family settings and members
</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Joined Date
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= @membership.created_at.strftime('%B %d, %Y at %I:%M %p') %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Time in Family
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= time_ago_in_words(@membership.created_at) %>
</dd>
</div>
</dl>
</div>
<!-- Family Information -->
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
Family Information
</h2>
<dl class="space-y-4">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Family Name
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100"><%= @family.name %></dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Family Creator
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100"><%= @family.creator.email %></dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Family Created
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= @family.created_at.strftime('%B %d, %Y') %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Total Members
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= pluralize(@family.members.count, 'member') %>
</dd>
</div>
</dl>
</div>
</div>
<!-- Actions and Warnings -->
<div class="mt-6 space-y-4">
<!-- Owner-specific Warning -->
<% if @membership.owner? %>
<div class="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Family Owner Protection
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
Family owners cannot remove their own membership. To leave the family, the owner must delete the entire family instead.
</p>
</div>
</div>
</div>
</div>
<% end %>
<!-- Self-removal Info -->
<% if @membership.user == current_user %>
<div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
Your Membership
</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
This is your own membership. <% if @membership.owner? %>As the owner, you can delete the family to leave it.<% else %>You can leave the family at any time from the family page.<% end %>
</p>
</div>
<div class="mt-3">
<%= link_to family_path(@family),
class: "text-blue-800 hover:text-blue-900 dark:text-blue-200 dark:hover:text-blue-100 font-medium" do %>
Go to Family Page →
<% end %>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>

View file

@ -69,7 +69,7 @@ Rails.application.routes.draw do
post :accept
end
end
resources :members, only: %i[index show destroy], controller: 'family_memberships'
resources :members, only: %i[destroy], controller: 'family_memberships'
end
# Public family invitation acceptance (no auth required)

View file

@ -15,70 +15,6 @@ RSpec.describe 'Family Memberships', type: :request do
sign_in user
end
describe 'GET /families/:family_id/members' do
it 'shows all family members' do
get "/families/#{family.id}/members"
expect(response).to have_http_status(:ok)
end
context 'when user is not in the family' do
let(:outsider) { create(:user) }
before { sign_in outsider }
it 'redirects to families index' do
get "/families/#{family.id}/members"
expect(response).to redirect_to(families_path)
end
end
context 'when not authenticated' do
before { sign_out user }
it 'redirects to login' do
get "/families/#{family.id}/members"
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'GET /families/:family_id/members/:id' do
it 'shows a specific membership' do
get "/families/#{family.id}/members/#{member_membership.id}"
expect(response).to have_http_status(:ok)
end
context 'when membership does not belong to the family' do
let(:other_family) { create(:family) }
let(:other_membership) { create(:family_membership, family: other_family) }
it 'returns not found' do
get "/families/#{family.id}/members/#{other_membership.id}"
expect(response).to have_http_status(:not_found)
end
end
context 'when user is not in the family' do
let(:outsider) { create(:user) }
before { sign_in outsider }
it 'redirects to families index' do
get "/families/#{family.id}/members/#{member_membership.id}"
expect(response).to redirect_to(families_path)
end
end
context 'when not authenticated' do
before { sign_out user }
it 'redirects to login' do
get "/families/#{family.id}/members/#{member_membership.id}"
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'DELETE /families/:family_id/members/:id' do
context 'when removing a regular member' do
it 'removes the member from the family' do
@ -170,23 +106,6 @@ RSpec.describe 'Family Memberships', type: :request do
end
end
context 'when member views another member' do
before { sign_in member_user }
it 'allows viewing membership' do
get "/families/#{family.id}/members/#{owner_membership.id}"
expect(response).to have_http_status(:ok)
end
end
context 'when member views members list' do
before { sign_in member_user }
it 'allows viewing members list' do
get "/families/#{family.id}/members"
expect(response).to have_http_status(:ok)
end
end
end
describe 'member removal workflow' do

View file

@ -79,12 +79,9 @@ RSpec.describe 'Family Workflows', type: :request do
expect(user3.reload.family).to eq(family)
expect(family.reload.members.count).to eq(3)
# Step 6: Family owner views and manages members
# Step 6: Family owner views members on family show page
sign_in user1
get "/families/#{family.id}/members"
expect(response).to have_http_status(:ok)
get "/families/#{family.id}/members/#{user2.family_membership.id}"
get "/families/#{family.id}"
expect(response).to have_http_status(:ok)
# Step 7: Owner removes a member
@ -257,9 +254,6 @@ RSpec.describe 'Family Workflows', type: :request do
sign_in user3
get "/families/#{family.id}"
expect(response).to redirect_to(families_path)
get "/families/#{family.id}/members"
expect(response).to redirect_to(families_path)
end
end