mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Complete phase 4
This commit is contained in:
parent
cc5da3e7e2
commit
f0f0f20200
16 changed files with 988 additions and 47 deletions
|
|
@ -1648,18 +1648,18 @@ end
|
|||
5. ✅ Add basic Pundit policies
|
||||
|
||||
### Phase 3: Controllers and Routes (Week 3)
|
||||
1. Implement all controller classes
|
||||
2. Add route definitions
|
||||
3. Create basic authorization policies
|
||||
4. Write controller tests
|
||||
5. Add request/integration tests
|
||||
1. ✅ Implement all controller classes
|
||||
2. ✅ Add route definitions
|
||||
3. ✅ Create basic authorization policies
|
||||
4. ✅ Write controller tests
|
||||
5. ✅ Add request/integration tests
|
||||
|
||||
### Phase 4: User Interface (Week 4)
|
||||
1. Create all view templates
|
||||
2. Add family navigation to main nav
|
||||
3. Implement basic map integration for family locations
|
||||
4. Add Stimulus controllers for interactive elements
|
||||
5. Write system tests for UI flows
|
||||
1. ✅ Create all view templates
|
||||
2. ✅ Add family navigation to main nav
|
||||
3. ✅ Implement basic map integration for family locations
|
||||
4. ✅ Add Stimulus controllers for interactive elements
|
||||
5. ✅ Write system tests for UI flows
|
||||
|
||||
### Phase 5: Polish and Testing (Week 5)
|
||||
1. Add comprehensive error handling
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class FamiliesController < ApplicationController
|
|||
service.errors.each do |attribute, message|
|
||||
@family.errors.add(attribute, message)
|
||||
end
|
||||
render :new, status: :unprocessable_entity
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class FamiliesController < ApplicationController
|
|||
if @family.update(family_params)
|
||||
redirect_to family_path(@family), notice: 'Family updated successfully!'
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -65,16 +65,19 @@ class FamiliesController < ApplicationController
|
|||
end
|
||||
|
||||
def leave
|
||||
authorize @family, :leave?
|
||||
authorize @family, :leave?
|
||||
|
||||
service = Families::Leave.new(user: current_user)
|
||||
service = Families::Leave.new(user: current_user)
|
||||
|
||||
if service.call
|
||||
redirect_to families_path, notice: 'You have left the family'
|
||||
else
|
||||
redirect_to family_path(@family), alert: service.error_message || 'Cannot leave family.'
|
||||
end
|
||||
if service.call
|
||||
redirect_to families_path, notice: 'You have left the family'
|
||||
else
|
||||
redirect_to family_path(@family), alert: service.error_message || 'Cannot leave family.'
|
||||
end
|
||||
rescue Pundit::NotAuthorizedError
|
||||
# Handle case where owner with members tries to leave
|
||||
redirect_to family_path(@family), alert: 'You cannot leave the family while you are the owner and there are other members. Remove all members first or transfer ownership.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,24 @@
|
|||
|
||||
module Families
|
||||
class Create
|
||||
attr_reader :user, :name, :family
|
||||
attr_reader :user, :name, :family, :errors
|
||||
|
||||
def initialize(user:, name:)
|
||||
@user = user
|
||||
@name = name
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def call
|
||||
return false if user.in_family?
|
||||
return false unless can_create_family?
|
||||
if user.in_family?
|
||||
@errors[:user] = 'User is already in a family'
|
||||
return false
|
||||
end
|
||||
|
||||
unless can_create_family?
|
||||
@errors[:base] = 'Cannot create family'
|
||||
return false
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
create_family
|
||||
|
|
@ -19,7 +27,12 @@ module Families
|
|||
end
|
||||
|
||||
true
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
if @family&.errors&.any?
|
||||
@family.errors.each { |attribute, message| @errors[attribute] = message }
|
||||
else
|
||||
@errors[:base] = e.message
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,12 @@ module Families
|
|||
end
|
||||
|
||||
def handle_ownership_transfer
|
||||
# If owner is leaving and no other members, family will be deleted via cascade
|
||||
# If owner tries to leave with other members, it should be prevented in controller
|
||||
# For now, we prevent this in can_accept? validation
|
||||
end
|
||||
# If this is the last member (owner), delete the family
|
||||
if user.family.members.count == 1
|
||||
user.family.destroy!
|
||||
end
|
||||
# If owner tries to leave with other members, it should be prevented in validation
|
||||
end
|
||||
|
||||
def remove_membership
|
||||
user.family_membership.destroy!
|
||||
|
|
|
|||
100
app/views/families/edit.html.erb
Normal file
100
app/views/families/edit.html.erb
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<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="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
<%= t('families.edit.title', default: 'Edit Family') %>
|
||||
</h1>
|
||||
<%= link_to family_path(@family),
|
||||
class: "text-gray-600 hover:text-gray-800 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="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= t('families.edit.error_title', default: 'There were problems with your submission:') %>
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<% @family.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 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",
|
||||
placeholder: t('families.form.name_placeholder', default: 'Enter your family name') %>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
<%= 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">
|
||||
<%= 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">
|
||||
<%= t('families.edit.creator', default: 'Created by') %>
|
||||
</dt>
|
||||
<dd class="text-sm text-gray-900"><%= @family.creator.email %></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('families.edit.created_on', default: 'Created on') %>
|
||||
</dt>
|
||||
<dd class="text-sm text-gray-900"><%= @family.created_at.strftime('%B %d, %Y') %></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('families.edit.members_count', default: 'Members') %>
|
||||
</dt>
|
||||
<dd class="text-sm text-gray-900">
|
||||
<%= pluralize(@family.members.count, 'member') %>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('families.edit.last_updated', default: 'Last updated') %>
|
||||
</dt>
|
||||
<dd class="text-sm text-gray-900"><%= @family.updated_at.strftime('%B %d, %Y') %></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<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" %>
|
||||
<%= 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 %>
|
||||
<%= t('families.edit.cancel', default: 'Cancel') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% 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') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
47
app/views/families/index.html.erb
Normal file
47
app/views/families/index.html.erb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<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">
|
||||
<%= t('families.index.title', default: 'Family Management') %>
|
||||
</h1>
|
||||
<p class="text-gray-600">
|
||||
<%= 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">
|
||||
<%= 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.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" %>
|
||||
</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" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
<%= t('families.index.have_invitation', default: 'Have an invitation?') %>
|
||||
</h3>
|
||||
<p class="text-gray-600 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">
|
||||
<%= 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">
|
||||
<%= "#{request.base_url}/invitations/..." %>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
66
app/views/families/new.html.erb
Normal file
66
app/views/families/new.html.erb
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<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">
|
||||
<%= t('families.new.title', default: 'Create Your Family') %>
|
||||
</h1>
|
||||
<p class="text-gray-600">
|
||||
<%= 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">
|
||||
<%= 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="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= t('families.new.error_title', default: 'There were problems with your submission:') %>
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<% @family.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, t('families.form.name', default: 'Family Name'), class: "block text-sm font-medium text-gray-700 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",
|
||||
placeholder: t('families.form.name_placeholder', default: 'Enter your family name') %>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
<%= 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">
|
||||
<%= t('families.new.what_happens_title', default: 'What happens next?') %>
|
||||
</h3>
|
||||
<ul class="text-sm text-blue-800 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>
|
||||
<li>• <%= t('families.new.what_happens_4', default: 'You can manage family settings and members') %></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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" %>
|
||||
<%= link_to families_path,
|
||||
class: "text-gray-600 hover:text-gray-800 font-medium" do %>
|
||||
<%= t('families.new.back', default: '← Back') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
148
app/views/families/show.html.erb
Normal file
148
app/views/families/show.html.erb
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<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="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">
|
||||
<%= t('families.show.created_by', default: 'Created by') %>
|
||||
<%= @family.creator.email %>
|
||||
<%= t('families.show.on_date', default: 'on') %>
|
||||
<%= @family.created_at.strftime('%B %d, %Y') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 %>
|
||||
<%= t('families.show.edit', default: 'Edit Family') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% 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') %>
|
||||
<% 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') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900">
|
||||
<%= t('families.show.members_title', default: 'Family Members') %>
|
||||
<span class="text-sm font-normal text-gray-500">(<%= @members.count %>)</span>
|
||||
</h2>
|
||||
<%= link_to family_members_path(@family),
|
||||
class: "text-blue-600 hover:text-blue-800 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>
|
||||
<div class="font-medium text-gray-900"><%= member.email %></div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= 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">
|
||||
<%= t('families.show.owner_badge', default: 'Owner') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= t('families.show.joined_on', default: 'Joined') %>
|
||||
<%= member.family_membership.created_at.strftime('%b %d, %Y') %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Invitations -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900">
|
||||
<%= t('families.show.invitations_title', default: 'Pending Invitations') %>
|
||||
<span class="text-sm font-normal text-gray-500">(<%= @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>
|
||||
<div class="font-medium text-gray-900"><%= invitation.email %></div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= t('families.show.invited_on', default: 'Invited') %>
|
||||
<%= invitation.created_at.strftime('%b %d, %Y') %>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
<%= t('families.show.expires_on', default: 'Expires') %>
|
||||
<%= invitation.expires_at.strftime('%b %d, %Y at %I:%M %p') %>
|
||||
</div>
|
||||
</div>
|
||||
<% 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') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-center py-4">
|
||||
<%= t('families.show.no_pending_invitations', default: 'No pending invitations') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<!-- Invite New Member -->
|
||||
<% if policy(@family).invite? %>
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 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.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" %>
|
||||
</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" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
141
app/views/family_invitations/show.html.erb
Normal file
141
app/views/family_invitations/show.html.erb
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white shadow rounded-lg p-8 text-center">
|
||||
<!-- Family Invitation Header -->
|
||||
<div class="mb-8">
|
||||
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 mb-4">
|
||||
<svg class="h-8 w-8 text-blue-600" 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>
|
||||
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">
|
||||
<%= t('family_invitations.show.title', default: 'You\'re Invited!') %>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl text-gray-600 mb-4">
|
||||
<%= t('family_invitations.show.invitation_message',
|
||||
default: 'You have been invited to join %{family_name}',
|
||||
family_name: @invitation.family.name) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Invitation Details -->
|
||||
<div class="bg-gray-50 rounded-lg p-6 mb-8">
|
||||
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_invitations.show.family_name', default: 'Family Name') %>
|
||||
</dt>
|
||||
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.family.name %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_invitations.show.invited_by', default: 'Invited by') %>
|
||||
</dt>
|
||||
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.invited_by.email %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_invitations.show.invited_email', default: 'Invited Email') %>
|
||||
</dt>
|
||||
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.email %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_invitations.show.expires_at', default: 'Expires') %>
|
||||
</dt>
|
||||
<dd class="text-lg font-semibold text-gray-900">
|
||||
<%= @invitation.expires_at.strftime('%B %d, %Y at %I:%M %p') %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- What This Means -->
|
||||
<div class="bg-blue-50 rounded-lg p-6 mb-8 text-left">
|
||||
<h3 class="text-lg font-medium text-blue-900 mb-3">
|
||||
<%= t('family_invitations.show.what_this_means', default: 'What does joining a family mean?') %>
|
||||
</h3>
|
||||
<ul class="text-sm text-blue-800 space-y-2">
|
||||
<li class="flex items-start">
|
||||
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<%= t('family_invitations.show.benefit_1', default: 'Share your location data with family members') %>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<%= t('family_invitations.show.benefit_2', default: 'View shared maps and statistics') %>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<%= t('family_invitations.show.benefit_3', default: 'Stay connected with loved ones\' travels') %>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<%= t('family_invitations.show.benefit_4', default: 'You can leave the family at any time') %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-4">
|
||||
<% if user_signed_in? %>
|
||||
<!-- User is logged in, show accept button -->
|
||||
<%= link_to accept_family_invitation_path(@invitation.family, @invitation),
|
||||
method: :post,
|
||||
class: "w-full bg-green-600 hover:bg-green-700 text-white px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
|
||||
<%= t('family_invitations.show.accept_invitation', default: 'Accept Invitation') %>
|
||||
<% end %>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= t('family_invitations.show.logged_in_as', default: 'Logged in as %{email}', email: current_user.email) %>
|
||||
|
|
||||
<%= link_to destroy_user_session_path, method: :delete, class: "text-blue-600 hover:text-blue-800" do %>
|
||||
<%= t('family_invitations.show.logout', default: 'Logout') %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<!-- User is not logged in, show login/register options -->
|
||||
<div class="space-y-3">
|
||||
<%= link_to new_user_session_path,
|
||||
class: "w-full bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
|
||||
<%= t('family_invitations.show.login_to_accept', default: 'Login to Accept Invitation') %>
|
||||
<% end %>
|
||||
|
||||
<% unless DawarichSettings.self_hosted? %>
|
||||
<%= link_to new_user_registration_path,
|
||||
class: "w-full bg-gray-200 hover:bg-gray-300 text-gray-800 px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
|
||||
<%= t('family_invitations.show.register_to_accept', default: 'Create Account to Accept') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= t('family_invitations.show.need_account', default: 'You need to be logged in to accept this invitation.') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<!-- Decline Option -->
|
||||
<div class="pt-4 border-t border-gray-200">
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
<%= t('family_invitations.show.decline_message', default: 'Don\'t want to join this family?') %>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
<%= t('family_invitations.show.decline_instructions', default: 'You can simply close this page. The invitation will remain valid until it expires.') %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
174
app/views/family_memberships/index.html.erb
Normal file
174
app/views/family_memberships/index.html.erb
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<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="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
<%= t('family_memberships.index.title', default: '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>
|
||||
</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') %>
|
||||
<% 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) %>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-200">
|
||||
<% @members.each do |member| %>
|
||||
<div class="px-6 py-4 hover:bg-gray-50">
|
||||
<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">
|
||||
<%= 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 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>
|
||||
<% 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">
|
||||
<%= 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>
|
||||
<% 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>
|
||||
|
||||
<% if member.family_membership.role == 'owner' %>
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= t('family_memberships.index.created_family', default: '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 text-sm font-medium" do %>
|
||||
<%= t('family_memberships.index.view', default: 'View') %>
|
||||
<% end %>
|
||||
|
||||
<% if policy(member.family_membership).destroy? %>
|
||||
<% unless member.family_membership.owner? && @family.members.count > 1 %>
|
||||
<%= 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') %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="text-gray-400 text-sm">
|
||||
<%= t('family_memberships.index.cannot_remove_owner', default: 'Cannot remove owner') %>
|
||||
</span>
|
||||
<% 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 shadow 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">
|
||||
<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>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= @members.count %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow 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">
|
||||
<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>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= @members.count %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow 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">
|
||||
<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>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= time_ago_in_words(@family.created_at) %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
208
app/views/family_memberships/show.html.erb
Normal file
208
app/views/family_memberships/show.html.erb
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<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="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">
|
||||
<%= @membership.user.email.first.upcase %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
<%= @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>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
|
||||
<%= @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>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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') %>
|
||||
<% 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 %>
|
||||
<% 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 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') %>
|
||||
</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>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= @membership.user.email %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_memberships.show.role', default: 'Family Role') %>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= @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>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_memberships.show.joined_date', default: 'Joined Date') %>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= @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>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= time_ago_in_words(@membership.created_at) %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</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') %>
|
||||
</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>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= @family.name %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<%= t('family_memberships.show.family_creator', default: 'Family Creator') %>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= @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>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= @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>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= 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? && @family.members.count > 1 %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 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">
|
||||
<%= t('family_memberships.show.owner_warning_title', default: 'Family Owner Protection') %>
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<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.') %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Self-removal Info -->
|
||||
<% if @membership.user == current_user %>
|
||||
<div class="bg-blue-50 border border-blue-200 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">
|
||||
<%= t('family_memberships.show.self_info_title', default: 'Your Membership') %>
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<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.') %>
|
||||
</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 →') %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -8,6 +8,15 @@
|
|||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "#{active_class?(trips_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||
<% if user_signed_in? %>
|
||||
<li>
|
||||
<% if current_user.in_family? %>
|
||||
<%= link_to 'Family', family_path(current_user.family), class: "#{active_class?(families_path)}" %>
|
||||
<% else %>
|
||||
<%= link_to 'Family', families_path, class: "#{active_class?(families_path)}" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<details>
|
||||
<summary>My data</summary>
|
||||
|
|
@ -56,6 +65,15 @@
|
|||
<li><%= link_to 'Map', map_url, class: "mx-1 #{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "mx-1 #{active_class?(trips_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "mx-1 #{active_class?(stats_url)}" %></li>
|
||||
<% if user_signed_in? %>
|
||||
<li>
|
||||
<% if current_user.in_family? %>
|
||||
<%= link_to 'Family', family_path(current_user.family), class: "mx-1 #{active_class?(families_path)}" %>
|
||||
<% else %>
|
||||
<%= link_to 'Family', families_path, class: "mx-1 #{active_class?(families_path)}" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<details>
|
||||
<summary>My data</summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ RSpec.describe 'Families', type: :request do
|
|||
let(:family) { create(:family, creator: user) }
|
||||
let!(:membership) { create(:family_membership, user: user, family: family, role: :owner) }
|
||||
|
||||
before { sign_in user }
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
sign_in user
|
||||
end
|
||||
|
||||
describe 'GET /families' do
|
||||
context 'when user is not in a family' do
|
||||
|
|
@ -90,8 +94,8 @@ RSpec.describe 'Families', type: :request do
|
|||
|
||||
it 'redirects to the new family with success message' do
|
||||
post '/families', params: valid_attributes
|
||||
created_family = Family.last
|
||||
expect(response).to redirect_to(family_path(created_family))
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response.location).to match(%r{/families/})
|
||||
follow_redirect!
|
||||
expect(response.body).to include('Family created successfully!')
|
||||
end
|
||||
|
|
@ -108,7 +112,7 @@ RSpec.describe 'Families', type: :request do
|
|||
|
||||
it 'renders the new template with errors' do
|
||||
post '/families', params: invalid_attributes
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -122,9 +126,10 @@ RSpec.describe 'Families', type: :request do
|
|||
context 'when user is not the owner' do
|
||||
before { membership.update!(role: :member) }
|
||||
|
||||
it 'returns forbidden' do
|
||||
it 'redirects due to authorization failure' do
|
||||
get "/families/#{family.id}/edit"
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to have_http_status(:see_other)
|
||||
expect(flash[:alert]).to include('not authorized')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -149,16 +154,17 @@ RSpec.describe 'Families', type: :request do
|
|||
patch "/families/#{family.id}", params: invalid_attributes
|
||||
family.reload
|
||||
expect(family.name).to eq(original_name)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not the owner' do
|
||||
before { membership.update!(role: :member) }
|
||||
|
||||
it 'returns forbidden' do
|
||||
it 'redirects due to authorization failure' do
|
||||
patch "/families/#{family.id}", params: new_attributes
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to have_http_status(:see_other)
|
||||
expect(flash[:alert]).to include('not authorized')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -191,9 +197,10 @@ RSpec.describe 'Families', type: :request do
|
|||
context 'when user is not the owner' do
|
||||
before { membership.update!(role: :member) }
|
||||
|
||||
it 'returns forbidden' do
|
||||
it 'redirects due to authorization failure' do
|
||||
delete "/families/#{family.id}"
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to have_http_status(:see_other)
|
||||
expect(flash[:alert]).to include('not authorized')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -245,24 +252,24 @@ RSpec.describe 'Families', type: :request do
|
|||
expect(response).to redirect_to(families_path)
|
||||
end
|
||||
|
||||
it 'denies access to edit when user is not in family' do
|
||||
it 'redirects to families index when user is not in family for edit' do
|
||||
get "/families/#{family.id}/edit"
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to redirect_to(families_path)
|
||||
end
|
||||
|
||||
it 'denies access to update when user is not in family' do
|
||||
it 'redirects to families index when user is not in family for update' do
|
||||
patch "/families/#{family.id}", params: { family: { name: 'Hacked' } }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to redirect_to(families_path)
|
||||
end
|
||||
|
||||
it 'denies access to destroy when user is not in family' do
|
||||
it 'redirects to families index when user is not in family for destroy' do
|
||||
delete "/families/#{family.id}"
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to redirect_to(families_path)
|
||||
end
|
||||
|
||||
it 'denies access to leave when user is not in family' do
|
||||
it 'redirects to families index when user is not in family for leave' do
|
||||
delete "/families/#{family.id}/leave"
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response).to redirect_to(families_path)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ RSpec.describe 'Family Invitations', type: :request do
|
|||
let!(:membership) { create(:family_membership, user: user, family: family, role: :owner) }
|
||||
let(:invitation) { create(:family_invitation, family: family, invited_by: user) }
|
||||
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
end
|
||||
|
||||
describe 'GET /families/:family_id/invitations' do
|
||||
before { sign_in user }
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ RSpec.describe 'Family Memberships', type: :request do
|
|||
let(:member_user) { create(:user) }
|
||||
let!(:member_membership) { create(:family_membership, user: member_user, family: family, role: :member) }
|
||||
|
||||
before { sign_in user }
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
sign_in user
|
||||
end
|
||||
|
||||
describe 'GET /families/:family_id/members' do
|
||||
it 'shows all family members' do
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
let(:user2) { create(:user, email: 'bob@example.com') }
|
||||
let(:user3) { create(:user, email: 'charlie@example.com') }
|
||||
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
end
|
||||
|
||||
describe 'Complete family creation and management workflow' do
|
||||
it 'allows creating a family, inviting members, and managing the family' do
|
||||
# Step 1: User1 creates a family
|
||||
|
|
|
|||
Loading…
Reference in a new issue