Fix leaving and deleting family confirmation dialogs

This commit is contained in:
Eugene Burmakin 2025-09-28 14:49:32 +02:00
parent 1f67e889e3
commit 5252388b8c
10 changed files with 228 additions and 226 deletions

File diff suppressed because one or more lines are too long

View file

@ -19,9 +19,9 @@ class FamilyMembershipsController < ApplicationController
def destroy
authorize @membership
if @membership.owner? && @family.members.count > 1
if @membership.owner?
redirect_to family_path(@family),
alert: 'Cannot remove family owner while other members exist. Transfer ownership first.'
alert: 'Family owners cannot remove their own membership. To leave the family, delete it instead.'
else
member_email = @membership.user.email
@membership.destroy!

View file

@ -6,7 +6,7 @@ class FamilyMailer < ApplicationMailer
@family = invitation.family
@invited_by = invitation.invited_by
@accept_url = family_invitation_url(@invitation.token)
pp @accept_url
mail(
to: @invitation.email,
subject: "You've been invited to join #{@family.name} on Dawarich"

View file

@ -1,25 +1,25 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white shadow rounded-lg p-6">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
<%= t('families.edit.title', default: 'Edit Family') %>
</h1>
<%= link_to family_path(@family),
class: "text-gray-600 hover:text-gray-800 font-medium" do %>
class: "text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 font-medium" do %>
<%= t('families.edit.back', default: '← Back to Family') %>
<% end %>
</div>
<%= form_with model: @family, local: true, class: "space-y-6" do |form| %>
<% if @family.errors.any? %>
<div class="bg-red-50 border border-red-200 rounded-md p-4">
<div class="bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-700 rounded-md p-4">
<div class="flex">
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
<%= t('families.edit.error_title', default: 'There were problems with your submission:') %>
</h3>
<div class="mt-2 text-sm text-red-700">
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul class="list-disc pl-5 space-y-1">
<% @family.errors.full_messages.each do |message| %>
<li><%= message %></li>
@ -32,45 +32,45 @@
<% end %>
<div>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" %>
<%= form.text_field :name,
class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
placeholder: t('families.form.name_placeholder', default: 'Enter your family name') %>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= t('families.edit.name_help', default: 'Choose a name that all family members will recognize.') %>
</p>
</div>
<div class="bg-gray-50 p-4 rounded-md">
<h3 class="text-sm font-medium text-gray-900 mb-2">
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-md">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
<%= t('families.edit.family_info', default: 'Family Information') %>
</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
<%= t('families.edit.creator', default: 'Created by') %>
</dt>
<dd class="text-sm text-gray-900"><%= @family.creator.email %></dd>
<dd class="text-sm text-gray-900 dark:text-gray-100"><%= @family.creator.email %></dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
<%= t('families.edit.created_on', default: 'Created on') %>
</dt>
<dd class="text-sm text-gray-900"><%= @family.created_at.strftime('%B %d, %Y') %></dd>
<dd class="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">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
<%= t('families.edit.members_count', default: 'Members') %>
</dt>
<dd class="text-sm text-gray-900">
<dd class="text-sm text-gray-900 dark:text-gray-100">
<%= pluralize(@family.members.count, 'member') %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
<%= t('families.edit.last_updated', default: 'Last updated') %>
</dt>
<dd class="text-sm text-gray-900"><%= @family.updated_at.strftime('%B %d, %Y') %></dd>
<dd class="text-sm text-gray-900 dark:text-gray-100"><%= @family.updated_at.strftime('%B %d, %Y') %></dd>
</div>
</dl>
</div>
@ -78,9 +78,9 @@
<div class="flex items-center justify-between pt-4">
<div class="flex space-x-3">
<%= form.submit t('families.edit.save_changes', default: 'Save Changes'),
class: "bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
class: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
<%= link_to family_path(@family),
class: "bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-md font-medium transition-colors duration-200" do %>
class: "bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-6 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('families.edit.cancel', default: 'Cancel') %>
<% end %>
</div>
@ -88,9 +88,9 @@
<% if policy(@family).destroy? %>
<%= link_to family_path(@family),
method: :delete,
confirm: t('families.edit.delete_confirm', default: 'Are you sure you want to delete this family? This action cannot be undone and will remove all members.'),
class: "text-red-600 hover:text-red-800 font-medium" do %>
<%= t('families.edit.delete_family', default: 'Delete Family') %>
data: { turbo_confirm: 'Are you sure you want to delete this family? This action cannot be undone and will remove all members.' },
class: "text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 font-medium" do %>
Delete Family
<% end %>
<% end %>
</div>

View file

@ -1,44 +1,44 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-4">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">
<%= t('families.index.title', default: 'Family Management') %>
</h1>
<p class="text-gray-600">
<p class="text-gray-600 dark:text-gray-400">
<%= t('families.index.description', default: 'Create or join a family to share your location data with loved ones.') %>
</p>
</div>
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
<%= t('families.index.create_family', default: 'Create Your Family') %>
</h2>
<%= form_with model: Family.new, local: true, class: "space-y-4" do |form| %>
<div>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
<%= form.text_field :name,
placeholder: t('families.form.name_placeholder', default: 'Enter your family name'),
class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" %>
class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" %>
</div>
<div class="flex justify-end">
<%= form.submit t('families.form.create', default: 'Create Family'),
class: "bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
class: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
</div>
<% end %>
</div>
<div class="mt-8 text-center">
<h3 class="text-lg font-medium text-gray-900 mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
<%= t('families.index.have_invitation', default: 'Have an invitation?') %>
</h3>
<p class="text-gray-600 mb-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">
<%= t('families.index.invitation_instructions', default: 'If someone has invited you to join their family, you should have received an email with an invitation link.') %>
</p>
<div class="text-sm text-gray-500">
<div class="text-sm text-gray-500 dark:text-gray-400">
<%= t('families.index.invitation_help', default: 'Check your email for an invitation link that looks like: ') %>
<code class="bg-gray-100 px-2 py-1 rounded text-xs">
<code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-xs">
<%= "#{request.base_url}/invitations/..." %>
</code>
</div>

View file

@ -1,24 +1,24 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-4">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">
<%= t('families.new.title', default: 'Create Your Family') %>
</h1>
<p class="text-gray-600">
<p class="text-gray-600 dark:text-gray-400">
<%= t('families.new.description', default: 'Create a family to share your location data with your loved ones.') %>
</p>
</div>
<div class="bg-white shadow rounded-lg p-6">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<%= form_with model: @family, local: true, class: "space-y-6" do |form| %>
<% if @family.errors.any? %>
<div class="bg-red-50 border border-red-200 rounded-md p-4">
<div class="bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-700 rounded-md p-4">
<div class="flex">
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
<%= t('families.new.error_title', default: 'There were problems with your submission:') %>
</h3>
<div class="mt-2 text-sm text-red-700">
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul class="list-disc pl-5 space-y-1">
<% @family.errors.full_messages.each do |message| %>
<li><%= message %></li>
@ -31,20 +31,20 @@
<% end %>
<div>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2" %>
<%= form.text_field :name,
class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
placeholder: t('families.form.name_placeholder', default: 'Enter your family name') %>
<p class="mt-1 text-sm text-gray-500">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= t('families.new.name_help', default: 'Choose a name that all family members will recognize, like "The Smith Family" or "Our Travel Group".') %>
</p>
</div>
<div class="bg-blue-50 p-4 rounded-md">
<h3 class="text-sm font-medium text-blue-900 mb-2">
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-md">
<h3 class="text-sm font-medium text-blue-900 dark:text-blue-200 mb-2">
<%= t('families.new.what_happens_title', default: 'What happens next?') %>
</h3>
<ul class="text-sm text-blue-800 space-y-1">
<ul class="text-sm text-blue-800 dark:text-blue-300 space-y-1">
<li>• <%= t('families.new.what_happens_1', default: 'You will become the family owner') %></li>
<li>• <%= t('families.new.what_happens_2', default: 'You can invite others to join your family') %></li>
<li>• <%= t('families.new.what_happens_3', default: 'Family members can view shared location data') %></li>
@ -54,9 +54,9 @@
<div class="flex items-center justify-between">
<%= form.submit t('families.new.create_family', default: 'Create Family'),
class: "bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
class: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-6 py-2 rounded-md font-medium transition-colors duration-200" %>
<%= link_to families_path,
class: "text-gray-600 hover:text-gray-800 font-medium" do %>
class: "text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 font-medium" do %>
<%= t('families.new.back', default: '← Back') %>
<% end %>
</div>

View file

@ -1,11 +1,11 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<!-- Family Header -->
<div class="bg-white shadow rounded-lg p-6 mb-6">
<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"><%= @family.name %></h1>
<p class="text-gray-600 mt-1">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100"><%= @family.name %></h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">
<%= t('families.show.created_by', default: 'Created by') %>
<%= @family.creator.email %>
<%= t('families.show.on_date', default: 'on') %>
@ -16,7 +16,7 @@
<div class="flex space-x-3">
<% if policy(@family).update? %>
<%= link_to edit_family_path(@family),
class: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
class: "bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('families.show.edit', default: 'Edit Family') %>
<% end %>
<% end %>
@ -24,18 +24,18 @@
<% if policy(@family).leave? %>
<%= link_to leave_family_path(@family),
method: :delete,
confirm: t('families.show.leave_confirm', default: 'Are you sure you want to leave this family?'),
class: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('families.show.leave', default: 'Leave Family') %>
data: { confirm: 'Are you sure you want to leave this family?', turbo_confirm: 'Are you sure you want to leave this 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 %>
Leave Family
<% end %>
<% end %>
<% if policy(@family).destroy? %>
<%= link_to family_path(@family),
method: :delete,
confirm: t('families.show.delete_confirm', default: 'Are you sure you want to delete this family? This action cannot be undone.'),
class: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('families.show.delete', default: 'Delete Family') %>
data: { confirm: 'Are you sure you want to delete this family? This action cannot be undone.', turbo_confirm: 'Are you sure you want to delete this family? This action cannot be undone.' },
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 %>
Delete Family
<% end %>
<% end %>
</div>
@ -44,33 +44,33 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Family Members -->
<div class="bg-white shadow rounded-lg p-6">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
<%= t('families.show.members_title', default: 'Family Members') %>
<span class="text-sm font-normal text-gray-500">(<%= @members.count %>)</span>
<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 text-sm font-medium" do %>
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">
<% @members.each do |member| %>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<div class="font-medium text-gray-900"><%= member.email %></div>
<div class="text-sm text-gray-500">
<div class="font-medium text-gray-900 dark:text-gray-100"><%= member.email %></div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<%= member.family_membership.role.humanize %>
<% if member.family_membership.role == 'owner' %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 ml-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 ml-2">
<%= t('families.show.owner_badge', default: 'Owner') %>
</span>
<% end %>
</div>
</div>
<div class="text-sm text-gray-500">
<div class="text-sm text-gray-500 dark:text-gray-400">
<%= t('families.show.joined_on', default: 'Joined') %>
<%= member.family_membership.created_at.strftime('%b %d, %Y') %>
</div>
@ -80,25 +80,25 @@
</div>
<!-- Pending Invitations -->
<div class="bg-white shadow rounded-lg p-6">
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
<%= t('families.show.invitations_title', default: 'Pending Invitations') %>
<span class="text-sm font-normal text-gray-500">(<%= @pending_invitations.count %>)</span>
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">(<%= @pending_invitations.count %>)</span>
</h2>
</div>
<% if @pending_invitations.any? %>
<div class="space-y-3 mb-4">
<% @pending_invitations.each do |invitation| %>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div>
<div class="font-medium text-gray-900"><%= invitation.email %></div>
<div class="text-sm text-gray-500">
<div class="font-medium text-gray-900 dark:text-gray-100"><%= invitation.email %></div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<%= t('families.show.invited_on', default: 'Invited') %>
<%= invitation.created_at.strftime('%b %d, %Y') %>
</div>
<div class="text-xs text-gray-400">
<div class="text-xs text-gray-400 dark:text-gray-500">
<%= t('families.show.expires_on', default: 'Expires') %>
<%= invitation.expires_at.strftime('%b %d, %Y at %I:%M %p') %>
</div>
@ -106,16 +106,16 @@
<% if policy(@family).manage_invitations? %>
<%= link_to family_invitation_path(@family, invitation),
method: :delete,
confirm: t('families.show.cancel_invitation_confirm', default: 'Are you sure you want to cancel this invitation?'),
class: "text-red-600 hover:text-red-800 text-sm font-medium" do %>
<%= t('families.show.cancel', default: 'Cancel') %>
confirm: 'Are you sure you want to cancel this invitation?',
class: "text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium" do %>
Cancel
<% end %>
<% end %>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-center py-4">
<p class="text-gray-500 dark:text-gray-400 text-center py-4">
<%= t('families.show.no_pending_invitations', default: 'No pending invitations') %>
</p>
<% end %>
@ -123,21 +123,21 @@
<!-- Invite New Member -->
<% if policy(@family).invite? %>
<div class="border-t pt-4">
<h3 class="text-lg font-medium text-gray-900 mb-3">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
<%= t('families.show.invite_member', default: 'Invite New Member') %>
</h3>
<%= form_with model: [@family, FamilyInvitation.new], url: family_invitations_path(@family), local: true, class: "space-y-3" do |form| %>
<div>
<%= form.label :email, t('families.show.email_label', default: 'Email Address'), class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.label :email, t('families.show.email_label', default: 'Email Address'), class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
<%= form.email_field :email,
placeholder: t('families.show.email_placeholder', default: 'Enter email address'),
class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" %>
class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" %>
</div>
<div class="flex justify-end">
<%= form.submit t('families.show.send_invitation', default: 'Send Invitation'),
class: "bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" %>
class: "bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" %>
</div>
<% end %>
</div>
@ -145,4 +145,4 @@
</div>
</div>
</div>
</div>
</div>

View file

@ -1,41 +1,41 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<!-- Header -->
<div class="bg-white shadow rounded-lg p-6 mb-6">
<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">
<%= t('family_memberships.index.title', default: 'Family Members') %>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
Family Members
</h1>
<p class="text-gray-600 mt-1">
<%= t('family_memberships.index.subtitle', default: 'Manage members of %{family_name}', family_name: @family.name) %>
<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 text-gray-700 px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('family_memberships.index.back_to_family', default: '← Back to 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 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<%= t('family_memberships.index.members_count', default: 'All Members (%{count})', count: @members.count) %>
<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">
<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">
<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 flex items-center justify-center">
<span class="text-sm font-medium text-gray-700">
<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>
@ -43,35 +43,35 @@
<div class="min-w-0 flex-1">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium text-gray-900 truncate">
<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 text-yellow-800">
<%= t('family_memberships.index.owner', default: '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 text-gray-800">
<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 text-blue-800">
<%= t('family_memberships.index.you', default: 'You') %>
<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">
<%= t('family_memberships.index.joined', default: 'Joined %{date}', date: member.family_membership.created_at.strftime('%B %d, %Y')) %>
<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">
<%= t('family_memberships.index.created_family', default: 'Created this family') %>
<p class="text-sm text-gray-500 dark:text-gray-400">
Created this family
</p>
<% end %>
</div>
@ -81,24 +81,18 @@
<!-- 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 text-sm font-medium" do %>
<%= t('family_memberships.index.view', default: 'View') %>
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? %>
<% unless member.family_membership.owner? && @family.members.count > 1 %>
<% if !member.family_membership.owner? %>
<%= link_to family_member_path(@family, member.family_membership),
method: :delete,
confirm: t('family_memberships.index.remove_confirm',
default: 'Are you sure you want to remove %{email} from the family?',
email: member.email),
class: "text-red-600 hover:text-red-800 text-sm font-medium" do %>
<%= t('family_memberships.index.remove', default: 'Remove') %>
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 %>
<% else %>
<span class="text-gray-400 text-sm">
<%= t('family_memberships.index.cannot_remove_owner', default: 'Cannot remove owner') %>
</span>
<% end %>
<% end %>
</div>
@ -110,19 +104,19 @@
<!-- Family Statistics -->
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white shadow rounded-lg p-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" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 truncate">
<%= t('family_memberships.index.total_members', default: 'Total Members') %>
<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">
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= @members.count %>
</dd>
</dl>
@ -130,19 +124,19 @@
</div>
</div>
<div class="bg-white shadow rounded-lg p-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-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 truncate">
<%= t('family_memberships.index.active_members', default: 'Active Members') %>
<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">
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= @members.count %>
</dd>
</dl>
@ -150,19 +144,19 @@
</div>
</div>
<div class="bg-white shadow rounded-lg p-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-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 truncate">
<%= t('family_memberships.index.family_age', default: 'Family Age') %>
<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">
<dd class="text-lg font-medium text-gray-900 dark:text-gray-100">
<%= time_ago_in_words(@family.created_at) %>
</dd>
</dl>
@ -171,4 +165,4 @@
</div>
</div>
</div>
</div>
</div>

View file

@ -1,33 +1,33 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto">
<!-- Header -->
<div class="bg-white shadow rounded-lg p-6 mb-6">
<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 flex items-center justify-center">
<span class="text-xl font-medium text-gray-700">
<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">
<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 text-yellow-800">
<%= t('family_memberships.show.owner', default: 'Family 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 text-gray-800">
<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 text-blue-800">
<%= t('family_memberships.show.you', default: 'You') %>
<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>
@ -36,20 +36,16 @@
<div class="flex space-x-3">
<%= link_to family_members_path(@family),
class: "bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('family_memberships.show.back_to_members', default: '← All Members') %>
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? %>
<% unless @membership.owner? && @family.members.count > 1 %>
<%= link_to family_member_path(@family, @membership),
method: :delete,
confirm: t('family_memberships.show.remove_confirm',
default: 'Are you sure you want to remove %{email} from the family?',
email: @membership.user.email),
class: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('family_memberships.show.remove_member', default: 'Remove Member') %>
<% 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>
@ -59,47 +55,47 @@
<!-- Member Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Basic Information -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">
<%= t('family_memberships.show.basic_info', default: '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">
<%= t('family_memberships.show.email', default: 'Email Address') %>
<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"><%= @membership.user.email %></dd>
<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">
<%= t('family_memberships.show.role', default: 'Family Role') %>
<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">
<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">
- <%= t('family_memberships.show.owner_description', default: 'Can manage family settings and members') %>
<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">
<%= t('family_memberships.show.joined_date', default: 'Joined Date') %>
<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">
<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">
<%= t('family_memberships.show.time_in_family', default: 'Time in Family') %>
<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">
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= time_ago_in_words(@membership.created_at) %>
</dd>
</div>
@ -107,40 +103,40 @@
</div>
<!-- Family Information -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">
<%= t('family_memberships.show.family_info', default: '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">
<%= t('family_memberships.show.family_name', default: 'Family Name') %>
<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"><%= @family.name %></dd>
<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">
<%= t('family_memberships.show.family_creator', default: 'Family Creator') %>
<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"><%= @family.creator.email %></dd>
<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">
<%= t('family_memberships.show.family_created', default: 'Family Created') %>
<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">
<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">
<%= t('family_memberships.show.total_members', default: 'Total Members') %>
<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">
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<%= pluralize(@family.members.count, 'member') %>
</dd>
</div>
@ -151,8 +147,8 @@
<!-- Actions and Warnings -->
<div class="mt-6 space-y-4">
<!-- Owner-specific Warning -->
<% if @membership.owner? && @family.members.count > 1 %>
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<% 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">
@ -160,13 +156,12 @@
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
<%= t('family_memberships.show.owner_warning_title', default: 'Family Owner Protection') %>
<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">
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
<%= t('family_memberships.show.owner_warning_message',
default: 'This member is the family owner and cannot be removed while other members exist. To remove the owner, first remove all other members or transfer ownership.') %>
Family owners cannot remove their own membership. To leave the family, the owner must delete the entire family instead.
</p>
</div>
</div>
@ -176,7 +171,7 @@
<!-- Self-removal Info -->
<% if @membership.user == current_user %>
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<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">
@ -184,19 +179,18 @@
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">
<%= t('family_memberships.show.self_info_title', default: 'Your Membership') %>
<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">
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
<%= t('family_memberships.show.self_info_message',
default: 'This is your own membership. You can leave the family at any time from the family page, unless you are the owner with other members present.') %>
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 font-medium" do %>
<%= t('family_memberships.show.go_to_family', default: 'Go to Family Page →') %>
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>

View file

@ -101,35 +101,30 @@ RSpec.describe 'Family Memberships', type: :request do
end
end
context 'when trying to remove the owner while other members exist' do
context 'when trying to remove the owner' do
it 'does not remove the owner' do
expect do
delete "/families/#{family.id}/members/#{owner_membership.id}"
end.not_to change(FamilyMembership, :count)
end
it 'redirects with error message' do
it 'redirects with error message explaining owners must delete family' do
delete "/families/#{family.id}/members/#{owner_membership.id}"
expect(response).to redirect_to(family_path(family))
follow_redirect!
expect(response.body).to include('Cannot remove family owner while other members exist')
expect(response.body).to include('Family owners cannot remove their own membership. To leave the family, delete it instead.')
end
end
context 'when owner is the only member' do
before { member_membership.destroy! }
it 'prevents owner removal even when they are the only member' do
member_membership.destroy!
it 'allows removing the owner' do
expect do
delete "/families/#{family.id}/members/#{owner_membership.id}"
end.to change(FamilyMembership, :count).by(-1)
end
end.not_to change(FamilyMembership, :count)
it 'redirects with success message' do
user_email = user.email
delete "/families/#{family.id}/members/#{owner_membership.id}"
expect(response).to redirect_to(family_path(family))
expect(flash[:notice]).to include("#{user_email} has been removed from the family")
follow_redirect!
expect(response.body).to include('Family owners cannot remove their own membership')
end
end
@ -210,7 +205,7 @@ RSpec.describe 'Family Memberships', type: :request do
expect(member_user.reload.family).to be_nil
end
it 'prevents removing owner when family has members' do
it 'prevents removing owner regardless of member count' do
# Verify initial state
expect(family.members.count).to eq(2)
expect(user.family_owner?).to be true
@ -224,7 +219,7 @@ RSpec.describe 'Family Memberships', type: :request do
expect(user.reload.family).to eq(family)
end
it 'allows removing owner when they are the only member' do
it 'prevents removing owner even when they are the only member' do
# Remove other member first
member_membership.destroy!
@ -232,12 +227,31 @@ RSpec.describe 'Family Memberships', type: :request do
expect(family.reload.members.count).to eq(1)
expect(family.members).to include(user)
# Remove owner
# Try to remove owner - should be prevented
expect do
delete "/families/#{family.id}/members/#{owner_membership.id}"
end.to change(FamilyMembership, :count).by(-1)
end.not_to change(FamilyMembership, :count)
expect(response).to redirect_to(family_path(family))
expect(user.reload.family).to eq(family)
expect(family.reload).to be_present
end
it 'requires owners to use family deletion to leave the family' do
# This test documents that owners must delete the family to leave
# rather than removing their membership
# Remove other member first
member_membership.destroy!
# Try to remove owner membership - should fail
delete "/families/#{family.id}/members/#{owner_membership.id}"
expect(response).to redirect_to(family_path(family))
expect(flash[:alert]).to include('Family owners cannot remove their own membership')
# Owner must delete the family instead
delete "/families/#{family.id}"
expect(response).to redirect_to(families_path)
expect(user.reload.family).to be_nil
end
end