検索後もソート機能を機能させたい
解決したいこと
Laravelを用いて商品管理システムを作成中です。
現状、検索後のソート機能において問題があります。
検索後のソートの対象が全商品になってしまうので、検索にて絞り込みされた商品のみをソートできるよう改修したいです。
ソートの機能に関しては検索前の商品一覧画面同様にテーブルヘッダーを押下することで対応したカラムにてソートを行え、押下回数によって昇順/降順が切り替えできる(初期表示時はid降順)ようにしたいです。
解決法を教えてください。
発生している問題・エラー
検索後にソート機能において、対象が全商品になってしまいます。
検索後のソート対象は、検索にて絞り込みされた商品のみにしたいです。
該当するソースコード
index.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">商品一覧画面</h1>
{{-- 検索フォーム --}}
<div class="search mt-5">
<!-- 検索フォーム。GETメソッドで、商品一覧のルートにデータを送信 -->
<form id="search-form" action="{{ route('products.search') }}" method="GET" class="row g-3">
<!-- 商品名検索用の入力欄 -->
<div class="col-sm-12 col-md-4">
<input type="text" name="keyword" class="form-control" placeholder="検索キーワード" value="{{ request('keyword') }}">
</div>
<!-- メーカー名検索用の入力欄 -->
<div class="col-sm-12 col-md-4">
<select class="form-select" name="search-company" value="{{ request('searchCompany') }}" placeholder="メーカーを選択">
<option value="" selected>メーカーを選択してください</option>
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->company_name }}</option>
@endforeach
</select>
</div>
<!-- 価格(下限〜上限)検索用の入力欄 -->
<div class="col-sm-12 col-md-4">
<input type="text" name="min_price" class="form-control" placeholder="最小価格" value="{{ request('min_price') }}">
</div>
<div class="col-sm-12 col-md-4">
<input type="text" name="max_price" class="form-control" placeholder="最大価格" value="{{ request('max_price') }}">
</div>
<!-- 在庫数(下限〜上限)検索用の入力欄 -->
<div class="col-sm-12 col-md-4">
<input type="text" name="min_stock" class="form-control" placeholder="最小在庫数" value="{{ request('min_stock') }}">
</div>
<div class="col-sm-12 col-md-4">
<input type="text" name="max_stock" class="form-control" placeholder="最大在庫数" value="{{ request('max_stock') }}">
</div>
<!-- 検索ボタン -->
<div class="col-sm-12 col-md-1">
<button id="search-btn" class="btn btn-outline-secondary" type="button">検索</button>
</div>
</form>
<div id="search-results" class="mt-5">
</div>
</div>
<div class="products mt-5">
<table class="table table-striped">
<thead>
<tr>
<th><a href="{{ route('products.index', ['sort' => 'id', 'order' => $sortColumn == 'id' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">ID</a></th>
<th>商品画像</th>
<th><a href="{{ route('products.index', ['sort' => 'product_name', 'order' => $sortColumn == 'product_name' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">商品名</a></th>
<th><a href="{{ route('products.index', ['sort' => 'price', 'order' => $sortColumn == 'price' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">価格</a></th>
<th><a href="{{ route('products.index', ['sort' => 'stock', 'order' => $sortColumn == 'stock' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">在庫数</th>
<th>メーカー名</th>
<th><a href="{{ route('products.create') }}" class="btn btn-warning">新規登録</a></th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr data-product-id="{{ $product->id }}">
<td>{{ $product->id }}</td>
<td><img src="{{ asset($product->img_path) }}" alt="商品画像" width="100"></td>
<td>{{ $product->product_name }}</td>
<td>{{ $product->price }}</td>
<td>{{ $product->stock }}</td>
<td>{{ $product->company_name }}</td>
</td>
<td>
{{-- 詳細ボタン --}}
<a href="{{ route('products.show', $product->id) }}" class="btn btn-info btn-sm mx-1">詳細</a>
<form method="POST" action="{{ route('products.destroy', $product->id) }}" class="d-inline">
@csrf
@method('DELETE')
{{-- 削除ボタン --}}
<button type="submit" class="btn btn-danger btn-sm mx-1 delete-product" data-product-id="{{ $product->id }}">削除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@section('scripts')
<script>
$(document).ready(function () {
// 商品データを削除するための Ajax リクエスト
$('.delete-product').on('click', 'delete-product', function (event) {
event.preventDefault();
var productId = $(this).data('product-id');
if (confirm("削除しますか?")) {
$.ajax({
type: 'POST',
data:{'_method':'delete'},
url: '/products/' + productId,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function (data) {
// 削除された行を非表示にする
$('tr[data-product-id="' + productId + '"]').hide();
alert('削除しました。');
},
error: function (data) {
alert('削除に失敗しました。');
}
});
}
});
$('#search-btn').on('click', function () {
var formData = $('#search-form').serialize();
// 検索ルートにAjaxリクエストを送信
$.ajax({
type: 'GET',
url: '/products/search',
data: formData,
dataType: 'json',
success: function (data) {
// 検索結果のdivを更新
$('#search-results').empty(); // 既存の内容をクリア
if(data.products.length > 0) {
// データが存在する場合の処理
var newTableHtml = '<table class="table table-striped">' +
'<thead><tr>' +
'<th><a href="{{ route('products.index', ['sort' => 'id', 'order' => $sortColumn == 'id' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">ID</a></th>' +
'<th>商品画像</th>' +
'<th><a href="{{ route('products.index', ['sort' => 'product_name', 'order' => $sortColumn == 'product_name' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">商品名</a></th>' +
'<th><a href="{{ route('products.index', ['sort' => 'price', 'order' => $sortColumn == 'price' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">価格</a></th>' +
'<th><a href="{{ route('products.index', ['sort' => 'stock', 'order' => $sortColumn == 'stock' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">在庫数</a></th>' +
'<th>メーカー名</th>' +
'<th></th>' +
'</tr></thead><tbody>';
$.each(data.products, function (index, product) {
// 適切な方法でデータを表示するための処理
newTableHtml += '<tr>' +
'<td>' + product.id + '</td>' +
'<td><img src="' + product.img_path + '" alt="商品画像" width="100"></td>' +
'<td>' + product.product_name + '</td>' +
'<td>' + product.price + '</td>' +
'<td>' + product.stock + '</td>' +
'<td>' + product.company_name + '</td>' +
'<td>' +
'<a href="/products/show/' + product.id + '" class="btn btn-info btn-sm mx-1">詳細</a>' +
'<form method="POST" action="/products/' + product.id + '" class="d-inline">' +
'@csrf' +
'@method("DELETE")' +
'<button type="submit" class="btn btn-danger btn-sm mx-1 delete-product" data-product-id="' + product.id + '">削除</button>' +
'</form>' +
'</td>' +
'</tr>';
});
newTableHtml += '</tbody></table>';
$('.products').hide();
$('#search-results').html(newTableHtml);
} else {
// データが存在しない場合の処理
$('#search-results').html('<p>該当する商品が見つかりませんでした。</p>');
}
},
error: function (data) {
alert('検索に失敗しました。もう一度お試しください。');
}
});
});
});
</script>
@endsection
search.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">検索結果</h1>
<div class="products mt-5">
<table class="table table-striped">
<thead>
<tr>
<th><a href="{{ route('products.index', ['sort' => 'id', 'order' => $sortColumn == 'id' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">ID</a></th>
<th>商品画像</th>
<th><a href="{{ route('products.index', ['sort' => 'product_name', 'order' => $sortColumn == 'product_name' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">商品名</a></th>
<th><a href="{{ route('products.index', ['sort' => 'price', 'order' => $sortColumn == 'price' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">価格</a></th>
<th><a href="{{ route('products.index', ['sort' => 'stock', 'order' => $sortColumn == 'stock' && $sortOrder == 'asc' ? 'desc' : 'asc']) }}">在庫数</th>
<th>メーカー名</th>
<th><a href="{{ route('products.create') }}" class="btn btn-warning">新規登録</a></th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td><img src="{{ asset($product->img_path) }}" alt="商品画像" width="100"></td>
<td>{{ $product->product_name }}</td>
<td>{{ $product->price }}</td>
<td>{{ $product->stock }}</td>
<td>{{ $product->company_name }}</td>
</td>
<td>
{{-- 詳細ボタン --}}
<a href="{{ route('products.show', $product->id) }}" class="btn btn-info btn-sm mx-1">詳細</a>
<form method="POST" action="{{ route('products.destroy', $product->id) }}" class="d-inline">
@csrf
@method('DELETE')
{{-- 削除ボタン --}}
<button type="submit" class="btn btn-danger btn-sm mx-1 delete-product" data-product-id="{{ $product->id }}">削除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endsection
ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Company;
use App\Http\Requests\ProductRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class ProductController extends Controller
{
// 一覧表示
public function index(Request $request)
{
$keyword = $request->input('keyword');
$searchCompany = $request->input('search-company');
$min_price = $request->input('min_price');
$max_price = $request->input('max_price');
$min_stock = $request->input('min_stock');
$max_stock = $request->input('max_stock');
$sortColumn = $request->input('sort', 'id');
$sortOrder = $request->input('order', 'desc');
$model = new Product;
$products = $model->searchList($keyword, $searchCompany, $min_price, $max_price, $min_stock, $max_stock, $sortColumn, $sortOrder);
$companies = DB::table('companies')->get();
return view('products.index', compact('products', 'companies', 'sortColumn', 'sortOrder'));
}
public function search(Request $request)
{
// リクエストから検索パラメータを抽出
$keyword = $request->input('keyword');
$searchCompany = $request->input('search-company');
$min_price = $request->input('min_price');
$max_price = $request->input('max_price');
$min_stock = $request->input('min_stock');
$max_stock = $request->input('max_stock');
$sortColumn = $request->input('sort', 'id');
$sortOrder = $request->input('order', 'desc');
// 既存の searchList メソッドを使用して検索を実行
$model = new Product;
$products = $model->searchList($keyword, $searchCompany, $min_price, $max_price, $min_stock, $max_stock, $sortColumn, $sortOrder);
// 検索結果を表示するビューを返す
return response()->json(['products' => $products]);
}
// 新規登録画面表示
public function create()
{
$companies = DB::table('companies')->get();
return view('products.create', compact('companies'));
}
// 新規登録処理
public function store(ProductRequest $request)
{
$model = new Product;
DB::beginTransaction();
try {
$image = $request->file('img_path');
if($image) {
$filename = $image->getClientOriginalName();
$image->storeAs('public/images', $filename);
$img_path = 'storage/images/'.$filename;
$model->storeProduct($request, $img_path);
} else {
$model->storeProductNoImg($request);
// $img_path = null;
}
// $model->storeProduct($request, $img_path);
DB::commit();
return redirect()->route('products.create')->with('success', 'Product created successfully');
} catch (\Exception $e) {
DB::rollback();
return redirect()->route('products.create')->with('error', 'Error creating the product');
}
}
// 詳細表示
public function show(Product $product)
{
return view('products.show', compact('product'));
}
// 商品情報編集
public function edit($id)
{
$companies = DB::table('companies')->get();
$model = New Product;
$product = $model->getProductById($id);
return view('products.edit', compact('product', 'companies'));
}
// 商品情報更新処理
public function update(ProductRequest $request, $id)
{
$model = New Product;
DB::beginTransaction();
try {
$image = $request->file('img_path');
if($image) {
$filename = $image->getClientOriginalName();
$image->storeAs('public/images', $filename);
$img_path = 'storage/images/'.$filename;
$model->updateProduct($request, $img_path, $id);
} else {
$model->updateProductNoImg($request, $id);
}
DB::commit();
return redirect(route('products.show', $id))->with('success', 'Product updated successfully');
} catch(Exception $e) {
DB::rollBack();
}
}
// 削除処理
public function destroy($id)
{
DB::beginTransaction();
try {
$model = new Product;
$model->deleteProduct($id);
DB::commit();
} catch(\Exception $e) {
DB::rollBack();
return back();
}
return redirect('/products/index');
}
}
Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use App\Http\Requests\ProductRequest;
class Product extends Model
{
use HasFactory;
protected $table = 'products';
// 検索処理
public function searchList($keyword, $searchCompany, $min_price, $max_price, $min_stock, $max_stock, $sortColumn = 'id', $sortOrder ='desc')
{
// productsテーブルからデータ取得。companiesテーブルをjoin
$query = DB::table('products')
->join('companies', 'products.company_id', '=', 'companies.id')
->select('products.*', 'companies.company_name');
if($keyword) {
$query->where('products.product_name', 'LIKE', "%".$keyword."%");
}
if($searchCompany) {
$query->where('products.company_id', '=', $searchCompany);
}
// 価格下限〜上限
if($min_price) {
$query->where('products.price', '>=', $min_price);
}
if($max_price) {
$query->where('products.price', '>=', $max_price);
}
// 在庫数下限〜上限
if($min_stock) {
$query->where('products.stock', '>=', $min_stock);
}
if($max_stock) {
$query->where('products.stock', '>=', $max_stock);
}
$query->orderBy($sortColumn, $sortOrder);
return $query->get();
}
public function getProductById($id)
{
return $this->findOrFail($id);
}
//バリデーション処理追加
public function storeProduct(ProductRequest $request, $img_path) {
// DB::table('products')->insert([
$this->create([
'product_name' => $request->product_name,
'company_id' => $request->company_id,
'price' => $request->price,
'stock' => $request->stock,
'comment' => $request->comment,
'img_path' => $img_path,
]);
}
// 新規登録処理
public function storeProductNoImg(ProductRequest $request) {
// DB::table('products')->insert([
$this->create([
'product_name' => $request->product_name,
'company_id' => $request->company_id,
'price' => $request->price,
'stock' => $request->stock,
'comment' => $request->comment,
]);
}
// 商品情報編集処理
public function updateProduct(ProductRequest $request, $img_path, $id) {
$this->findOrFail($id)->update([
'product_name' => $request->product_name,
'company_id' => $request->company_id,
'price' => $request->price,
'stock' => $request->stock,
'comment' => $request->comment,
'img_path' => $request->img_path,
]);
}
public function updateProductNoImg(ProductRequest $request, $id) {
$this->findOrFail($id)->update([
'product_name' => $request->product_name,
'company_id' => $request->company_id,
'price' => $request->price,
'stock' => $request->stock,
'comment' => $request->comment,
]);
}
public function deleteProduct($id) {
$product = $this->findOrFail($id);
$product->delete();
}
protected $fillable = [
'product_name',
'price',
'stock',
'company_id',
'comment',
'img_path',
];
public function sales() {
return $this->hasMany(Sale::class);
}
public function company() {
return $this->belongsTo(Company::class);
}
}
web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
// use Illuminate\Support\Fecades\Auth;
// Route::get('/', function () {
// if(Auth::check()) {
// return redirect()->route('products.index');
// } else {
// return redirect()->route('login');
// }
// });
Auth::routes();
// Route::group(['middleware' => 'auth'], function() {
// Route::resource('products', ProductController::class);
// });
Route::get('/', function () {
return view('welcome');
});
Route::get('/products/index', 'App\Http\Controllers\ProductController@index')->name('products.index');
Route::get('/products/create', 'App\Http\Controllers\ProductController@create')->name('products.create');
Route::post('/products/store', 'App\Http\Controllers\ProductController@store')->name('products.store');
Route::get('/products/show/{product}', 'App\Http\Controllers\ProductController@show')->name('products.show');
Route::get('/products/edit/{id}', 'App\Http\Controllers\ProductController@edit')->name('products.edit');
Route::put('/products/edit/{id}', 'App\Http\Controllers\ProductController@update')->name('products.update');
Route::delete('/products/{product}', 'App\Http\Controllers\ProductController@destroy')->name('products.destroy');
Route::get('/products/search', 'App\Http\Controllers\ProductController@search')->name('products.search');
自分で試したこと
chatgptに尋ねて、下記のようなコードで解決できるとのことだったので、下記コードをindex.blade.phpのJavaScriptコードに付け加えました。
// ソートボタンのクリック時に検索結果を使用する処理
$('th a').on('click', function(event) {
event.preventDefault();
var sortColumn = $(this).attr('href').split('sort=')[1].split('&')[0];
var sortOrder = $(this).attr('href').split('order=')[1];
// ソート関数を呼び出して、検索結果をソート
searchResults = sortResults(searchResults, sortColumn, sortOrder);
// ソート後の結果を表示
displaySearchResults(searchResults);
});
// 検索結果を指定の列でソートする関数
function sortResults(results, column, order) {
return results.sort(function (a, b) {
var aValue = a[column];
var bValue = b[column];
if (order === 'asc') {
return aValue.localeCompare(bValue);
} else {
return bValue.localeCompare(aValue);
}
});
}
// ソート後の検索結果を表示する関数
function displaySearchResults(results) {
var newTableHtml = '<table class="table table-striped">' +
'<thead><tr>' +
'<th><a href="#" onclick="sortResults(\'id\', \'' + (sortColumn === 'id' && sortOrder === 'asc' ? 'desc' : 'asc') + '\')">ID</a></th>' +
'<th>商品画像</th>' +
'<th><a href="#" onclick="sortResults(\'product_name\', \'' + (sortColumn === 'product_name' && sortOrder === 'asc' ? 'desc' : 'asc') + '\')">商品名</a></th>' +
'<th><a href="#" onclick="sortResults(\'price\', \'' + (sortColumn === 'price' && sortOrder === 'asc' ? 'desc' : 'asc') + '\')">価格</a></th>' +
'<th><a href="#" onclick="sortResults(\'stock\', \'' + (sortColumn === 'stock' && sortOrder === 'asc' ? 'desc' : 'asc') + '\')">在庫数</a></th>' +
'<th>メーカー名</th>' +
'<th></th>' +
'</tr></thead><tbody>';
$.each(results, function (index, product) {
// 適切な方法でデータを表示するための処理
newTableHtml += '<tr>' +
'<td>' + product.id + '</td>' +
'<td><img src="' + product.img_path + '" alt="商品画像" width="100"></td>' +
'<td>' + product.product_name + '</td>' +
'<td>' + product.price + '</td>' +
'<td>' + product.stock + '</td>' +
'<td>' + product.company_name + '</td>' +
'<td>' +
'<a href="/products/show/' + product.id + '" class="btn btn-info btn-sm mx-1">詳細</a>' +
'<form method="POST" action="/products/' + product.id + '" class="d-inline">' +
'@csrf' +
'@method("DELETE")' +
'<button type="submit" class="btn btn-danger btn-sm mx-1 delete-product" data-product-id="' + product.id + '">削除</button>' +
'</form>' +
'</td>' +
'</tr>';
});
newTableHtml += '</tbody></table>';
// 新しいHTMLを #search-results 要素に追加
$('#search-results').html(newTableHtml);
}
0