Compare commits

...

8 Commits

Author SHA1 Message Date
33b4b3f69d Merge remote-tracking branch 'origin/main' into main_watanabe
All checks were successful
Deploy preview (main_watanabe) / deploy (push) Successful in 14s
2026-02-02 17:26:40 +09:00
fd5b28497d Merge pull request '駐輪場選択画面コメント修正' (#64) from main_higashide into main
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 23s
Reviewed-on: #64
2026-01-30 19:08:14 +09:00
92f48ecc0d 駐輪場選択画面コメント修正 2026-01-30 19:07:42 +09:00
0f683e1555 Merge pull request '駐輪場選択画面修正' (#63) from main_higashide into main
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 24s
Reviewed-on: #63
2026-01-30 16:38:21 +09:00
9908d3e5b7 一般側ルート修正に伴い修正 2026-01-30 16:37:27 +09:00
637312661e モーダルのフォーカス修正 2026-01-30 16:30:28 +09:00
c1a9fc80c8 駐輪場選択画面修正 2026-01-30 16:29:38 +09:00
3dc376b121 Merge pull request '1/29 一般資材改修' (#62) from main_watanabe into main
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 26s
Reviewed-on: #62
2026-01-29 17:05:05 +09:00
9 changed files with 768 additions and 368 deletions

View File

@ -2,35 +2,164 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Traits\AuthenticatesUser;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class ParkDetailController extends Controller class ParkDetailController extends Controller
{ {
public function show($park_id) use AuthenticatesUser;
{
$park = DB::table('park')->where('park_id', $park_id)->first();
$zones = DB::table('zone') /**
* 駐輪場詳細情報を取得Ajax用
*
* @param string $management_code
* @param int $park_id
* @return \Illuminate\Http\JsonResponse
*/
public function show($management_code, $park_id)
{
if ($redirect = $this->handleSessionExpired('駐輪場詳細', false)) {
return $redirect;
}
$userId = session('user_id');
$management = session('management');
if (!$management) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 運営元情報取得失敗 user_id=" . $userId);
return response()->json(['error' => 'Management not found'], 404);
}
// マルチテナント対応:運営元に紐づく駐輪場のみ取得
$park = $this->getParkInfo($park_id, $management->management_id);
if (!$park) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場情報取得失敗 park_id=" . $park_id . ", management_id=" . $management->management_id . ", user_id=" . $userId);
return response()->json(['error' => 'Park not found'], 404);
}
// ゾーン情報取得
$zones = $this->getZones($park_id);
// 予約情報取得
$reserves = $this->getReserves($park_id);
// 車種別の基準台数集計
$zoneStandardSum = $this->calculateZoneStandardSum($zones);
// 車種別の空き台数計算
$vacancyData = $this->calculateVacancy($zones, $reserves);
// 駐輪場ごとの更新可能期間取得
$parkGracePeriod = $this->getParkGracePeriod($park_id);
\Log::info("[INFO] " . now()->format('Y-m-d H:i:s') . " 駐輪場詳細取得成功 park_id=" . $park_id . ", management_id=" . $management->management_id . ", user_id=" . $userId);
try {
$html = view('regular_contract.park_detail', compact(
'park',
'zones',
'reserves',
'zoneStandardSum',
'vacancyData',
'parkGracePeriod'
))->render();
\Log::info("[INFO] " . now()->format('Y-m-d H:i:s') . " ビュー生成成功 park_id=" . $park_id . ", HTML長=" . strlen($html));
return response()->json(['html' => $html]);
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " ビュー生成失敗 park_id=" . $park_id . ", error=" . $e->getMessage());
return response()->json(['error' => 'View rendering failed'], 500);
}
}
/**
* 駐輪場情報取得(マルチテナントフィルタ適用)
*
* 運営元に紐づく駐輪場のみを取得することで、他の運営元のデータが
* 誤って表示されることを防ぐ。
*
* @param int $parkId
* @param int $managementId
* @return object|null
*/
private function getParkInfo(int $parkId, int $managementId)
{
try {
return DB::table('park')
->where('park_id', $parkId)
->where('management_id', $managementId)
->first();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場情報取得エラー park_id=" . $parkId . ", error=" . $e->getMessage());
return null;
}
}
/**
* ゾーン情報取得
*
* @param int $parkId
* @return \Illuminate\Support\Collection
*/
private function getZones(int $parkId)
{
try {
return DB::table('zone')
->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id') ->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id')
->leftJoin('ptype', 'zone.ptype_id', '=', 'ptype.ptype_id') ->leftJoin('ptype', 'zone.ptype_id', '=', 'ptype.ptype_id')
->select('zone.*', 'psection.psection_subject', 'ptype.ptype_subject') ->select(
->where('zone.park_id', $park_id) 'zone.*',
'psection.psection_subject',
'ptype.ptype_subject'
)
->where('zone.park_id', $parkId)
->get(); ->get();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " zone情報取得失敗 park_id=" . $parkId . ", error=" . $e->getMessage());
return collect();
}
}
$reserves = DB::table('reserve') /**
* 予約情報取得
*
* @param int $parkId
* @return \Illuminate\Support\Collection
*/
private function getReserves(int $parkId)
{
try {
return DB::table('reserve')
->join('zone', function ($join) { ->join('zone', function ($join) {
$join->on('reserve.psection_id', '=', 'zone.psection_id') $join->on('reserve.psection_id', '=', 'zone.psection_id')
->on('reserve.ptype_id', '=', 'zone.ptype_id'); ->on('reserve.ptype_id', '=', 'zone.ptype_id');
}) })
->join('ptype', 'zone.ptype_id', '=', 'ptype.ptype_id') ->join('ptype', 'zone.ptype_id', '=', 'ptype.ptype_id')
->where('reserve.park_id', $park_id) ->where('reserve.park_id', $parkId)
->where('reserve.valid_flag', 1) ->where('reserve.valid_flag', 1)
->select('reserve.*', 'ptype.ptype_subject') ->select('reserve.*', 'ptype.ptype_subject')
->get(); ->get();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " reserve情報取得失敗 park_id=" . $parkId . ", error=" . $e->getMessage());
return collect();
}
}
/**
* 車種別の基準台数集計
*
* 駐輪場の車種別定員zone_standard の合計)を算出する。
*
* @param \Illuminate\Support\Collection $zones
* @return array
*/
private function calculateZoneStandardSum($zones)
{
$zoneStandardSum = []; $zoneStandardSum = [];
foreach ($zones as $zone) { foreach ($zones as $zone) {
$key = $zone->psection_subject; // 「自転車」「原付」など $key = $zone->psection_subject; // 「自転車」「原付」など
if (!isset($zoneStandardSum[$key])) { if (!isset($zoneStandardSum[$key])) {
@ -39,14 +168,29 @@ class ParkDetailController extends Controller
$zoneStandardSum[$key] += $zone->zone_standard; $zoneStandardSum[$key] += $zone->zone_standard;
} }
// 空き台数集計用配列 return $zoneStandardSum;
}
/**
* 車種別の空き台数計算
*
* 予約を考慮した実際の空き台数を算出する。
* 許容台数から現在台数を引き、さらに有効な予約件数分を減算することで、正確な空き状況を把握する。
*
* @param \Illuminate\Support\Collection $zones
* @param \Illuminate\Support\Collection $reserves
* @return array
*/
private function calculateVacancy($zones, $reserves)
{
$vacancyData = []; $vacancyData = [];
// zone_tolerance - zone_number を集計
foreach ($zones as $zone) { foreach ($zones as $zone) {
$key = $zone->psection_id . '_' . $zone->ptype_subject; $key = $zone->psection_id . '_' . $zone->ptype_subject;
if (!isset($vacancyData[$key])) { if (!isset($vacancyData[$key])) {
$vacancyData[$key] = 0; $vacancyData[$key] = 0;
} }
// zone_tolerance - zone_number を合計
$vacancyData[$key] += ($zone->zone_tolerance - $zone->zone_number); $vacancyData[$key] += ($zone->zone_tolerance - $zone->zone_number);
} }
@ -54,23 +198,36 @@ class ParkDetailController extends Controller
foreach ($reserves as $reserve) { foreach ($reserves as $reserve) {
$key = $reserve->psection_id . '_' . $reserve->ptype_subject; $key = $reserve->psection_id . '_' . $reserve->ptype_subject;
if (isset($vacancyData[$key])) { if (isset($vacancyData[$key])) {
$vacancyData[$key] -= 1; // 1件分減算 $vacancyData[$key] -= 1;
} }
} }
// 更新期間取得 return $vacancyData;
$city_grace_periods = DB::table('city') }
->select('city_id', 'update_grace_period_start_date', 'update_grace_period_start_time', 'update_grace_period_end_date', 'update_grace_period_end_time')
->whereIn('city_id', function ($query) {
$query->select('city_id')->from('park');
})
->get()
->keyBy('city_id');
// 必要なら他テーブルJOINや追加情報も取得 /**
return response()->json([ * 駐輪場の更新可能期間取得
'html' => view('regular_contract.park_detail', compact('park', 'zones', 'reserves', 'zoneStandardSum', 'vacancyData', 'city_grace_periods'))->render() *
]); * @param int $parkId
* @return object|null
*/
private function getParkGracePeriod(int $parkId)
{
try {
return DB::table('park')
->select(
'park_id',
'update_grace_period_start_date',
'update_grace_period_start_time',
'update_grace_period_end_date',
'update_grace_period_end_time'
)
->where('park_id', $parkId)
->first();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場更新可能期間取得失敗 park_id=" . $parkId . ", error=" . $e->getMessage());
return null;
}
} }
public function showWait($reserve_id) public function showWait($reserve_id)

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Traits\AuthenticatesUser;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
@ -16,50 +17,175 @@ use Carbon\Carbon;
class RegularContractCreateController extends Controller class RegularContractCreateController extends Controller
{ {
// 新規作成画面表示 use AuthenticatesUser;
/**
* 駐輪場選択画面を表示
*
* @param Request $request
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse
*/
public function show(Request $request) public function show(Request $request)
{ {
$user_id = session('user_id'); if ($redirect = $this->handleSessionExpired('駐輪場選択')) {
if (!$user_id) { return $redirect;
return redirect('/login');
} }
$user = DB::table('user')->where('user_id', $user_id)->first();
// 市町村名park→city JOINで重複排除 $userId = session('user_id');
$cities = DB::table('park') $user = DB::table('user')->where('user_id', $userId)->first();
// マルチテナント対応のため、運営元情報をセッションから取得
$management = session('management');
if (!$management) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 運営元情報が見つからない user_id=" . $userId);
abort(404);
}
// 検索条件・ページング取得
$cityId = $request->input('city_id');
$stationName = $request->input('station_neighbor_station');
$parkId = $request->input('park_id');
$page = (int)$request->input('page', 1);
$perPage = 10;
// ルート名で画面種別判定(新規定期契約 or 駐輪場検索)
$isRegularContract = $request->route()->getName() === 'regular_contract.create';
$activeMenu = $isRegularContract ? 'SWC-8-1' : 'SWC-10-1';
// マスタデータ取得(運営元フィルタ適用)
$cities = $this->getCitiesList($management->management_id);
$stations = $this->getStationsList($management->management_id);
$parks = $this->getParksList($management->management_id);
// 運営元が取り扱う車種一覧を取得
$availableVehicles = $this->getAvailableVehicles($management->management_id);
// 駐輪場テーブルデータ取得(検索・ページング、運営元フィルタ適用)
$parksTableData = $this->getParksTableData($management->management_id, $cityId, $stationName, $parkId, $page, $perPage);
// zone・reserve マスタ取得(運営元フィルタ適用)
$zones = $this->getZonesByParkId($management->management_id);
$reserve = $this->getReserveByParkId($management->management_id);
// 駐輪場ごとの更新可能期間取得(運営元フィルタ適用)
$parkGracePeriods = $this->getParkGracePeriods($management->management_id);
// 駐輪場ごと・車種ごとの状態を事前計算
$parksTableData['data'] = $this->calculateVehicleStatus(
$parksTableData['data'],
$zones,
$reserve,
$parkGracePeriods,
$availableVehicles
);
// アクセスログ記録
$screenName = $isRegularContract ? '新規定期契約登録' : '駐輪場検索';
\Log::info("[INFO] " . now()->format('Y-m-d H:i:s') . " {$screenName}画面アクセス user_id=" . $userId . ", management_id=" . $management->management_id . ", city_id=" . ($cityId ?? 'null') . ", park_id=" . ($parkId ?? 'null'));
return view('regular_contract.create', [
'active_menu' => $activeMenu,
'user_name' => $user->user_name ?? '',
'cities' => $cities,
'stations' => $stations,
'parks' => $parks,
'parks_table' => $parksTableData['data'],
'parks_table_total' => $parksTableData['total'],
'parks_table_page' => $page,
'parks_table_perPage' => $perPage,
'available_vehicles' => $availableVehicles,
'reserve' => $reserve,
'isRegularContract' => $isRegularContract,
]);
}
/**
* 市町村一覧取得(運営元フィルタ適用)
* マルチテナント対応のため、運営元に紐づく駐輪場の市町村のみ表示
*
* @param int $managementId
* @return \Illuminate\Support\Collection
*/
private function getCitiesList(int $managementId)
{
try {
return DB::table('park')
->join('city', 'park.city_id', '=', 'city.city_id') ->join('city', 'park.city_id', '=', 'city.city_id')
->where('park.management_id', $managementId)
->select('city.city_id', 'city.city_name') ->select('city.city_id', 'city.city_name')
->distinct() ->distinct()
->get(); ->get();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 市町村一覧取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return collect();
}
}
// city_idごとの更新可能期間情報を取得 /**
$city_grace_periods = DB::table('city') * 駅名一覧取得(運営元フィルタ適用)
->select('city_id', 'update_grace_period_start_date', 'update_grace_period_start_time', 'update_grace_period_end_date', 'update_grace_period_end_time') *
->get() * マルチテナント対応のため、運営元に紐づく駐輪場の駅のみ表示する。
->keyBy('city_id'); *
* @param int $managementId
// 駅名stationテーブルのstation_neighbor_station全件 * @return \Illuminate\Support\Collection
$stations = DB::table('station') */
->select('station_neighbor_station') private function getStationsList(int $managementId)
{
try {
return DB::table('station')
->join('park', 'station.park_id', '=', 'park.park_id')
->where('park.management_id', $managementId)
->select('station.station_neighbor_station')
->distinct() ->distinct()
->get(); ->get();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駅名一覧取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return collect();
}
}
// 駐輪場名parkテーブルのpark_name全件 /**
$parks = DB::table('park') * 駐輪場名一覧取得(運営元フィルタ適用)
*
* マルチテナント対応のため、運営元に紐づく駐輪場のみ表示する。
*
* @param int $managementId
* @return \Illuminate\Support\Collection
*/
private function getParksList(int $managementId)
{
try {
return DB::table('park')
->where('management_id', $managementId)
->select('park_id', 'park_name') ->select('park_id', 'park_name')
->distinct() ->distinct()
->get(); ->get();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場名一覧取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return collect();
}
}
// テーブル表示用データpark/city/station JOIN, park_id昇順, 10件ずつページング /**
$page = request()->input('page', 1); * 駐輪場テーブルデータ取得(検索・ページング適用、運営元フィルタ適用)
$perPage = 10; *
$city_id = request()->input('city_id'); * マルチテナント対応のため、運営元に紐づく駐輪場のみ表示する。
$station_name = request()->input('station_neighbor_station'); *
$park_id = request()->input('park_id'); * @param int $managementId
* @param int|null $cityId
* @param string|null $stationName
* @param int|null $parkId
* @param int $page
* @param int $perPage
* @return array ['data' => Collection, 'total' => int]
*/
private function getParksTableData(int $managementId, $cityId, $stationName, $parkId, int $page, int $perPage)
{
try {
$query = DB::table('park') $query = DB::table('park')
->join('city', 'park.city_id', '=', 'city.city_id') ->join('city', 'park.city_id', '=', 'city.city_id')
->leftJoin('station', 'park.park_id', '=', 'station.park_id') ->leftJoin('station', 'park.park_id', '=', 'station.park_id')
->where('park.management_id', $managementId)
->select( ->select(
'park.park_id', 'park.park_id',
'park.park_name', 'park.park_name',
@ -70,94 +196,254 @@ class RegularContractCreateController extends Controller
'station.station_name_ruby' 'station.station_name_ruby'
); );
if ($city_id) { // 検索条件適用
$query->where('city.city_id', $city_id); if ($cityId) {
$query->where('city.city_id', $cityId);
} }
if ($station_name) { if ($stationName) {
$query->where('station.station_neighbor_station', $station_name); $query->where('station.station_neighbor_station', $stationName);
} }
if ($park_id) { if ($parkId) {
$query->where('park.park_id', $park_id); $query->where('park.park_id', $parkId);
} }
// 並び替えパラメータ取得
$sort = request()->input('sort', 'park_id');
$order = request()->input('order', 'asc');
$sortable = [
'park_ruby' => 'park.park_ruby',
'city_id' => 'city.city_id',
'station_name_ruby' => 'station.station_name_ruby',
'park_id' => 'park.park_id',
];
$query->orderBy('park.park_id', 'asc'); $query->orderBy('park.park_id', 'asc');
$total = $query->count(); $total = $query->count();
$parks_table = $query->get();
if ($sort === 'park_ruby' || $sort === 'station_name_ruby') { // ページング適用
$collator = new \Collator('ja'); $data = $query
$parks_table = $parks_table->sort(function ($a, $b) use ($order, $sort, $collator) { ->skip(($page - 1) * $perPage)
$a_val = $a->$sort ?? ''; ->take($perPage)
$b_val = $b->$sort ?? ''; ->get();
return $order === 'asc'
? $collator->compare($a_val, $b_val) \Log::info("[INFO] " . now()->format('Y-m-d H:i:s') . " 駐輪場データ取得成功 management_id=" . $managementId . ", 件数=" . $total . ", page=" . $page);
: $collator->compare($b_val, $a_val);
})->values(); return [
} else { 'data' => $data,
// park_id, city_idなどはSQLのorderByで十分なので、ここでPHPソートは不要 'total' => $total,
if (isset($sortable[$sort])) { ];
$parks_table = $parks_table->sortBy($sort, SORT_REGULAR, $order === 'desc')->values(); } catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場テーブルデータ取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return [
'data' => collect(),
'total' => 0,
];
} }
} }
$page = request()->input('page', 1);
$perPage = 10;
$parks_table = $parks_table->slice(($page - 1) * $perPage, $perPage)->values();
// zoneテーブルデータを取得psectionテーブルとJOINしてpsection_subjectも取得 /**
$zones = DB::table('zone') * ゾーンデータを park_id でグループ化して取得(運営元フィルタ適用)
*
* マルチテナント対応のため、運営元に紐づく駐輪場のゾーンのみ取得する。
*
* @param int $managementId
* @return \Illuminate\Support\Collection
*/
private function getZonesByParkId(int $managementId)
{
try {
return DB::table('zone')
->join('park', 'zone.park_id', '=', 'park.park_id')
->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id') ->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id')
->select('zone.zone_id', 'zone.park_id', 'zone.ptype_id', 'zone.psection_id', 'zone.zone_number', 'zone.zone_tolerance', 'psection.psection_subject') ->where('park.management_id', $managementId)
->select(
'zone.zone_id',
'zone.park_id',
'zone.ptype_id',
'zone.psection_id',
'zone.zone_number',
'zone.zone_tolerance',
'psection.psection_subject'
)
->get() ->get()
->groupBy('park_id'); ->groupBy('park_id');
} catch (\Exception $e) {
// 空き予約マスタデータを取得 \Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " zone データ取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
$reserve = DB::table('reserve') return collect();
->select('reserve_id', 'park_id', 'ptype_id', 'psection_id') }
->where('valid_flag', 1)
->get()
->groupBy('park_id');
// ルート名で画面表示を切り替え(新規定期契約 or 駐輪場検索)
$isRegularContract = $request->route()->getName() === 'regular_contract.create';
// ヘッダーの選択状態を分岐
$active_menu = $isRegularContract ? 'SWC-8-1' : 'SWC-10-1';
if ($isRegularContract) {
\Log::info('新規定期契約-駐輪場選択画面にアクセス', [
'user_id' => $user_id,
]);
} else {
\Log::info('駐輪場検索-駐輪場選択画面にアクセス', [
'user_id' => $user_id,
]);
} }
return view('regular_contract.create', [ /**
'active_menu' => $active_menu, // この画面のID * 予約データを park_id でグループ化して取得(運営元フィルタ適用)
'user_name' => $user ? $user->user_name : '', // ユーザー名(ヘッダー用) *
'cities' => $cities, * マルチテナント対応のため、運営元に紐づく駐輪場の予約のみ取得する。
'stations' => $stations, *
'parks' => $parks, * @param int $managementId
'parks_table' => $parks_table, * @return \Illuminate\Support\Collection
'parks_table_total' => $total, */
'parks_table_page' => $page, private function getReserveByParkId(int $managementId)
'parks_table_perPage' => $perPage, {
'zones' => $zones, try {
'city_grace_periods' => $city_grace_periods, return DB::table('reserve')
'reserve' => $reserve, ->join('park', 'reserve.park_id', '=', 'park.park_id')
'isRegularContract' => $isRegularContract ->where('park.management_id', $managementId)
]); ->where('reserve.valid_flag', 1)
->select('reserve.reserve_id', 'reserve.park_id', 'reserve.ptype_id', 'reserve.psection_id')
->get()
->groupBy('park_id');
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " reserve データ取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return collect();
}
}
/**
* 駐輪場ごとの更新可能期間を取得(運営元フィルタ適用)
*
*
* @param int $managementId
* @return \Illuminate\Support\Collection
*/
private function getParkGracePeriods(int $managementId)
{
try {
return DB::table('park')
->where('management_id', $managementId)
->select(
'park_id',
'update_grace_period_start_date',
'update_grace_period_start_time',
'update_grace_period_end_date',
'update_grace_period_end_time'
)
->get()
->keyBy('park_id');
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 駐輪場更新可能期間取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return collect();
}
}
/**
* 運営元が取り扱う車種一覧を取得
*
* マルチテナント対応のため、運営元ごとに表示する車種を可変にする。
*
* @param int $managementId
* @return array
*/
private function getAvailableVehicles(int $managementId)
{
try {
return DB::table('zone')
->join('park', 'zone.park_id', '=', 'park.park_id')
->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id')
->where('park.management_id', $managementId)
->whereNotNull('psection.psection_subject')
->select('psection.psection_subject')
->distinct()
->orderByRaw("FIELD(psection.psection_subject, '自転車', '原付', '自動二輪', '自動車')")
->pluck('psection_subject')
->toArray();
} catch (\Exception $e) {
\Log::error("[ERROR] " . now()->format('Y-m-d H:i:s') . " 取り扱い車種取得失敗 management_id=" . $managementId . ", error=" . $e->getMessage());
return [];
}
}
/**
* 駐輪場ごと・車種ごとの状態を計算
*
* @param \Illuminate\Support\Collection $parks
* @param \Illuminate\Support\Collection $zones
* @param \Illuminate\Support\Collection $reserve
* @param \Illuminate\Support\Collection $parkGracePeriods
* @param array $availableVehicles
* @return \Illuminate\Support\Collection
*/
private function calculateVehicleStatus($parks, $zones, $reserve, $parkGracePeriods, $availableVehicles)
{
$now = \Carbon\Carbon::now();
foreach ($parks as $park) {
$park->vehicle_status = [];
foreach ($availableVehicles as $vehicle) {
$zonesForType = ($zones[$park->park_id] ?? collect())->where('psection_subject', $vehicle);
// 空き台数計算
$hasVacancy = false;
foreach ($zonesForType as $zone) {
$reserveCount = ($reserve[$park->park_id] ?? collect())
->where('psection_id', $zone->psection_id)
->where('ptype_id', $zone->ptype_id)
->count();
$vacancy = $zone->zone_tolerance - $zone->zone_number - $reserveCount;
if ($vacancy > 0) {
$hasVacancy = true;
break;
}
}
// 猶予期間判定
$grace = $parkGracePeriods[$park->park_id] ?? null;
$inGrace = $this->isInGracePeriod($grace, $now);
// 状態判定
if ($zonesForType->isEmpty() || !$grace) {
$park->vehicle_status[$vehicle] = 'none';
} elseif ($inGrace && $hasVacancy) {
$park->vehicle_status[$vehicle] = 'available';
} elseif (!$inGrace) {
$park->vehicle_status[$vehicle] = 'out_of_period';
} elseif (!$hasVacancy) {
$park->vehicle_status[$vehicle] = 'waiting';
} else {
$park->vehicle_status[$vehicle] = 'none';
}
}
}
return $parks;
}
/**
* 猶予期間内かどうかを判定
*
* @param object|null $grace
* @param \Carbon\Carbon $now
* @return bool
*/
private function isInGracePeriod($grace, $now)
{
if (!$grace) {
return false;
}
// バリデーション
if (
!is_numeric($grace->update_grace_period_start_date) ||
!preg_match('/^\d{1,2}:\d{2}$/', $grace->update_grace_period_start_time) ||
!is_numeric($grace->update_grace_period_end_date) ||
!preg_match('/^\d{1,2}:\d{2}$/', $grace->update_grace_period_end_time)
) {
return false;
}
$year = $now->year;
$month = $now->month;
$startDay = (int)$grace->update_grace_period_start_date;
$endDay = (int)$grace->update_grace_period_end_date;
if ($startDay > $endDay) {
// 月またぎ(例: 25日5日
$prevMonth = $now->copy()->subMonth();
$startPrev = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $prevMonth->year, $prevMonth->month, $startDay, $grace->update_grace_period_start_time));
$endCurr = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $endDay, $grace->update_grace_period_end_time));
$startCurr = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $startDay, $grace->update_grace_period_start_time));
$nextMonth = $month == 12 ? 1 : $month + 1;
$nextYear = $month == 12 ? $year + 1 : $year;
$endNext = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $nextYear, $nextMonth, $endDay, $grace->update_grace_period_end_time));
return $now->between($startPrev, $endCurr) || $now->between($startCurr, $endNext);
} else {
// 同月(例: 5日25日
$start = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $startDay, $grace->update_grace_period_start_time));
$end = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $endDay, $grace->update_grace_period_end_time));
return $now->between($start, $end);
}
} }
public function regulationCheck(Request $request) public function regulationCheck(Request $request)

View File

@ -38,11 +38,11 @@ trait AuthenticatesUser
if ($saveIntended) { if ($saveIntended) {
// ログイン後に元の画面に戻る // ログイン後に元の画面に戻る
return redirect()->guest(route('swo8_1', ['management_code' => $managementCode])); return redirect()->guest(route('login_input', ['management_code' => $managementCode]));
} else { } else {
// ログイン後はマイページトップへ // ログイン後はマイページトップへ
session()->forget('url.intended'); session()->forget('url.intended');
return redirect()->route('swo8_1', ['management_code' => $managementCode]); return redirect()->route('login_input', ['management_code' => $managementCode]);
} }
} }
} }

View File

@ -17,7 +17,6 @@ table.tablesorter tfoot tr th {
} }
table.tablesorter thead tr .header { table.tablesorter thead tr .header {
background-image: url(bg.gif);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center right; background-position: center right;
cursor: pointer; cursor: pointer;
@ -34,14 +33,6 @@ table.tablesorter tbody tr.odd td {
background-color: #F0F0F6; background-color: #F0F0F6;
} }
table.tablesorter thead tr .headerSortUp {
background-image: url(asc.gif);
}
table.tablesorter thead tr .headerSortDown {
background-image: url(desc.gif);
}
table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortDown,
table.tablesorter thead tr .headerSortUp { table.tablesorter thead tr .headerSortUp {
background-color: #8dbdd8; background-color: #8dbdd8;

View File

@ -115,17 +115,26 @@ $(function () {
}); });
// 駐輪場詳細ポップアップ // 駐輪場詳細ポップアップ
$(document).on('click', '.btn-popup', function () { $(document).on('click', '.btn-popup', function (e) {
var parkId = $(this).data('park-id'); e.preventDefault();
const url = $(this).data('url');
// data-url が存在しない場合はエラー
if (!url) {
console.error('data-url属性が設定されていません');
alert('URLが取得できませんでした。');
return;
}
$.ajax({ $.ajax({
url: '/api/park-detail/' + parkId, url: url,
method: 'GET', method: 'GET',
success: function (data) { success: function (response) {
$('#modalArea').html(data.html); // モーダル全体を挿入 $('#modalArea').html(response.html);
$('#parkDetailModal').modal('show'); // モーダルを表示 $('#popup-modal').modal('show');
}, },
error: function () { error: function () {
alert('詳細情報の取得に失敗しました'); alert('詳細情報の取得に失敗しました');
} }
}); });
}); });

View File

@ -18,6 +18,7 @@
<link href="{{ asset('assets/css/mypage/style.css') }}" rel="stylesheet"> <link href="{{ asset('assets/css/mypage/style.css') }}" rel="stylesheet">
<link href="{{ asset('assets/css/mypage/jquery-confirm.min.css') }}" rel="stylesheet"> <link href="{{ asset('assets/css/mypage/jquery-confirm.min.css') }}" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.4/jquery-confirm.min.css">
@yield('head') @yield('head')
<style> <style>
#main-news.jumbotron { #main-news.jumbotron {
@ -49,6 +50,20 @@
<script src="{{ asset('assets/js/vendor/jquery.tablesorter/jquery.tablesorter.min.js') }}" type="text/javascript"></script> <script src="{{ asset('assets/js/vendor/jquery.tablesorter/jquery.tablesorter.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('assets/js/commons.js') }}"></script> <script src="{{ asset('assets/js/commons.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.4/jquery-confirm.min.js"></script>
<script>
$(document).ready(function() {
// モーダルを閉じる前にフォーカスを解除
$('.modal').on('hide.bs.modal', function(e) {
$(this).find(':focus').blur();
});
// モーダルが完全に閉じた後、body にフォーカスを戻す
$('.modal').on('hidden.bs.modal', function() {
$('body').focus();
});
});
</script>
@yield('scripts') @yield('scripts')
</body> </body>

View File

@ -25,7 +25,7 @@ $management_code = $management ? $management->management_code : '';
<li class="nav-item {{ $active_menu == 'SWC-8-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('regular_contract.create', ['management_code' => $management_code]) }}">新規定期契約</a></li> <li class="nav-item {{ $active_menu == 'SWC-8-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('regular_contract.create', ['management_code' => $management_code]) }}">新規定期契約</a></li>
<li class="nav-item {{ $active_menu == 'SWC-11-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('park_waitlist.index', ['management_code' => $management_code]) }}">空き待ち状況確認</a></li> <li class="nav-item {{ $active_menu == 'SWC-11-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('park_waitlist.index', ['management_code' => $management_code]) }}">空き待ち状況確認</a></li>
<li class="nav-item {{ $active_menu == 'SWC-10-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('park_search', ['management_code' => $management_code]) }}">駐輪場検索</a></li> <li class="nav-item {{ $active_menu == 'SWC-10-1' ? 'active' : '' }}"><a class="nav-link" href="{{ route('park_search', ['management_code' => $management_code]) }}">駐輪場検索</a></li>
<li class="nav-item"><a class="nav-link" href="{{ route('swo16_1', ['management_code' => $management_code]) }}" target="_blank">このページの使い方</a></li> <li class="nav-item"><a class="nav-link" href="{{ route('guide', ['management_code' => $management_code]) }}" target="_blank">このページの使い方</a></li>
</ul> </ul>
</div> </div>
</nav> </nav>

View File

@ -1,11 +1,12 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', $isRegularContract ? '空き駐輪場を確認する-新規定期契約 | So-Manager' : '駐輪場選択-駐輪場検索 | So-Manager')
@section('content') @section('content')
<main> <main>
<header class="alert alert-success"> <header class="title-header">
@if($isRegularContract) @if($isRegularContract)
<h4 class="container">新規定期契約 > 空き駐輪場を確認する</h4> 新規定期契約 &gt; 空き駐輪場を確認する
@else @else
<h4 class="container">駐輪場検索 > 駐輪場選択</h4> 駐輪場検索 &gt; 駐輪場選択
@endif @endif
</header> </header>
<section id="" class="container mt20 mb20"> <section id="" class="container mt20 mb20">
@ -20,11 +21,11 @@
<div class="w-100 alert alert-success"> <div class="w-100 alert alert-success">
<h6><a class="text-success" data-toggle="collapse" href="#search-option" role="button" <h6><a class="text-success" data-toggle="collapse" href="#search-option" role="button"
aria-expanded="false" aria-controls="search-option">絞込み条件を追加する</a></h6> aria-expanded="false" aria-controls="search-option">絞込み条件を追加する</a></h6>
<div class="collapse row" id="search-option"> <div class="collapse row show" id="search-option">
<div class="col-3">町村名</div> <div class="col-3">町村名</div>
<div class="col-9 mb10"> <div class="col-9 mb10">
<select id="city-select" name="city_id" class="form-control form-control-lg" onchange="this.form.submit()"> <select id="city-select" name="city_id" class="form-control form-control-lg" onchange="this.form.submit()">
<option value="">町村を選択してください</option> <option value="">町村を選択してください</option>
@foreach($cities as $city) @foreach($cities as $city)
<option value="{{ $city->city_id }}" @if(request('city_id')==$city->city_id) selected @endif>{{ $city->city_name }}</option> <option value="{{ $city->city_id }}" @if(request('city_id')==$city->city_id) selected @endif>{{ $city->city_name }}</option>
@endforeach @endforeach
@ -56,84 +57,35 @@
<thead> <thead>
<tr> <tr>
<th class="header" style="cursor:default; pointer-events: none; background-color: #d4edda !important;">駐輪場名</th> <th class="header" style="cursor:default; pointer-events: none; background-color: #d4edda !important;">駐輪場名</th>
<th class="header" style="cursor:default; pointer-events: none;">町村名</th> <th class="header" style="cursor:default; pointer-events: none;">町村名</th>
<th class="header" style="cursor:default; pointer-events: none;">駅名</th> <th class="header" style="cursor:default; pointer-events: none;">駅名</th>
<th class="header" style="cursor:default; pointer-events: none;">自転車</th> @foreach($available_vehicles as $vehicle)
<th class="header" style="cursor:default; pointer-events: none;">原付</th> <th class="header" style="cursor:default; pointer-events: none;">{{ $vehicle }}</th>
<th class="header" style="cursor:default; pointer-events: none;">自動二輪</th> @endforeach
<th class="header" style="cursor:default; pointer-events: none;">自動車</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($parks_table as $row) @forelse($parks_table as $row)
<tr> <tr>
<td><a href="javascript:void(0);" class="btn-popup text-primary" data-park-id="{{ $row->park_id }}"> <td><a href="javascript:void(0);"
class="btn-popup text-primary"
data-park-id="{{ $row->park_id }}"
data-url="{{ url(session('management')->management_code . '/api/park-detail/' . $row->park_id) }}">
{{ $row->park_name }} {{ $row->park_name }}
</a></td> </a></td>
<td>{{ $row->city_name }}</td> <td>{{ $row->city_name }}</td>
<td>{{ $row->station_neighbor_station }}</td> <td>{{ $row->station_neighbor_station }}</td>
{{-- 自転車・原付・自動二輪・自動車列 --}} @foreach($available_vehicles as $vehicle)
@foreach(['自転車', '原付', '自動二輪', '自動車'] as $vehicle)
<td> <td>
@php @php
$zonesForType = ($zones[$row->park_id] ?? collect())->where('psection_subject', $vehicle); $status = $row->vehicle_status[$vehicle] ?? 'none';
// 空き台数計算
$hasVacancy = false;
foreach ($zonesForType as $zone) {
$reserveCount = ($reserve[$row->park_id] ?? collect())
->where('psection_id', $zone->psection_id)
->where('ptype_id', $zone->ptype_id)
->count();
$vacancy = $zone->zone_tolerance - $zone->zone_number - $reserveCount;
if ($vacancy > 0) {
$hasVacancy = true;
break;
}
}
// 猶予期間判定
$grace = $city_grace_periods[$row->city_id] ?? null;
$gracePeriodValid =
$grace &&
is_numeric($grace->update_grace_period_start_date) &&
preg_match('/^\d{1,2}:\d{2}$/', $grace->update_grace_period_start_time) &&
is_numeric($grace->update_grace_period_end_date) &&
preg_match('/^\d{1,2}:\d{2}$/', $grace->update_grace_period_end_time);
$inGrace = false;
if ($gracePeriodValid) {
$now = \Carbon\Carbon::now();
$year = $now->year;
$month = $now->month;
$startDay = (int)$grace->update_grace_period_start_date;
$endDay = (int)$grace->update_grace_period_end_date;
if ($startDay > $endDay) {
// 月またぎ
// 前月の開始日~今月の終了日
$prevMonth = $now->copy()->subMonth();
$startPrev = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $prevMonth->year, $prevMonth->month, $startDay, $grace->update_grace_period_start_time));
$endCurr = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $endDay, $grace->update_grace_period_end_time));
// 今月の開始日~翌月の終了日
$startCurr = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $startDay, $grace->update_grace_period_start_time));
$nextMonth = $month == 12 ? 1 : $month + 1;
$nextYear = $month == 12 ? $year + 1 : $year;
$endNext = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $nextYear, $nextMonth, $endDay, $grace->update_grace_period_end_time));
$inGrace = $now->between($startPrev, $endCurr) || $now->between($startCurr, $endNext);
} else {
// 同月
$start = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $startDay, $grace->update_grace_period_start_time));
$end = \Carbon\Carbon::createFromFormat('Y-m-d H:i', sprintf('%04d-%02d-%02d %s', $year, $month, $endDay, $grace->update_grace_period_end_time));
$inGrace = $now->between($start, $end);
}
}
@endphp @endphp
@if ($zonesForType->isNotEmpty() && $gracePeriodValid) @if($status === 'available')
@if ($hasVacancy && $inGrace) <button class="btn btn-block btn-sm btn-outline-success btn_82-table btn-popup" data-park-id="{{ $row->park_id }}" data-url="{{ url(session('management')->management_code . '/api/park-detail/' . $row->park_id) }}">定期契約</button>
<button class="btn btn-block btn-sm btn-outline-success btn_82-table btn-popup" data-park-id="{{ $row->park_id }}">定期契約</button> @elseif($status === 'out_of_period')
@elseif (!$inGrace) <button class="btn btn-block btn-sm btn-outline-danger btn_103-table btn-popup" data-park-id="{{ $row->park_id }}" data-url="{{ url(session('management')->management_code . '/api/park-detail/' . $row->park_id) }}">販売期間外(予約可)</button>
<button class="btn btn-block btn-sm btn-outline-danger btn_103-table btn-popup" data-park-id="{{ $row->park_id }}">販売期間外</button> @elseif($status === 'waiting')
@elseif (!$hasVacancy && $inGrace) <button class="btn btn-block btn-sm btn-outline-danger btn_103-table btn-popup" data-park-id="{{ $row->park_id }}" data-url="{{ url(session('management')->management_code . '/api/park-detail/' . $row->park_id) }}">空き待ち予約</button>
<button class="btn btn-block btn-sm btn-outline-danger btn_103-table btn-popup" data-park-id="{{ $row->park_id }}">空き待ち申込</button>
@endif
@else @else
<span class="text-muted"></span> <span class="text-muted"></span>
@endif @endif
@ -142,7 +94,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="text-center">該当する駐輪場はありません。</td> <td colspan="{{ 3 + count($available_vehicles) }}" class="text-center">該当する駐輪場はありません。</td>
</tr> </tr>
@endforelse @endforelse
</tbody> </tbody>
@ -180,11 +132,10 @@
</div> </div>
</section> </section>
</main> </main>
<div id="modalArea"></div> <div class="modal fade" id="popup-modal" tabindex="-1" role="dialog">
<div class="modal fade" id="popup-modal" tabindex="-1"> <div class="modal-dialog modal-lg">
<div class="modal-dialog"> <div class="modal-content" id="modalArea">
<div class="modal-content" id="popup-content"> <!-- Ajaxで取得したHTMLがここに挿入される -->
<!-- Ajaxで詳細HTMLを挿入 -->
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,4 @@
<!-- Bootstrapモーダル --> <!-- Bootstrapモーダル -->
<div class="modal fade" id="parkDetailModal" tabindex="-1" aria-labelledby="parkDetailLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="parkDetailLabel">{{ $park->park_name }}</h5> <h5 class="modal-title" id="parkDetailLabel">{{ $park->park_name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="閉じる"> <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
@ -41,12 +38,12 @@
@endphp @endphp
@foreach($zonesByPtype as $ptypeId => $zonesGroup) @foreach($zonesByPtype as $ptypeId => $zonesGroup)
<div class="mb-3"> <div class="mb-3">
<strong>{{ $zonesGroup->first()->ptype_subject }}</strong> <strong>{{ $zonesGroup->first()->ptype_subject }}{{ $zonesGroup->first()->zone_name }}</strong>
<div style="display: flex; gap: 1em;"> <div style="display: flex; gap: 1em;">
@foreach($zonesGroup as $zone) @foreach($zonesGroup as $zone)
@php @php
$vacant = $vacancyData[$zone->psection_id . '_' . $zone->ptype_subject] ?? 0; $vacant = $vacancyData[$zone->psection_id . '_' . $zone->ptype_subject] ?? 0;
$grace = $city_grace_periods[$park->city_id] ?? null; $grace = $parkGracePeriod ?? null;
$now = \Carbon\Carbon::now(); $now = \Carbon\Carbon::now();
// 猶予期間判定 // 猶予期間判定
@ -95,7 +92,7 @@
data-park-id="{{ $park->park_id }}" data-park-id="{{ $park->park_id }}"
data-psection-id="{{ $zone->psection_id }}" data-psection-id="{{ $zone->psection_id }}"
data-ptype-id="{{ $zone->ptype_id }}"> data-ptype-id="{{ $zone->ptype_id }}">
空き待ち申込 空き待ち予約
</button> </button>
@endif @endif
@else @else
@ -103,7 +100,7 @@
data-park-id="{{ $park->park_id }}" data-park-id="{{ $park->park_id }}"
data-psection-id="{{ $zone->psection_id }}" data-psection-id="{{ $zone->psection_id }}"
data-ptype-id="{{ $zone->ptype_id }}"> data-ptype-id="{{ $zone->ptype_id }}">
販売期間外 販売期間外(予約可)
</button> </button>
@endif @endif
</div> </div>
@ -116,9 +113,3 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
</div> </div>
</div>
</div>
</div>
<!-- jQuery Confirm用CSS/JSjQuery本体は既に読み込まれていれば不要 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.4/jquery-confirm.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.4/jquery-confirm.min.js"></script>