develop #3
@@ -5,6 +5,9 @@ class ApplicationController < ActionController::Base
|
|||||||
# Ensures that all non-GET requests include a valid authenticity token
|
# Ensures that all non-GET requests include a valid authenticity token
|
||||||
protect_from_forgery with: :exception
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
# Redirect authenticated users to onboarding if not completed
|
||||||
|
before_action :require_onboarding_completion
|
||||||
|
|
||||||
# Restrict access to modern browsers only
|
# Restrict access to modern browsers only
|
||||||
# Requires browsers to support modern web standards:
|
# Requires browsers to support modern web standards:
|
||||||
# - WebP images for better compression
|
# - WebP images for better compression
|
||||||
@@ -14,4 +17,29 @@ class ApplicationController < ActionController::Base
|
|||||||
# - CSS nesting and :has() pseudo-class
|
# - CSS nesting and :has() pseudo-class
|
||||||
# allow_browser versions: :modern
|
# allow_browser versions: :modern
|
||||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_onboarding_completion
|
||||||
|
# Skip onboarding check for these paths
|
||||||
|
return if skip_onboarding_check?
|
||||||
|
|
||||||
|
# Only apply to signed-in users
|
||||||
|
if user_signed_in? && current_user.needs_onboarding?
|
||||||
|
redirect_to onboarding_path unless request.path == onboarding_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_onboarding_check?
|
||||||
|
# Skip for devise controllers (login, signup, password reset, etc.)
|
||||||
|
devise_controller? ||
|
||||||
|
# Skip for onboarding controller itself
|
||||||
|
controller_name == "onboarding" ||
|
||||||
|
# Skip for API endpoints
|
||||||
|
controller_name.start_with?("api/") ||
|
||||||
|
# Skip for health checks
|
||||||
|
controller_name == "rails/health" ||
|
||||||
|
# Skip for home page (when not signed in)
|
||||||
|
(controller_name == "pages" && action_name == "home")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
38
app/controllers/onboarding_controller.rb
Normal file
38
app/controllers/onboarding_controller.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
class OnboardingController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :redirect_if_onboarding_complete, except: [:complete]
|
||||||
|
|
||||||
|
def index
|
||||||
|
# Display the onboarding form
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete
|
||||||
|
if onboarding_params_valid?
|
||||||
|
current_user.update!(onboarding_params)
|
||||||
|
current_user.complete_onboarding!
|
||||||
|
|
||||||
|
flash[:notice] = "Bienvenue sur AperoNight ! Votre profil a été configuré avec succès."
|
||||||
|
redirect_to dashboard_path
|
||||||
|
else
|
||||||
|
flash.now[:alert] = "Veuillez remplir tous les champs requis."
|
||||||
|
render :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def onboarding_params
|
||||||
|
params.require(:user).permit(:first_name, :last_name, :company_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def onboarding_params_valid?
|
||||||
|
onboarding_params[:first_name].present? &&
|
||||||
|
onboarding_params[:last_name].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_if_onboarding_complete
|
||||||
|
if current_user&.onboarding_completed?
|
||||||
|
redirect_to dashboard_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/helpers/onboarding_helper.rb
Normal file
2
app/helpers/onboarding_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module OnboardingHelper
|
||||||
|
end
|
||||||
25
app/javascript/controllers/toggle_section_controller.js
Normal file
25
app/javascript/controllers/toggle_section_controller.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Connects to data-controller="toggle-section"
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["section", "icon"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Ensure the section starts hidden
|
||||||
|
this.sectionTarget.classList.add("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
const isHidden = this.sectionTarget.classList.contains("hidden")
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
// Show the section
|
||||||
|
this.sectionTarget.classList.remove("hidden")
|
||||||
|
this.iconTarget.classList.add("rotate-180")
|
||||||
|
} else {
|
||||||
|
// Hide the section
|
||||||
|
this.sectionTarget.classList.add("hidden")
|
||||||
|
this.iconTarget.classList.remove("rotate-180")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,15 @@ class User < ApplicationRecord
|
|||||||
validates :first_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
validates :first_name, length: { minimum: 2, maximum: 50, allow_blank: true }
|
||||||
validates :company_name, length: { minimum: 2, maximum: 100, allow_blank: true }
|
validates :company_name, length: { minimum: 2, maximum: 100, allow_blank: true }
|
||||||
|
|
||||||
|
# Onboarding methods
|
||||||
|
def needs_onboarding?
|
||||||
|
!onboarding_completed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete_onboarding!
|
||||||
|
update!(onboarding_completed: true)
|
||||||
|
end
|
||||||
|
|
||||||
# Authorization methods
|
# Authorization methods
|
||||||
def can_manage_events?
|
def can_manage_events?
|
||||||
# For now, all authenticated users can manage events
|
# For now, all authenticated users can manage events
|
||||||
|
|||||||
@@ -30,17 +30,19 @@
|
|||||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- User Dropdown Menu -->
|
<!-- User Dropdown Menu -->
|
||||||
<div data-header-target="userMenu" class="hidden absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
<div data-header-target="userMenu" class="hidden absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
||||||
<div class="px-4 py-2 text-sm text-gray-900 border-b border-gray-100">
|
<div class="px-4 py-2 text-sm text-gray-900 border-b border-gray-100">
|
||||||
<div class="font-medium"><%= current_user.first_name || current_user.email %></div>
|
<div class="font-medium"><%= current_user.first_name || current_user.email %></div>
|
||||||
<div class="text-gray-500"><%= current_user.email %></div>
|
<div class="text-gray-500"><%= current_user.email %></div>
|
||||||
</div>
|
</div>
|
||||||
<%= link_to "Profile", edit_user_registration_path,
|
|
||||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
|
||||||
<%= link_to "Reservations", "#",
|
<%= link_to "Reservations", "#",
|
||||||
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||||
|
<div class="border-t border-gray-100">
|
||||||
|
<%= link_to "Sécurité", edit_user_registration_path,
|
||||||
|
class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-200" %>
|
||||||
|
</div>
|
||||||
<div class="border-t border-gray-100">
|
<div class="border-t border-gray-100">
|
||||||
<%= link_to "Sign out", destroy_user_session_path,
|
<%= link_to "Sign out", destroy_user_session_path,
|
||||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||||
@@ -86,11 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 space-y-1">
|
<div class="px-2 space-y-1">
|
||||||
<%= link_to t("header.profile"), edit_user_registration_path,
|
<%= link_to "Réservations", "#",
|
||||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||||
<%= link_to t("header.reservations"), "#",
|
<%= link_to "Sécurité", edit_user_registration_path,
|
||||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||||
<%= link_to t("header.logout"), destroy_user_session_path,
|
<%= link_to "Déconnexion", destroy_user_session_path,
|
||||||
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
data: { controller: "logout", action: "click->logout#signOut", logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
|
||||||
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
class: "block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
143
app/views/onboarding/index.html.erb
Normal file
143
app/views/onboarding/index.html.erb
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||||
|
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="mx-auto w-20 h-20 bg-purple-100 rounded-full flex items-center justify-center mb-6">
|
||||||
|
<svg class="w-10 h-10 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-7 4h12a3 3 0 003-3V7a3 3 0 00-3-3H6a3 3 0 00-3 3v4a3 3 0 003 3z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-3">Bienvenue sur <%= Rails.application.config.app_name %> !</h1>
|
||||||
|
<p class="text-lg text-gray-600 max-w-lg mx-auto">
|
||||||
|
Configurons rapidement votre profil pour personnaliser votre expérience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onboarding Form -->
|
||||||
|
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<%= form_with model: current_user, url: complete_onboarding_path, local: true, method: :post, class: "space-y-6" do |form| %>
|
||||||
|
|
||||||
|
<!-- Progress indicator -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between text-xs text-gray-500 mb-2">
|
||||||
|
<span>Étape 1 sur 1</span>
|
||||||
|
<span>Configuration du profil</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-purple-600 h-2 rounded-full w-full transition-all duration-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Fields -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Personal Information Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||||
|
</svg>
|
||||||
|
Informations personnelles
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- First Name -->
|
||||||
|
<div>
|
||||||
|
<%= form.label :first_name, "Prénom", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :first_name,
|
||||||
|
value: current_user.first_name,
|
||||||
|
class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||||
|
placeholder: "Votre prénom",
|
||||||
|
required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Name -->
|
||||||
|
<div>
|
||||||
|
<%= form.label :last_name, "Nom", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :last_name,
|
||||||
|
value: current_user.last_name,
|
||||||
|
class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||||
|
placeholder: "Votre nom de famille",
|
||||||
|
required: true %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Company Information Section (Optional) -->
|
||||||
|
<div data-controller="toggle-section">
|
||||||
|
<!-- Toggle Button -->
|
||||||
|
<button type="button"
|
||||||
|
data-action="click->toggle-section#toggle"
|
||||||
|
class="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors duration-200 border border-gray-200">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-lg font-semibold text-gray-900">Ajouter des informations d'entreprise</span>
|
||||||
|
<span class="ml-2 text-sm font-normal text-gray-500">(optionnel)</span>
|
||||||
|
</div>
|
||||||
|
<svg data-toggle-section-target="icon" class="w-5 h-5 text-gray-400 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Company Form Section (Hidden by default) -->
|
||||||
|
<div data-toggle-section-target="section" class="mt-4 p-4 bg-white border border-gray-200 rounded-lg">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Informations professionnelles</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :company_name, "Nom de l'entreprise", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :company_name,
|
||||||
|
value: current_user.company_name,
|
||||||
|
class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors",
|
||||||
|
placeholder: "Nom de votre entreprise" %>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
|
Cette information peut être utile si vous organisez des événements professionnels.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="pt-6 border-t border-gray-200">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Vous pourrez modifier ces informations plus tard.
|
||||||
|
</p>
|
||||||
|
<%= form.submit "Finaliser mon profil",
|
||||||
|
class: "w-full px-8 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors cursor-pointer" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benefits Preview -->
|
||||||
|
<div class="mt-8 bg-white rounded-xl shadow-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4 text-center">
|
||||||
|
Après la configuration, vous pourrez :
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="flex items-center p-3 bg-green-50 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-green-800">Réserver des billets</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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>
|
||||||
|
<span class="text-sm font-medium text-blue-800">Gérer vos commandes</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-purple-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-purple-800">Créer des événements</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -31,6 +31,10 @@ Rails.application.routes.draw do
|
|||||||
confirmation: "auth/confirmations" # Custom controller for confirmations
|
confirmation: "auth/confirmations" # Custom controller for confirmations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# === Onboarding ===
|
||||||
|
get "onboarding", to: "onboarding#index", as: "onboarding"
|
||||||
|
post "onboarding", to: "onboarding#complete", as: "complete_onboarding"
|
||||||
|
|
||||||
# === Pages ===
|
# === Pages ===
|
||||||
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
||||||
|
|
||||||
|
|||||||
5
db/migrate/20250908092220_add_onboarding_to_users.rb
Normal file
5
db/migrate/20250908092220_add_onboarding_to_users.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddOnboardingToUsers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :onboarding_completed, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
ActiveRecord::Schema[8.0].define(version: 2025_09_08_092220) do
|
||||||
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
@@ -94,6 +94,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
|||||||
t.string "stripe_customer_id"
|
t.string "stripe_customer_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.boolean "onboarding_completed", default: false, null: false
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|||||||
57
test/controllers/application_controller_onboarding_test.rb
Normal file
57
test/controllers/application_controller_onboarding_test.rb
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ApplicationControllerOnboardingTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@user_without_onboarding = users(:one)
|
||||||
|
@user_without_onboarding.update!(onboarding_completed: false)
|
||||||
|
|
||||||
|
@user_with_onboarding = users(:two)
|
||||||
|
@user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should redirect incomplete users to onboarding from dashboard" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
get dashboard_path
|
||||||
|
assert_redirected_to onboarding_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should allow complete users to access dashboard" do
|
||||||
|
sign_in @user_with_onboarding
|
||||||
|
get dashboard_path
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should redirect incomplete users to onboarding from events" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
get events_path
|
||||||
|
assert_redirected_to onboarding_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should allow complete users to access events" do
|
||||||
|
sign_in @user_with_onboarding
|
||||||
|
get events_path
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not redirect from home page when not signed in" do
|
||||||
|
get root_path
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should redirect signed in incomplete users from home to onboarding" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
get root_path
|
||||||
|
assert_redirected_to dashboard_path # Home redirects to dashboard for signed in users
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not interfere with devise controllers" do
|
||||||
|
get new_user_session_path
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not redirect when already on onboarding page" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
get onboarding_path
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
104
test/controllers/onboarding_controller_test.rb
Normal file
104
test/controllers/onboarding_controller_test.rb
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class OnboardingControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@user_without_onboarding = users(:one)
|
||||||
|
@user_without_onboarding.update!(onboarding_completed: false)
|
||||||
|
|
||||||
|
@user_with_onboarding = users(:two)
|
||||||
|
@user_with_onboarding.update!(onboarding_completed: true, first_name: "John", last_name: "Doe")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should redirect to onboarding when user not signed in" do
|
||||||
|
get onboarding_path
|
||||||
|
assert_redirected_to new_user_session_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show onboarding page for incomplete user" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
get onboarding_path
|
||||||
|
assert_response :success
|
||||||
|
assert_select "h1", "Bienvenue sur AperoNight !"
|
||||||
|
assert_select "form"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should redirect completed user to dashboard" do
|
||||||
|
sign_in @user_with_onboarding
|
||||||
|
get onboarding_path
|
||||||
|
assert_redirected_to dashboard_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should complete onboarding with valid data" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
|
||||||
|
assert_not @user_without_onboarding.onboarding_completed?
|
||||||
|
|
||||||
|
post complete_onboarding_path, params: {
|
||||||
|
user: {
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
company_name: "Test Company"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to dashboard_path
|
||||||
|
follow_redirect!
|
||||||
|
assert_select ".notification", /Bienvenue sur AperoNight/
|
||||||
|
|
||||||
|
@user_without_onboarding.reload
|
||||||
|
assert @user_without_onboarding.onboarding_completed?
|
||||||
|
assert_equal "Jane", @user_without_onboarding.first_name
|
||||||
|
assert_equal "Smith", @user_without_onboarding.last_name
|
||||||
|
assert_equal "Test Company", @user_without_onboarding.company_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should complete onboarding without optional company name" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
|
||||||
|
post complete_onboarding_path, params: {
|
||||||
|
user: {
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Smith",
|
||||||
|
company_name: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to dashboard_path
|
||||||
|
@user_without_onboarding.reload
|
||||||
|
assert @user_without_onboarding.onboarding_completed?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not complete onboarding without required fields" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
|
||||||
|
post complete_onboarding_path, params: {
|
||||||
|
user: {
|
||||||
|
first_name: "",
|
||||||
|
last_name: "Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select ".notification", /Veuillez remplir tous les champs requis/
|
||||||
|
|
||||||
|
@user_without_onboarding.reload
|
||||||
|
assert_not @user_without_onboarding.onboarding_completed?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not complete onboarding without last name" do
|
||||||
|
sign_in @user_without_onboarding
|
||||||
|
|
||||||
|
post complete_onboarding_path, params: {
|
||||||
|
user: {
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select ".notification", /Veuillez remplir tous les champs requis/
|
||||||
|
|
||||||
|
@user_without_onboarding.reload
|
||||||
|
assert_not @user_without_onboarding.onboarding_completed?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,7 +5,10 @@ class OrdersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
@user = User.create!(
|
@user = User.create!(
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
password: "password123",
|
password: "password123",
|
||||||
password_confirmation: "password123"
|
password_confirmation: "password123",
|
||||||
|
onboarding_completed: true,
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "User"
|
||||||
)
|
)
|
||||||
|
|
||||||
@event = Event.create!(
|
@event = Event.create!(
|
||||||
|
|||||||
2
test/fixtures/users.yml
vendored
2
test/fixtures/users.yml
vendored
@@ -5,9 +5,11 @@ one:
|
|||||||
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
|
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
|
||||||
last_name: Trump
|
last_name: Trump
|
||||||
first_name: Donald
|
first_name: Donald
|
||||||
|
onboarding_completed: true
|
||||||
|
|
||||||
two:
|
two:
|
||||||
email: user2@example.com
|
email: user2@example.com
|
||||||
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
|
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
|
||||||
last_name: Obama
|
last_name: Obama
|
||||||
first_name: Barack
|
first_name: Barack
|
||||||
|
onboarding_completed: true
|
||||||
|
|||||||
@@ -63,4 +63,33 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
refute user.valid?, "User with last_name longer than 12 chars should be invalid"
|
refute user.valid?, "User with last_name longer than 12 chars should be invalid"
|
||||||
assert_not_nil user.errors[:last_name], "No validation error for too long last_name"
|
assert_not_nil user.errors[:last_name], "No validation error for too long last_name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Test onboarding functionality
|
||||||
|
test "new users should need onboarding by default" do
|
||||||
|
user = User.new(email: "test@example.com", password: "password123")
|
||||||
|
assert user.needs_onboarding?, "New user should need onboarding"
|
||||||
|
assert_not user.onboarding_completed?, "New user should not have completed onboarding"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should complete onboarding" do
|
||||||
|
user = users(:one)
|
||||||
|
user.update!(onboarding_completed: false)
|
||||||
|
|
||||||
|
assert user.needs_onboarding?, "User should need onboarding initially"
|
||||||
|
|
||||||
|
user.complete_onboarding!
|
||||||
|
|
||||||
|
assert_not user.needs_onboarding?, "User should not need onboarding after completion"
|
||||||
|
assert user.onboarding_completed?, "User should have completed onboarding"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "needs_onboarding? should return correct value" do
|
||||||
|
user = users(:one)
|
||||||
|
|
||||||
|
user.update!(onboarding_completed: false)
|
||||||
|
assert user.needs_onboarding?, "User with false onboarding_completed should need onboarding"
|
||||||
|
|
||||||
|
user.update!(onboarding_completed: true)
|
||||||
|
assert_not user.needs_onboarding?, "User with true onboarding_completed should not need onboarding"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ module ActiveSupport
|
|||||||
fixtures :all
|
fixtures :all
|
||||||
|
|
||||||
# Add more helper methods to be used by all tests here...
|
# Add more helper methods to be used by all tests here...
|
||||||
|
|
||||||
|
# Helper to create users with completed onboarding by default for tests
|
||||||
|
def create_test_user(attributes = {})
|
||||||
|
User.create!({
|
||||||
|
email: "test#{rand(10000)}@example.com",
|
||||||
|
password: "password123",
|
||||||
|
password_confirmation: "password123",
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "User",
|
||||||
|
onboarding_completed: true
|
||||||
|
}.merge(attributes))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user