はじめに
本 Chapter では、顧客情報に関する機能、具体的には「顧客情報の閲覧」・「顧客のステータスの変更」する機能を実装していきます。
管理者側の最後の機能です。
コントローラを作成しよう
コンテナが立ち上がっていることが確認できましたら、ecommerce_web コンテナに入ります。
その後、下記コマンドを実行して、必要なコントローラを作成しましょう。
$ rails g controller admin/customers index show --no-helper
続いて、ルーティングの設定を行います。
config/routes.rb
を開いて下記コードを追加してください。
Rails.application.routes.draw do
devise_for :admins, controllers: {
sessions: 'admin/sessions'
}
devise_for :customers, controllers: {
sessions: 'customer/sessions',
registrations: 'customer/registrations'
}
root to: 'pages#home'
namespace :admin do
root to: 'pages#home'
resources :products, only: %i[index show new create edit update]
resources :orders, only: %i[show update]
+ resources :customers, only: %i[index show update]
end
scope module: :customer do
resources :products, only: %i[index show]
resources :cart_items, only: %i[index create destroy] do
member do
patch 'increase'
patch 'decrease'
end
end
resources :checkouts, only: [:create]
resources :webhooks, only: [:create]
resources :orders, only: %i[index show] do
collection do
get 'success'
end
end
end
get '/up/', to: 'up#index', as: :up
get '/up/databases', to: 'up#databases', as: :up_databases
end
生成されたパスを確認してみましょう。
$ rails routes | grep admin/customers
admin_customers GET /admin/customers(.:format) admin/customers#index
admin_customer GET /admin/customers/:id(.:format) admin/customers#show
PATCH /admin/customers/:id(.:format) admin/customers#update
PUT /admin/customers/:id(.:format) admin/customers#update
index
、show
、update
アクションが定義されていますね。
では次に、コントローラの各アクションの中身を記述していきます。
app/controllers/admin/customers_controller.rb
を開いて下記コードを記述してください。
class Admin::CustomersController < ApplicationController
before_action :authenticate_admin!
before_action :set_customer, only: %i[show update]
def index
@customers = Customer.latest
end
def show; end
def update
@customer.update(customer_params)
redirect_to request.referer, notice: 'Successfully updated customer status'
end
private
def set_customer
@customer = Customer.find(params[:id])
end
def customer_params
params.require(:customer).permit(:status)
end
end
通常の CRUD 処理ですね。
顧客情報を更新する際には、顧客の status のみを編集するため、customer_params
では、status のみを許可しています。
ビューを作成しよう
それでは、次に Tailwind CSS を使ってビューを作成していきましょう。
最初に作成するのは、顧客の一覧画面です。app/views/admin/customers/index.html.erb
を開いて、下記コードを記述してください。
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Customers
</span>
</div>
<div class="flex flex-wrap p-6">
<div class="flex-none w-full max-w-full px-3">
<div class="relative flex flex-col min-w-0 mb-6 break-words bg-white border-0 border-transparent border-solid rounded-2xl bg-clip-border">
<div class="flex-auto px-0 pt-0 pb-2">
<div class="p-0 overflow-x-auto">
<table class="items-center w-full mb-0 align-top border-gray-200 text-slate-500">
<thead class="align-bottom">
<tr>
<th class="px-6 py-3 font-bold text-left uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Name</th>
<th class="px-6 py-3 pl-2 font-bold text-left uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Email</th>
<th class="px-6 py-3 font-bold text-center uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Status</th>
<th class="px-6 py-3 font-bold text-center uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Previous Orders</th>
<th class="px-6 py-3 font-semibold capitalize align-middle bg-transparent border-b border-gray-200 border-solid shadow-none tracking-none whitespace-nowrap text-slate-400"></th>
</tr>
</thead>
<tbody>
<% @customers.each do |customer| %>
<tr>
<td class="p-2 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<div class="flex px-2 py-1">
<div class="flex flex-col justify-center">
<p class="mb-0 leading-normal text-md"><%= customer.name %></p>
</div>
</div>
</td>
<td class="p-2 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<p class="font-semibold leading-tight text-md"><%= customer.email %></p>
</td>
<td class="p-2 leading-normal text-center align-middle bg-transparent border-b text-sm whitespace-nowrap shadow-transparent">
<% if customer.status === "normal" %>
<span class="bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded">Normal</span>
<% elsif customer.status === "withdrawn" %>
<span class="bg-orange-100 text-orange-500 text-xs font-semibold p-2 rounded">Withdrawn</span>
<% else %>
<span class="bg-red-100 text-red-800 text-xs font-semibold p-2 rounded">Banned</span>
<% end %>
</td>
<td class="p-2 text-center align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<span class="font-semibold leading-tight text-md"><%= customer.orders.size %></span>
</td>
<td class="p-2 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<%= link_to admin_customer_path(customer), class: "font-semibold leading-tight text-sm text-slate-400" do %>
Details
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
CustomersController で定義した @customers
を each 文を使って表示していますね。
それでは、http://localhost:8000/admin/customers にアクセスしてみましょう。
上記のような画面が表示されていれば OK です。
続いて、顧客の詳細画面です。app/views/admin/customers/show.html.erb
を開いて、下記コードを記述してください。
<div class="px-6">
<div class="flex justify-start item-start space-y-2 flex-col mb-8">
<h1 class="text-3xl lg:text-4xl font-semibold leading-7 lg:leading-9 text-gray-800">Customer ID #<%= @customer.id %></h1>
<p class="text-base font-medium leading-6 text-gray-600">CreatedAt: <%= l @customer.created_at, format: :short %></p>
<p class="text-base font-medium leading-6 text-gray-600">UpdatedAt: <%= l @customer.updated_at, format: :short %></p>
</div>
<div class="flex flex-col xl:flex-row jusitfy-center w-full xl:space-x-8 space-y-4 md:space-y-6 xl:space-y-0">
<div class="flex flex-col justify-start items-start w-full space-y-4 xl:space-y-8">
<div class="bg-gray-50 w-full h-fit px-4 py-6 md:p-6 xl:p-8">
<h3 class="text-xl font-semibold leading-5 text-gray-800">Customer</h3>
<div class="flex justify-center w-full xl:justify-start items-center space-x-4 py-8 border-b border-gray-200">
<div class="flex justify-start items-start flex-col space-y-2">
<p class="text-base font-semibold leading-4 text-left text-gray-800">
<%= @customer.name %>
</p>
<p class="text-sm leading-5 text-gray-600">
<%= @customer.orders.count %> Previous Orders
</p>
</div>
</div>
<div class="flex justify-center text-gray-800 xl:justify-start items-center space-x-4 py-4 border-b border-gray-200 w-full">
<svg class="w-5" viewBox="0 0 8 6" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0h8v6h-8zm.75 .75v4.5h6.5v-4.5zM0 0l4 3 4-3v1l-4 3-4-3z"/>
</svg>
<p class="text-sm"><%= @customer.email %></p>
</div>
</div>
<div class="flex justify-center flex-col md:flex-row items-stretch w-full space-y-4 md:space-y-0 md:space-x-6 xl:space-x-8">
<div class="px-4 py-6 md:p-6 xl:p-8 w-full bg-gray-50 space-y-6">
<h3 class="text-xl font-semibold leading-5 text-gray-800">Customer Status</h3>
<p>Current Status:
<% if @customer.status === "normal" %>
<span class="bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded">Normal</span>
<% elsif @customer.status === "withdrawn" %>
<span class="bg-orange-100 text-orange-500 text-xs font-semibold p-2 rounded">Withdrawn</span>
<% else %>
<span class="bg-red-100 text-red-800 text-xs font-semibold p-2 rounded">Banned</span>
<% end %>
</p>
<%= form_with model: @customer, url: admin_customer_path(@customer), local: true do |f| %>
<div class="mb-6">
<%= f.select :status, Customer.statuses.keys.to_a, {}, {class: "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 appearance-none"} %>
</div>
<%= f.submit class: "inline-flex w-full items-center justify-center rounded-md bg-indigo-500 p-3 text-white duration-100 ease-in-out hover:bg-indigo-600 focus:outline-none cursor-pointer" %>
<% end %>
</div>
</div>
</div>
<div class="flex flex-col justify-start items-start bg-gray-50 px-4 py-6 md:py-6 md:p-6 xl:p-8 w-full overflow-x-auto">
<h3 class="mb-4 text-xl font-semibold leading-5 text-gray-800">Order History</h3>
<% if @customer.orders.size > 0 %>
<table class="items-center w-full mb-0 align-top border-gray-200 text-slate-500">
<thead class="align-bottom">
<tr>
<th class="px-6 py-3 pl-2 font-bold text-left uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Billings</th>
<th class="px-6 py-3 font-bold text-center uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Status</th>
<th class="px-6 py-3 font-bold text-center uppercase align-middle bg-transparent border-b border-gray-200 shadow-none text-lg border-b-solid tracking-none whitespace-nowrap text-slate-400">Order date</th>
<th class="px-6 py-3 font-semibold capitalize align-middle bg-transparent border-b border-gray-200 border-solid shadow-none tracking-none whitespace-nowrap text-slate-400"></th>
</tr>
</thead>
<tbody>
<% @customer.orders.latest.each do |order| %>
<tr>
<td class="p-3 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<p class="font-semibold leading-tight text-md"><%= number_to_currency(order.billing_amount, unit: "¥", strip_insignificant_zeros: true) %></p>
</td>
<td class="p-3 leading-normal text-center align-middle bg-transparent border-b text-sm whitespace-nowrap shadow-transparent">
<span class="bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded"><%= order.status %></span>
</td>
<td class="p-3 text-center align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<span class="font-semibold leading-tight text-md"><%= l order.created_at, format: :short %></span>
</td>
<td class="p-3 align-middle bg-transparent border-b whitespace-nowrap shadow-transparent">
<%= link_to admin_order_path(order), class: "font-semibold leading-tight text-sm text-slate-400" do %>
Details
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No history</p>
<% end %>
</div>
</div>
</div>
上記コードにより CustomersController で定義した @customer
の情報、顧客のステータスを編集するためのフォームが表示されます。
それでは、http://localhost:8000/admin/customers/1 にアクセスしてみましょう。id には適切な customer の id を指定してください。
上記のような画面が表示されていれば OK です。
試しに顧客のステータスを変更してみましょう。
今回は、「Normal」から「Withdrawn」に変更してみます。
上記のように正常に status が編集されていれば OK です。
顧客一覧画面が完成したので、顧客一覧画面へのリンクが付与されたアイコンをナビゲーションバーに追加します。app/views/layouts/application.html.erb
を開いてください。
注文履歴一覧画面へのリンクが付与されたアイコンの右隣に追加してあげましょう。
<li class='px-4 text-white no-underline'>
<%= link_to admin_root_path do %>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-full p-3 text-white" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<% end %>
</li>
<li class='px-4 text-white no-underline'>
<%= link_to admin_customers_path do %>
<svg style="color: white" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" fill="white"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87" fill="white"></path><path d="M16 3.13a4 4 0 0 1 0 7.75" fill="white"></path>
</svg>
<% end %>
</li>
上記のように、アイコンが追加されれば OK です。
最後に、RuboCop を実行し、必要であればコードを修正しましょう。
$ rubocop
今回は、RuboCop の静的解析を全て PASS したようです。
Inspecting 61 files
.............................................................
61 files inspected, no offenses detected
もしも指摘事項があれば修正してください。
それでは、コミットしておきましょう。
$ git add . && git commit -m "Implement functions related to customer information"
おわりに
お疲れ様でした。
本 Chapter では、管理者側の顧客情報に関する機能を実装しました。
これで、管理者側の機能は全て実装し終わりました 🎉
しかし、パフォーマンス面でやるべきことが残っています。
次の Chapter では、現在発生している 1+N 問題を解消していきます。