From 839120f2f413b0dfddd3c861834a317549e70f33 Mon Sep 17 00:00:00 2001 From: kbe Date: Wed, 3 Sep 2025 01:52:48 +0200 Subject: [PATCH] wip: OrdersController#new --- BACKLOG.md | 8 +- app/controllers/api/v1/events_controller.rb | 1 + app/controllers/orders_controller.rb | 28 +++- app/controllers/pages_controller.rb | 6 +- app/javascript/controllers/index.js | 23 +-- .../ticket_selection_controller.js | 149 ++++++++++-------- app/views/events/show.html.erb | 2 +- app/views/orders/new.html.erb | 71 +++++++++ app/views/pages/home.html.erb | 6 +- config/routes.rb | 8 +- 10 files changed, 206 insertions(+), 96 deletions(-) create mode 100644 app/views/orders/new.html.erb diff --git a/BACKLOG.md b/BACKLOG.md index 7b11eb5..a2e3c4e 100755 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1,8 +1,7 @@ # Project Backlog - - ## 📋 Todo + - [ ] Set up project infrastructure - [ ] Design user interface mockups - [ ] Create user dashboard @@ -20,11 +19,11 @@ - [ ] Deploy to production ## 🚧 Doing -- [x] Add login functionality - +- [ ] refactor: Moving checkout to OrdersController ## ✅ Done + - [x] Initialize git repository - [x] Set up development environment - [x] Create project structure @@ -42,3 +41,4 @@ - [x] Configure environment variables - [x] Create authentication system - [x] Implement user registration +- [x] Add login functionality diff --git a/app/controllers/api/v1/events_controller.rb b/app/controllers/api/v1/events_controller.rb index 074edae..399031a 100755 --- a/app/controllers/api/v1/events_controller.rb +++ b/app/controllers/api/v1/events_controller.rb @@ -62,6 +62,7 @@ module Api def store_cart cart_data = params[:cart] || {} session[:pending_cart] = cart_data + session[:event_id] = @event.id render json: { status: "success", message: "Cart stored successfully" } rescue => e diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index 5e61d2c..2cc0b4c 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -6,7 +6,31 @@ class OrdersController < ApplicationController before_action :authenticate_user! before_action :set_order, only: [:show, :checkout, :retry_payment, :increment_payment_attempt] + # Display new order form + # + # On this page user can complete the tickets details (first name and last name), + # add a comment on the order. + def new + @cart_data = session[:pending_cart] || {} + + if @cart_data.empty? + redirect_to root_path, alert: "Veuillez d'abord sĂ©lectionner vos billets" + return + end + + # Build order preview from cart data + @event_id = session[:event_id] + if @event_id.present? + @event = Event.includes(:ticket_types).find_by(id: @event_id) + redirect_to root_path, alert: "ÉvĂ©nement non trouvĂ©" unless @event + else + redirect_to root_path, alert: "Informations manquantes" + end + end + # Display order summary + # + # def show @tickets = @order.tickets.includes(:ticket_type) end @@ -115,7 +139,7 @@ class OrdersController < ApplicationController if order_id.present? order = current_user.orders.find_by(id: order_id, status: "draft") - + if order&.can_retry_payment? redirect_to order_checkout_path(order), alert: "Le paiement a Ă©tĂ© annulĂ©. Vous pouvez rĂ©essayer." @@ -163,4 +187,4 @@ class OrdersController < ApplicationController } ) end -end \ No newline at end of file +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index e879142..38dca2d 100755 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,13 +1,13 @@ # Controller for static pages and user dashboard # Handles basic page rendering and user-specific content class PagesController < ApplicationController - # Skip authentication for public pages - # skip_before_action :authenticate_user!, only: [ :home ] before_action :authenticate_user!, only: [ :dashboard ] # Homepage showing featured events + # + # Display homepage with featured events and incoming ones def home - @events = Event.published.featured.limit(3) + @featured_events = Event.published.featured.limit(3) if user_signed_in? redirect_to(dashboard_path) diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 7712b3d..d6b17ff 100755 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -2,32 +2,25 @@ // Run that command whenever you add a new controller or create them with // ./bin/rails generate stimulus controllerName -import { application } from "./application" +import { application } from "./application"; import LogoutController from "./logout_controller"; application.register("logout", LogoutController); -import CounterController from "./counter_controller" +import CounterController from "./counter_controller"; application.register("counter", CounterController); -import FlashMessageController from "./flash_message_controller" +import FlashMessageController from "./flash_message_controller"; application.register("flash-message", FlashMessageController); -import TicketSelectionController from "./ticket_selection_controller" -application.register("ticket-selection", TicketSelectionController); +import TicketSelectionController from "./ticket_selection_controller"; +// application.register("ticket-selection", TicketSelectionController); -import HeaderController from "./header_controller" +import HeaderController from "./header_controller"; application.register("header", HeaderController); -import EventFormController from "./event_form_controller" +import EventFormController from "./event_form_controller"; application.register("event-form", EventFormController); -import TicketTypeFormController from "./ticket_type_form_controller" +import TicketTypeFormController from "./ticket_type_form_controller"; application.register("ticket-type-form", TicketTypeFormController); - - - - - - - diff --git a/app/javascript/controllers/ticket_selection_controller.js b/app/javascript/controllers/ticket_selection_controller.js index b041a2a..b0b83c0 100644 --- a/app/javascript/controllers/ticket_selection_controller.js +++ b/app/javascript/controllers/ticket_selection_controller.js @@ -1,150 +1,169 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; // Controller for handling ticket selection on the event show page // Manages quantity inputs, calculates totals, and enables/disables the checkout button export default class extends Controller { - static targets = ["quantityInput", "totalQuantity", "totalAmount", "checkoutButton", "form"] - static values = { eventSlug: String, eventId: String } + static targets = [ + "quantityInput", + "totalQuantity", + "totalAmount", + "checkoutButton", + "form", + ]; + static values = { eventSlug: String, eventId: String }; // Initialize the controller and update the cart summary connect() { - this.updateCartSummary() - this.bindFormSubmission() + this.updateCartSummary(); + this.bindFormSubmission(); } // Bind form submission to handle cart storage bindFormSubmission() { if (this.hasFormTarget) { - this.formTarget.addEventListener('submit', this.submitCart.bind(this)) + this.formTarget.addEventListener("submit", this.submitCart.bind(this)); } } // Increment the quantity for a specific ticket type increment(event) { - const ticketTypeId = event.currentTarget.dataset.target - const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId) - const value = parseInt(input.value) || 0 - const max = parseInt(input.max) || 0 + const ticketTypeId = event.currentTarget.dataset.target; + const input = this.quantityInputTargets.find( + (input) => input.dataset.target === ticketTypeId, + ); + const value = parseInt(input.value) || 0; + const max = parseInt(input.max) || 0; if (value < max) { - input.value = value + 1 - this.updateCartSummary() + input.value = value + 1; + this.updateCartSummary(); } } // Decrement the quantity for a specific ticket type decrement(event) { - const ticketTypeId = event.currentTarget.dataset.target - const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId) - const value = parseInt(input.value) || 0 + const ticketTypeId = event.currentTarget.dataset.target; + const input = this.quantityInputTargets.find( + (input) => input.dataset.target === ticketTypeId, + ); + const value = parseInt(input.value) || 0; if (value > 0) { - input.value = value - 1 - this.updateCartSummary() + input.value = value - 1; + this.updateCartSummary(); } } // Update quantity when directly edited in the input field updateQuantity(event) { - const input = event.currentTarget - let value = parseInt(input.value) || 0 - const max = parseInt(input.max) || 0 + const input = event.currentTarget; + let value = parseInt(input.value) || 0; + const max = parseInt(input.max) || 0; // Ensure value is within valid range (0 to max available) - if (value < 0) value = 0 - if (value > max) value = max + if (value < 0) value = 0; + if (value > max) value = max; - input.value = value - this.updateCartSummary() + input.value = value; + this.updateCartSummary(); } // Calculate and update the cart summary (total quantity and amount) updateCartSummary() { - let totalQuantity = 0 - let totalAmount = 0 + let totalQuantity = 0; + let totalAmount = 0; // Sum up quantities and calculate total amount - this.quantityInputTargets.forEach(input => { - const quantity = parseInt(input.value) || 0 - const price = parseInt(input.dataset.price) || 0 + this.quantityInputTargets.forEach((input) => { + const quantity = parseInt(input.value) || 0; + const price = parseInt(input.dataset.price) || 0; - totalQuantity += quantity - totalAmount += quantity * price - }) + totalQuantity += quantity; + totalAmount += quantity * price; + }); // Update the displayed total quantity and amount - this.totalQuantityTarget.textContent = totalQuantity - this.totalAmountTarget.textContent = `€${(totalAmount / 100).toFixed(2)}` + this.totalQuantityTarget.textContent = totalQuantity; + this.totalAmountTarget.textContent = `€${(totalAmount / 100).toFixed(2)}`; // Enable/disable checkout button based on whether any tickets are selected if (totalQuantity > 0) { - this.checkoutButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed') - this.checkoutButtonTarget.disabled = false + this.checkoutButtonTarget.classList.remove( + "opacity-50", + "cursor-not-allowed", + ); + this.checkoutButtonTarget.disabled = false; } else { - this.checkoutButtonTarget.classList.add('opacity-50', 'cursor-not-allowed') - this.checkoutButtonTarget.disabled = true + this.checkoutButtonTarget.classList.add( + "opacity-50", + "cursor-not-allowed", + ); + this.checkoutButtonTarget.disabled = true; } } // Handle form submission - store cart in session before proceeding async submitCart(event) { - event.preventDefault() + event.preventDefault(); - const cartData = this.buildCartData() + const cartData = this.buildCartData(); if (Object.keys(cartData).length === 0) { - alert('Veuillez sĂ©lectionner au moins un billet') - return + alert("Veuillez sĂ©lectionner au moins un billet"); + return; } try { // Store cart data in session - await this.storeCartInSession(cartData) - - // Redirect to tickets/new page - const ticketNewUrl = `/events/${this.eventSlugValue}.${this.eventIdValue}/tickets/new` - window.location.href = ticketNewUrl + await this.storeCartInSession(cartData); + // Redirect to orders/new page + const OrderNewUrl = `/orders/new`; + window.location.href = OrderNewUrl; } catch (error) { - console.error('Error storing cart:', error) - alert('Une erreur est survenue. Veuillez rĂ©essayer.') + console.error("Error storing cart:", error); + alert("Une erreur est survenue. Veuillez rĂ©essayer."); } } // Build cart data from current form state buildCartData() { - const cartData = {} + const cartData = {}; - this.quantityInputTargets.forEach(input => { - const quantity = parseInt(input.value) || 0 + this.quantityInputTargets.forEach((input) => { + const quantity = parseInt(input.value) || 0; if (quantity > 0) { - const ticketTypeId = input.dataset.target + const ticketTypeId = input.dataset.target; cartData[ticketTypeId] = { - quantity: quantity - } + quantity: quantity, + }; } - }) + }); - return cartData + return cartData; } // Store cart data in session via AJAX async storeCartInSession(cartData) { - const storeCartUrl = `/api/v1/events/${this.eventIdValue}/store_cart` + const storeCartUrl = `/api/v1/events/${this.eventIdValue}/store_cart`; const response = await fetch(storeCartUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + "Content-Type": "application/json", + "X-CSRF-Token": document + .querySelector('meta[name="csrf-token"]') + .getAttribute("content"), }, - body: JSON.stringify({ cart: cartData }) - }) + body: JSON.stringify({ cart: cartData }), + }); if (!response.ok) { - throw new Error(`Failed to store cart data: ${response.status} ${response.statusText}`) + throw new Error( + `Failed to store cart data: ${response.status} ${response.statusText}`, + ); } - return response.json() + return response.json(); } } diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 6b1d7ed..6f3e95d 100755 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -208,7 +208,7 @@
- <%= form_with url: "#", method: :post, id: "checkout_form", local: true, data: { + <%= form_with url: order_new_path, method: :get, id: "checkout_form", local: true, data: { controller: "ticket-selection", ticket_selection_target: "form", ticket_selection_event_slug_value: @event.slug, diff --git a/app/views/orders/new.html.erb b/app/views/orders/new.html.erb new file mode 100644 index 0000000..ff4e2eb --- /dev/null +++ b/app/views/orders/new.html.erb @@ -0,0 +1,71 @@ +
+
+ +
+

Nouvelle Commande

+

Vérifiez vos billets avant de continuer vers le paiement

+
+ + +
+

Résumé de votre commande

+ + <% if @event %> +
+

<%= @event.name %>

+

<%= @event.venue_name %>

+ <% if @event.starts_at %> +

+ <%= @event.starts_at.strftime("%d/%m/%Y Ă  %H:%M") %> +

+ <% end %> +
+ + +
+ <% @cart_data.each do |ticket_type_id, item| %> + <% ticket_type = @event.ticket_types.find_by(id: ticket_type_id) %> + <% if ticket_type && item["quantity"].to_i > 0 %> +
+
+

<%= ticket_type.name %>

+

<%= ticket_type.description if ticket_type.description.present? %>

+
+
+

Quantité: <%= item["quantity"] %>

+

+ <%= number_to_currency(ticket_type.price_cents * item["quantity"].to_i / 100.0, unit: "€", separator: ",", delimiter: " ") %> +

+
+
+ <% end %> + <% end %> +
+ + +
+ <%= link_to ticket_new_path, + class: "inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-xl text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %> + + + + Continuer vers les détails + <% end %> +
+ <% end %> +
+ + +
+ <% if @event %> + <%= link_to event_path(@event.slug, @event), + class: "inline-flex items-center text-purple-600 hover:text-purple-700 font-medium transition-colors" do %> + + + + Retour à l'événement + <% end %> + <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index 9f6979a..d70108c 100755 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -26,7 +26,7 @@
- \ No newline at end of file + diff --git a/config/routes.rb b/config/routes.rb index b35b17b..ea9bc3c 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,6 +39,9 @@ Rails.application.routes.draw do get "events/:slug.:id", to: "events#show", as: "event" # === Orders === + + get "orders/new", to: "orders#new", as: "order_new" + resources :orders, only: [:show] do member do get :checkout @@ -47,13 +50,12 @@ Rails.application.routes.draw do end end - # Order payment routes get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success" get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel" # === Tickets === - get "events/:slug.:id/tickets/new", to: "tickets#new", as: "ticket_new" - post "events/:slug.:id/tickets/create", to: "tickets#create", as: "ticket_create" + get "tickets/new", to: "tickets#new", as: "ticket_new" + post "tickets/create", to: "tickets#create", as: "ticket_create" # Keep these for now but they redirect to order system get "events/:slug.:id/tickets/checkout", to: "tickets#checkout", as: "ticket_checkout"