Merge branch 'main' of https://git.so-manager-dev.com/so-manager/krgm.so-manager-dev.com
All checks were successful
Deploy main / deploy (push) Successful in 22s
All checks were successful
Deploy main / deploy (push) Successful in 22s
This commit is contained in:
commit
5949e912b4
22
.env
22
.env
@ -46,16 +46,22 @@ REDIS_HOST=127.0.0.1
|
|||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
#MAIL_MAILER=smtp
|
||||||
#MAIL_SCHEME=null
|
#MAIL_SCHEME=null
|
||||||
MAIL_HOST=tomatofox9.sakura.ne.jp
|
#MAIL_HOST=tomatofox9.sakura.ne.jp
|
||||||
MAIL_PORT=587
|
#MAIL_PORT=587
|
||||||
MAIL_USERNAME=demo@so-rin.jp
|
#MAIL_USERNAME=demo@so-rin.jp
|
||||||
MAIL_PASSWORD=rokuchou4665
|
#MAIL_PASSWORD=rokuchou4665
|
||||||
MAIL_ENCRYPTION=tls
|
#MAIL_ENCRYPTION=tls
|
||||||
MAIL_FROM_ADDRESS=demo@so-rin.jp
|
#MAIL_FROM_ADDRESS=demo@so-rin.jp
|
||||||
|
#MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
#MAIL_ADMIN=demo@so-rin.jp
|
||||||
|
|
||||||
|
MAIL_MAILER=sendmail
|
||||||
|
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -bs -i"
|
||||||
|
MAIL_FROM_ADDRESS=demo@so-manager-dev.com
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
MAIL_ADMIN=demo@so-rin.jp
|
MAIL_ADMIN=demo@so-manager-dev.com
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
@ -16,20 +16,29 @@ class CityController extends Controller
|
|||||||
$sortType = $request->input('sort_type', 'asc');
|
$sortType = $request->input('sort_type', 'asc');
|
||||||
$page = $request->get('page', 1);
|
$page = $request->get('page', 1);
|
||||||
|
|
||||||
|
$menuAccessService = app(\App\Services\MenuAccessService::class);
|
||||||
|
|
||||||
|
// メニューアクセス制御: 非ソーリンユーザーは所属自治体のみ表示
|
||||||
$query = City::query();
|
$query = City::query();
|
||||||
|
if (!$menuAccessService->isSorin()) {
|
||||||
|
$operator = auth()->user();
|
||||||
|
if ($operator && isset($operator->management_id)) {
|
||||||
|
$query->where('management_id', $operator->management_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('city_name')) {
|
if ($request->filled('city_name')) {
|
||||||
$query->where('city_name', 'like', '%' . $request->input('city_name') . '%');
|
$query->where('city_name', 'like', '%' . $request->input('city_name') . '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排序处理
|
// ソート処理
|
||||||
if (!empty($sort)) {
|
if (!empty($sort)) {
|
||||||
$query->orderBy($sort, $sortType);
|
$query->orderBy($sort, $sortType);
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $query->paginate(20);
|
$list = $query->paginate(20);
|
||||||
|
|
||||||
// 页码越界处理
|
// インデックス超過処理
|
||||||
if ($list->total() > 0 && $page > $list->lastPage()) {
|
if ($list->total() > 0 && $page > $list->lastPage()) {
|
||||||
return redirect()->route('city', [
|
return redirect()->route('city', [
|
||||||
'sort' => $sort,
|
'sort' => $sort,
|
||||||
@ -109,6 +118,12 @@ class CityController extends Controller
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// メニューアクセス制御確認
|
||||||
|
$menuAccessService = app(\App\Services\MenuAccessService::class);
|
||||||
|
if (!$menuAccessService->canAccessCity($city->city_id)) {
|
||||||
|
abort(403, 'この自治体へのアクセス権限がありません。');
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
$rules = [
|
$rules = [
|
||||||
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||||
@ -171,4 +186,53 @@ class CityController extends Controller
|
|||||||
return redirect()->route('city')->with('error', __('削除に失敗しました。'));
|
return redirect()->route('city')->with('error', __('削除に失敗しました。'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自治体ダッシュボード
|
||||||
|
*/
|
||||||
|
public function dashboard(Request $request, $city_id)
|
||||||
|
{
|
||||||
|
$city = City::find($city_id);
|
||||||
|
if (!$city) {
|
||||||
|
return redirect()->route('city')->with('error', '指定された自治体が見つかりません。');
|
||||||
|
}
|
||||||
|
|
||||||
|
// この自治体に関連する駐輪場データを取得
|
||||||
|
$parks = \App\Models\Park::where('city_id', $city_id)->get();
|
||||||
|
$parkIds = $parks->pluck('park_id')->toArray();
|
||||||
|
|
||||||
|
// この自治体の統計情報を取得
|
||||||
|
$contractsCount = 0;
|
||||||
|
$usersCount = 0;
|
||||||
|
$waitingCount = 0;
|
||||||
|
|
||||||
|
if (!empty($parkIds)) {
|
||||||
|
// 契約数を取得
|
||||||
|
$contractsCount = \App\Models\RegularContract::whereIn('park_id', $parkIds)->count();
|
||||||
|
|
||||||
|
// この自治体の駐輪場で契約しているユニークユーザー数を取得
|
||||||
|
$userIds = \App\Models\RegularContract::whereIn('park_id', $parkIds)
|
||||||
|
->distinct()
|
||||||
|
->pluck('user_id')
|
||||||
|
->toArray();
|
||||||
|
$usersCount = count(array_filter($userIds));
|
||||||
|
|
||||||
|
// 予約待ち人数を取得(valid_flag = 1 かつ reserve_order が設定されているもの)
|
||||||
|
$waitingCount = DB::table('reserve')
|
||||||
|
->whereIn('park_id', $parkIds)
|
||||||
|
->where('valid_flag', 1)
|
||||||
|
->whereNotNull('reserve_order')
|
||||||
|
->where('reserve_order', '>', 0)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'parks_count' => $parks->count(),
|
||||||
|
'contracts_count' => $contractsCount,
|
||||||
|
'users_count' => $usersCount,
|
||||||
|
'waiting_count' => $waitingCount,
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.CityMaster.dashboard', compact('city', 'parks', 'stats'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -56,6 +56,12 @@ class InformationController extends Controller
|
|||||||
return view('admin.information.list', compact('jobs','period','type','status'));
|
return view('admin.information.list', compact('jobs','period','type','status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ダッシュボード表示
|
||||||
|
public function dashboard(Request $request)
|
||||||
|
{
|
||||||
|
return view('admin.information.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
// ステータス一括更新(着手=2 / 対応完了=3)
|
// ステータス一括更新(着手=2 / 対応完了=3)
|
||||||
public function updateStatus(Request $request)
|
public function updateStatus(Request $request)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@ -7,6 +8,9 @@ use App\Models\Ope;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
use App\Models\Feature;
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Models\OpePermission;
|
||||||
|
|
||||||
class OpeController extends Controller
|
class OpeController extends Controller
|
||||||
{
|
{
|
||||||
@ -23,11 +27,10 @@ class OpeController extends Controller
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Blade 側は $list / $sort / $sort_type を参照
|
// Blade 側は $list / $sort / $sort_type を参照
|
||||||
$list = Ope::search($inputs);
|
$list = Ope::search($inputs);
|
||||||
$sort = $inputs['sort'];
|
$sort = $inputs['sort'];
|
||||||
$sort_type = $inputs['sort_type'];
|
$sort_type = $inputs['sort_type'];
|
||||||
|
|
||||||
|
|
||||||
return view('admin.opes.list', compact('list', 'sort', 'sort_type'));
|
return view('admin.opes.list', compact('list', 'sort', 'sort_type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,25 +39,39 @@ class OpeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function add(Request $request)
|
public function add(Request $request)
|
||||||
{
|
{
|
||||||
if ($request->isMethod('get')) {
|
// ※機能(画面)一覧を取得(プルダウン用)
|
||||||
|
$features = Feature::query()
|
||||||
return view('admin.opes.add', [
|
->orderBy('id')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
// ※操作権限一覧を取得(チェックボックス用)
|
||||||
|
$permissions = Permission::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['id', 'code', 'name']);
|
||||||
|
|
||||||
|
if ($request->isMethod('get')) {
|
||||||
|
return view('admin.opes.add', [
|
||||||
'isEdit' => false,
|
'isEdit' => false,
|
||||||
'record' => new Ope(),
|
'record' => new Ope(),
|
||||||
'ope_id' => null,
|
|
||||||
'ope_name' => '',
|
'ope_id' => null,
|
||||||
'ope_type' => '',
|
'ope_name' => '',
|
||||||
'ope_mail' => '',
|
'ope_type' => '',
|
||||||
'ope_phone'=> '',
|
'ope_mail' => '',
|
||||||
|
'ope_phone' => '',
|
||||||
|
|
||||||
'ope_sendalart_que1' => 0, 'ope_sendalart_que2' => 0, 'ope_sendalart_que3' => 0,
|
'ope_sendalart_que1' => 0, 'ope_sendalart_que2' => 0, 'ope_sendalart_que3' => 0,
|
||||||
'ope_sendalart_que4' => 0, 'ope_sendalart_que5' => 0, 'ope_sendalart_que6' => 0,
|
'ope_sendalart_que4' => 0, 'ope_sendalart_que5' => 0, 'ope_sendalart_que6' => 0,
|
||||||
'ope_sendalart_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0,
|
'ope_sendalart_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0,
|
||||||
'ope_sendalart_que10'=> 0, 'ope_sendalart_que11'=> 0, 'ope_sendalart_que12'=> 0,
|
'ope_sendalart_que10' => 0, 'ope_sendalart_que11' => 0, 'ope_sendalart_que12' => 0,
|
||||||
'ope_sendalart_que13'=> 0,
|
'ope_sendalart_que13' => 0,
|
||||||
'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '',
|
'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '',
|
||||||
'ope_quit_flag' => 0, 'ope_quitday' => '',
|
'ope_quit_flag' => 0, 'ope_quitday' => '',
|
||||||
|
|
||||||
|
// ▼追加:権限設定UI用
|
||||||
|
'features' => $features,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
'selectedFeatureId' => old('feature_id', null),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,36 +113,73 @@ class OpeController extends Controller
|
|||||||
return redirect()->route('opes')->with('success', '登録しました。');
|
return redirect()->route('opes')->with('success', '登録しました。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 編集(GET 画面 / POST 更新)
|
* 編集(GET 画面 / POST 更新)
|
||||||
|
* ※権限(自治体×機能×操作)も同画面で設定する
|
||||||
*/
|
*/
|
||||||
public function edit($id, Request $request)
|
public function edit($id, Request $request)
|
||||||
{
|
{
|
||||||
$ope = Ope::getByPk($id);
|
$ope = Ope::getByPk($id);
|
||||||
if (!$ope) abort(404);
|
if (!$ope) abort(404);
|
||||||
|
|
||||||
|
// ※機能(画面)一覧を取得(プルダウン用)
|
||||||
|
$features = Feature::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
// ※操作権限一覧を取得(チェックボックス用)
|
||||||
|
$permissions = Permission::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['id', 'code', 'name']);
|
||||||
|
|
||||||
|
// ※自治体ID(opeに紐づく想定)
|
||||||
|
$municipalityId = (int)($ope->municipality_id ?? 0);
|
||||||
|
|
||||||
if ($request->isMethod('get')) {
|
if ($request->isMethod('get')) {
|
||||||
return view('admin.opes.edit', [
|
return view('admin.opes.edit', [
|
||||||
'isEdit' => true,
|
'isEdit' => true,
|
||||||
'record' => $ope,
|
'record' => $ope,
|
||||||
|
|
||||||
|
// ▼追加:権限設定UI用
|
||||||
|
'features' => $features,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
'selectedFeatureId' => old('feature_id', null),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ▼権限設定の保存(feature_id + permission_ids[])
|
||||||
|
* ※画面側の保存ボタンを「権限も同時保存」にする場合はここで処理する
|
||||||
|
* ※もし「基本情報の更新」と「権限更新」をボタンで分けたい場合は、別アクションに分離推奨
|
||||||
|
*/
|
||||||
|
if ($request->has('feature_id')) {
|
||||||
|
$request->validate([
|
||||||
|
'feature_id' => ['required', 'integer', 'exists:features,id'],
|
||||||
|
'permission_ids' => ['nullable', 'array'],
|
||||||
|
'permission_ids.*' => ['integer', 'exists:permissions,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$featureId = (int)$request->input('feature_id');
|
||||||
|
$permissionIds = array_map('intval', (array)$request->input('permission_ids', []));
|
||||||
|
|
||||||
|
DB::transaction(function () use ($municipalityId, $featureId, $permissionIds) {
|
||||||
|
// ※機能単位で置換(自治体単位)
|
||||||
|
OpePermission::replaceByFeature($municipalityId, $featureId, $permissionIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 入力値を一旦取得
|
// 入力値を一旦取得
|
||||||
$data = $request->all();
|
$data = $request->all();
|
||||||
|
|
||||||
// --- バリデーション ---
|
// --- バリデーション ---
|
||||||
$rules = [
|
$rules = [
|
||||||
'login_id' => "required|string|max:255|unique:ope,login_id,{$id},ope_id", // 編集時は自分を除外
|
'login_id' => "required|string|max:255|unique:ope,login_id,{$id},ope_id",
|
||||||
'ope_name' => 'required|string|max:255',
|
'ope_name' => 'required|string|max:255',
|
||||||
'ope_type' => 'required|string|max:50',
|
'ope_type' => 'required|string|max:50',
|
||||||
'ope_phone' => 'nullable|string|max:50',
|
'ope_phone' => 'nullable|string|max:50',
|
||||||
'ope_mail' => [
|
'ope_mail' => [
|
||||||
'required',
|
'required',
|
||||||
function ($attribute, $value, $fail) {
|
function ($attribute, $value, $fail) {
|
||||||
// , でも ; でもOKにする
|
|
||||||
$emails = array_map('trim', explode(';', str_replace(',', ';', $value)));
|
$emails = array_map('trim', explode(';', str_replace(',', ';', $value)));
|
||||||
foreach ($emails as $email) {
|
foreach ($emails as $email) {
|
||||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
@ -134,7 +188,7 @@ class OpeController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'password' => 'nullable|string|min:8|confirmed', // 編集時は任意
|
'password' => 'nullable|string|min:8|confirmed',
|
||||||
];
|
];
|
||||||
|
|
||||||
$request->validate($rules);
|
$request->validate($rules);
|
||||||
@ -157,18 +211,42 @@ class OpeController extends Controller
|
|||||||
return redirect()->route('opes')->with('success', '更新しました。');
|
return redirect()->route('opes')->with('success', '更新しました。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 権限回顧(AJAX)
|
||||||
|
* /opes/{id}/permissions?feature_id=xx
|
||||||
|
* ※ope_permissionが自治体単位のため、opeの自治体IDで取得する
|
||||||
|
*/
|
||||||
|
public function getPermissionsByFeature(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$ope = Ope::getByPk($id);
|
||||||
|
if (!$ope) abort(404);
|
||||||
|
|
||||||
|
$featureId = (int)$request->query('feature_id');
|
||||||
|
if ($featureId <= 0) {
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$municipalityId = (int)($ope->municipality_id ?? 0);
|
||||||
|
|
||||||
|
$ids = OpePermission::query()
|
||||||
|
->where('municipality_id', $municipalityId)
|
||||||
|
->where('feature_id', $featureId)
|
||||||
|
->pluck('permission_id')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return response()->json($ids);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 削除(単体 or 複数)
|
* 削除(単体 or 複数)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function delete(Request $request)
|
public function delete(Request $request)
|
||||||
{
|
{
|
||||||
$ids = [];
|
$ids = [];
|
||||||
|
|
||||||
// 単体削除
|
// 単体削除
|
||||||
if ($request->filled('id')) {
|
if ($request->filled('id')) {
|
||||||
$ids[] = (int) $request->input('id');
|
$ids[] = (int)$request->input('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 複数削除
|
// 複数削除
|
||||||
|
|||||||
138
app/Http/Controllers/Auth/EmailOtpController.php
Normal file
138
app/Http/Controllers/Auth/EmailOtpController.php
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\EmailOtpMail;
|
||||||
|
use App\Models\Ope;
|
||||||
|
use App\Services\EmailOtpService;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP メール認証コントローラー
|
||||||
|
*
|
||||||
|
* ログイン後の OTP(ワンタイムパスワード)検証プロセスを処理します
|
||||||
|
*/
|
||||||
|
class EmailOtpController extends Controller
|
||||||
|
{
|
||||||
|
use ValidatesRequests;
|
||||||
|
|
||||||
|
protected EmailOtpService $otpService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* コンストラクタ
|
||||||
|
*/
|
||||||
|
public function __construct(EmailOtpService $otpService)
|
||||||
|
{
|
||||||
|
$this->otpService = $otpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP 入力フォームを表示
|
||||||
|
*
|
||||||
|
* ログイン直後、ユーザーに6桁の OTP コードを入力させるページを表示します
|
||||||
|
* メールアドレスはマスク表示(例:a***@example.com)
|
||||||
|
*/
|
||||||
|
public function show(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Ope */
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// メールアドレスをマスク(最初の1文字のみ表示)
|
||||||
|
$maskedEmail = $this->otpService->maskEmail($user->ope_mail);
|
||||||
|
|
||||||
|
// 次の重発までの待機時間
|
||||||
|
$resendWaitSeconds = $this->otpService->getResendWaitSeconds($user);
|
||||||
|
|
||||||
|
return view('auth.otp', [
|
||||||
|
'maskedEmail' => $maskedEmail,
|
||||||
|
'resendWaitSeconds' => $resendWaitSeconds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コード検証
|
||||||
|
*
|
||||||
|
* ユーザーが入力した6桁のコードを検証します
|
||||||
|
*
|
||||||
|
* 成功時:email_otp_verified_at を更新し、ホームページにリダイレクト
|
||||||
|
* 失敗時:エラーメッセージと共に OTP 入力フォームに戻す
|
||||||
|
*/
|
||||||
|
public function verify(Request $request)
|
||||||
|
{
|
||||||
|
// 入力値を検証
|
||||||
|
$validated = $this->validate($request, [
|
||||||
|
'code' => ['required', 'string', 'size:6', 'regex:/^\d{6}$/'],
|
||||||
|
], [
|
||||||
|
'code.required' => 'OTPコードは必須です。',
|
||||||
|
'code.size' => 'OTPコードは6桁である必要があります。',
|
||||||
|
'code.regex' => 'OTPコードは6桁の数字である必要があります。',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var Ope */
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// OTP コードを検証
|
||||||
|
if ($this->otpService->verify($user, $validated['code'])) {
|
||||||
|
// 検証成功:ホームページにリダイレクト
|
||||||
|
return redirect()->intended(route('home'))
|
||||||
|
->with('success', 'OTP認証が完了しました。');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 検証失敗:エラーメッセージと共に戻す
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', '無効なまたは有効期限切れのOTPコードです。');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コード再送
|
||||||
|
*
|
||||||
|
* ユーザーが OTP コード再送をリクエストした場合に実行
|
||||||
|
* 60秒以内の連続再送はブロックします
|
||||||
|
*/
|
||||||
|
public function resend(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Ope */
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// 重発可能か確認
|
||||||
|
if (!$this->otpService->canResend($user)) {
|
||||||
|
$waitSeconds = $this->otpService->getResendWaitSeconds($user);
|
||||||
|
|
||||||
|
return back()->with('error', "後 {$waitSeconds} 秒待機してからリクエストしてください。");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 新しい OTP コードを発行
|
||||||
|
$otpCode = $this->otpService->issue($user);
|
||||||
|
|
||||||
|
// ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出
|
||||||
|
$operatorEmails = explode(';', trim($user->ope_mail));
|
||||||
|
$primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail);
|
||||||
|
|
||||||
|
Log::info('OTP 再送メール送信開始: ' . $primaryEmail);
|
||||||
|
|
||||||
|
// メール送信
|
||||||
|
Mail::to($primaryEmail)->send(new EmailOtpMail(
|
||||||
|
$otpCode,
|
||||||
|
$user->name ?? 'ユーザー'
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::info('OTP 再送メール送信完了: ' . $primaryEmail);
|
||||||
|
|
||||||
|
return back()->with('success', 'OTPコードを再送信しました。');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('OTP resend error: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'user_id' => $user->ope_id ?? null,
|
||||||
|
'user_email' => $user->ope_mail ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'OTP送信に失敗しました。もう一度お試しください。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ForgotPasswordController extends Controller
|
class ForgotPasswordController extends Controller
|
||||||
{
|
{
|
||||||
@ -50,12 +51,22 @@ class ForgotPasswordController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
// メール送信
|
// メール送信
|
||||||
$resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
|
try {
|
||||||
Mail::raw("下記URLからパスワード再設定を行ってください。\n\n{$resetUrl}", function ($message) use ($user) {
|
$resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
|
||||||
$message->to($user->ope_mail)
|
|
||||||
->subject('パスワード再設定のご案内');
|
Mail::raw("下記URLからパスワード再設定を行ってください。\n\n{$resetUrl}", function ($message) use ($user) {
|
||||||
});
|
$message->to($user->ope_mail)
|
||||||
|
->from(config('mail.from.address'), config('mail.from.name'))
|
||||||
|
->subject('パスワード再設定のご案内');
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('ForgotPassword mail send failed', [
|
||||||
|
'to' => $user->ope_mail,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
return back()->withErrors(['email' => 'メール送信に失敗しました。サーバログを確認してください。']);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('status', 'パスワード再設定メールを送信しました。');
|
return back()->with('status', 'パスワード再設定メールを送信しました。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,12 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\EmailOtpMail;
|
||||||
|
use App\Services\EmailOtpService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@ -135,6 +139,10 @@ class LoginController extends Controller
|
|||||||
/**
|
/**
|
||||||
* ログイン成功時のレスポンス
|
* ログイン成功時のレスポンス
|
||||||
*
|
*
|
||||||
|
* OTP認証チェック:
|
||||||
|
* - 24時間以内に OTP 認証済みの場合:/home にリダイレクト
|
||||||
|
* - 未認証の場合:OTP メール送信 → /otp にリダイレクト
|
||||||
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
@ -147,7 +155,45 @@ class LoginController extends Controller
|
|||||||
// ここで保持する値も login_id(入力名は ope_id のまま)
|
// ここで保持する値も login_id(入力名は ope_id のまま)
|
||||||
$request->session()->put('login_ope_id', $request->input('ope_id'));
|
$request->session()->put('login_ope_id', $request->input('ope_id'));
|
||||||
|
|
||||||
return redirect()->intended($this->redirectTo);
|
// OTP認証チェック
|
||||||
|
$otpService = app(EmailOtpService::class);
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// 24時間以内に OTP 認証済みの場合
|
||||||
|
if ($otpService->isOtpRecent($user)) {
|
||||||
|
return redirect()->intended($this->redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OTP 未認証の場合:OTP コード発行 → メール送信 → /otp にリダイレクト
|
||||||
|
try {
|
||||||
|
$otpCode = $otpService->issue($user);
|
||||||
|
|
||||||
|
// ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出
|
||||||
|
$operatorEmails = explode(';', trim($user->ope_mail));
|
||||||
|
$primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail);
|
||||||
|
|
||||||
|
Log::info('OTP メール送信開始: ' . $primaryEmail);
|
||||||
|
|
||||||
|
Mail::to($primaryEmail)->send(new EmailOtpMail(
|
||||||
|
$otpCode,
|
||||||
|
$user->name ?? 'ユーザー'
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::info('OTP メール送信完了: ' . $primaryEmail);
|
||||||
|
|
||||||
|
return redirect()->route('otp.show')
|
||||||
|
->with('info', 'OTP認証コードをメール送信しました。');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('OTP issue/send failed: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'user_id' => $user->ope_id ?? null,
|
||||||
|
'user_email' => $user->ope_mail ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// メール送信エラー時は home にリダイレクトするか、カスタムエラーを返す
|
||||||
|
return redirect($this->redirectTo)
|
||||||
|
->with('warning', 'OTP認証メールの送信に失敗しました。');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\City;
|
||||||
|
use App\Services\MenuAccessService;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
@ -22,10 +24,15 @@ class HomeController extends Controller
|
|||||||
* アプリケーションのダッシュボードを表示
|
* アプリケーションのダッシュボードを表示
|
||||||
* 認証後のホーム画面
|
* 認証後のホーム画面
|
||||||
*
|
*
|
||||||
|
* @param MenuAccessService $menuAccessService メニューアクセス制御サービス
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index(MenuAccessService $menuAccessService)
|
||||||
{
|
{
|
||||||
return view('home');
|
// ログイン中のオペレータが表示可能な自治体一覧を取得
|
||||||
|
$visibleCities = $menuAccessService->visibleCities();
|
||||||
|
$isSorin = $menuAccessService->isSorin();
|
||||||
|
|
||||||
|
return view('home', compact('visibleCities', 'isSorin'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
app/Http/Middleware/EnsureOtpVerified.php
Normal file
57
app/Http/Middleware/EnsureOtpVerified.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Services\EmailOtpService;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP 認証チェックミドルウェア
|
||||||
|
*
|
||||||
|
* ログイン後、OTP 認証が完了していないユーザーを OTP 入力ページにリダイレクト
|
||||||
|
* 24時間以内に OTP 認証が完了している場合はスキップ
|
||||||
|
*/
|
||||||
|
class EnsureOtpVerified
|
||||||
|
{
|
||||||
|
protected EmailOtpService $otpService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* コンストラクタ
|
||||||
|
*/
|
||||||
|
public function __construct(EmailOtpService $otpService)
|
||||||
|
{
|
||||||
|
$this->otpService = $otpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* リクエストを処理
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Closure $next
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
// ユーザーが認証されていない場合はスキップ
|
||||||
|
if (!$request->user()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OTP ページ関連のリクエストはスキップ(無限ループ防止)
|
||||||
|
// /otp /* のパターンを許可
|
||||||
|
if ($request->routeIs(['otp.show', 'otp.verify', 'otp.resend'])) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24時間以内に OTP 認証が完了している場合はスキップ
|
||||||
|
if ($this->otpService->isOtpRecent($request->user())) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OTP 認証が必要な場合は OTP ページにリダイレクト
|
||||||
|
return redirect()->route('otp.show')
|
||||||
|
->with('info', 'セキュリティ確認のため OTP 認証が必要です。');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Http/Middleware/ShareMenuAccessData.php
Normal file
90
app/Http/Middleware/ShareMenuAccessData.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Services\MenuAccessService;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メニューアクセス制御データをビューに共有する Middleware
|
||||||
|
*
|
||||||
|
* すべてのビューで $isSorin と $visibleCities が利用可能になります。
|
||||||
|
* また、nav bar に表示される ハード異常・タスク情報も同時に共有します。
|
||||||
|
*/
|
||||||
|
class ShareMenuAccessData
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next)
|
||||||
|
{
|
||||||
|
$menuAccessService = app(MenuAccessService::class);
|
||||||
|
|
||||||
|
// メニュー関連データ
|
||||||
|
$viewData = [
|
||||||
|
'isSorin' => $menuAccessService->isSorin(),
|
||||||
|
'visibleCities' => $menuAccessService->visibleCities(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Nav bar に表示される ハード異常・タスク件数を取得
|
||||||
|
if (auth()->check()) {
|
||||||
|
// ハード異常(que_class > 99)かつステータスが未対応(1)または進行中(2)
|
||||||
|
$hardwareIssues = DB::table('operator_que as oq')
|
||||||
|
->leftJoin('user as u', 'oq.user_id', '=', 'u.user_id')
|
||||||
|
->leftJoin('park as p', 'oq.park_id', '=', 'p.park_id')
|
||||||
|
->select(
|
||||||
|
'oq.que_id', 'oq.que_class', 'oq.que_comment',
|
||||||
|
'oq.created_at', 'oq.updated_at', 'oq.que_status'
|
||||||
|
)
|
||||||
|
->where('oq.que_class', '>', 99)
|
||||||
|
->whereIn('oq.que_status', [1, 2])
|
||||||
|
->orderBy('oq.created_at', 'DESC')
|
||||||
|
->limit(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// タスク(que_class < 99)かつステータスが未対応(1)または進行中(2)
|
||||||
|
$taskIssues = DB::table('operator_que as oq')
|
||||||
|
->leftJoin('user as u', 'oq.user_id', '=', 'u.user_id')
|
||||||
|
->leftJoin('park as p', 'oq.park_id', '=', 'p.park_id')
|
||||||
|
->select(
|
||||||
|
'oq.que_id', 'oq.que_class', 'oq.que_comment',
|
||||||
|
'oq.created_at', 'oq.updated_at', 'oq.que_status'
|
||||||
|
)
|
||||||
|
->where('oq.que_class', '<', 99)
|
||||||
|
->whereIn('oq.que_status', [1, 2])
|
||||||
|
->orderBy('oq.created_at', 'DESC')
|
||||||
|
->limit(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// ハード異常・タスク件数計算
|
||||||
|
$hardCount = DB::table('operator_que')
|
||||||
|
->where('que_class', '>', 99)
|
||||||
|
->whereIn('que_status', [1, 2])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$taskCount = DB::table('operator_que')
|
||||||
|
->where('que_class', '<', 99)
|
||||||
|
->whereIn('que_status', [1, 2])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// 最新のハード異常・タスク日時
|
||||||
|
$hardLatest = $hardwareIssues->first()?->created_at;
|
||||||
|
$taskLatest = $taskIssues->first()?->created_at;
|
||||||
|
|
||||||
|
// Nav bar 関連データをマージ
|
||||||
|
$viewData = array_merge($viewData, [
|
||||||
|
'hardCount' => $hardCount,
|
||||||
|
'hardLatest' => $hardLatest,
|
||||||
|
'latestHards' => $hardwareIssues,
|
||||||
|
'taskCount' => $taskCount,
|
||||||
|
'taskLatest' => $taskLatest,
|
||||||
|
'latestTasks' => $taskIssues,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// すべてのビューでこれらのデータが利用可能
|
||||||
|
View::share($viewData);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Mail/EmailOtpMail.php
Normal file
69
app/Mail/EmailOtpMail.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP メール送信クラス
|
||||||
|
*/
|
||||||
|
class EmailOtpMail extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コード(6桁)
|
||||||
|
*/
|
||||||
|
public string $otpCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* オペレータ名
|
||||||
|
*/
|
||||||
|
public string $operatorName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* コンストラクタ
|
||||||
|
*/
|
||||||
|
public function __construct(string $otpCode, string $operatorName)
|
||||||
|
{
|
||||||
|
$this->otpCode = $otpCode;
|
||||||
|
$this->operatorName = $operatorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メールのエンベロープ
|
||||||
|
*/
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'ログイン確認用OTPコード(有効期限:10分間)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メールのコンテンツ
|
||||||
|
*/
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'emails.otp',
|
||||||
|
with: [
|
||||||
|
'otpCode' => $this->otpCode,
|
||||||
|
'operatorName' => $this->operatorName,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メールの添付ファイル
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ class City extends Model
|
|||||||
'print_layout',
|
'print_layout',
|
||||||
'city_user',
|
'city_user',
|
||||||
'city_remarks',
|
'city_remarks',
|
||||||
|
'management_id',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
@ -26,11 +27,40 @@ class City extends Model
|
|||||||
public static function getList(?int $operatorId = null): array
|
public static function getList(?int $operatorId = null): array
|
||||||
{
|
{
|
||||||
return static::query()
|
return static::query()
|
||||||
->when($operatorId, fn ($q) => $q->where('operator_id', $operatorId))
|
->when($operatorId, fn($q) => $q->where('operator_id', $operatorId))
|
||||||
->orderBy('city_name')
|
->orderBy('city_name')
|
||||||
->pluck('city_name', 'city_id')
|
->pluck('city_name', 'city_id')
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
}
|
* この都市が属する運営元を取得
|
||||||
|
*/
|
||||||
|
public function management(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Management::class, 'management_id', 'management_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自治体別ダッシュボード画面の表示
|
||||||
|
*
|
||||||
|
* 指定された city_id に基づいて都市情報を取得し、
|
||||||
|
* 該当データが存在しない場合は 404 エラーを返します。
|
||||||
|
* 正常に取得できた場合は、ダッシュボード画面を表示します。
|
||||||
|
*
|
||||||
|
* @param int $city_id 都市ID
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function dashboard($city_id)
|
||||||
|
{
|
||||||
|
$city = City::find($city_id);
|
||||||
|
if (!$city) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ここに自治体別ダッシュボードの処理を書く
|
||||||
|
return view('admin.CityMaster.dashboard', [
|
||||||
|
'city' => $city,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
23
app/Models/Feature.php
Normal file
23
app/Models/Feature.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Feature extends Model
|
||||||
|
{
|
||||||
|
// テーブル名(Laravel規約なら省略可だが明示)
|
||||||
|
protected $table = 'features';
|
||||||
|
|
||||||
|
// 主キー
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
// タイムスタンプ使用
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
// 一括代入許可カラム
|
||||||
|
protected $fillable = [
|
||||||
|
'code', // 機能コード
|
||||||
|
'name', // 機能名
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -19,9 +19,7 @@ class Ope extends Authenticatable
|
|||||||
// オペレータタイプ定数(旧システムから継承)
|
// オペレータタイプ定数(旧システムから継承)
|
||||||
const OPE_TYPE = [
|
const OPE_TYPE = [
|
||||||
'管理者',
|
'管理者',
|
||||||
'マネージャー',
|
'職員',
|
||||||
'オペレーター',
|
|
||||||
'エリアマネージャー',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $table = 'ope'; // データベーステーブル名(旧システムと同じ)
|
protected $table = 'ope'; // データベーステーブル名(旧システムと同じ)
|
||||||
@ -29,7 +27,7 @@ class Ope extends Authenticatable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 一括代入可能な属性
|
* 一括代入可能な属性
|
||||||
* Laravel 5.7から引き継いだフィールド構成
|
* Laravel 5.7から引き継いだフィールド構成 + OTP認証フィールド
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'//TODO オペレータID not found in database specs',
|
'//TODO オペレータID not found in database specs',
|
||||||
@ -58,6 +56,11 @@ class Ope extends Authenticatable
|
|||||||
'ope_auth4',
|
'ope_auth4',
|
||||||
'ope_quit_flag',
|
'ope_quit_flag',
|
||||||
'ope_quitday',
|
'ope_quitday',
|
||||||
|
// OTP認証関連フィールド(メール二段階認証)
|
||||||
|
'email_otp_code_hash',
|
||||||
|
'email_otp_expires_at',
|
||||||
|
'email_otp_last_sent_at',
|
||||||
|
'email_otp_verified_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,4 +181,12 @@ class Ope extends Authenticatable
|
|||||||
{
|
{
|
||||||
return self::pluck('ope_name', 'ope_id');
|
return self::pluck('ope_name', 'ope_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* このオペレータが属する運営元を取得
|
||||||
|
*/
|
||||||
|
public function management(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Management::class, 'management_id', 'management_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
65
app/Models/OpePermission.php
Normal file
65
app/Models/OpePermission.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class OpePermission extends Model
|
||||||
|
{
|
||||||
|
// テーブル名
|
||||||
|
protected $table = 'ope_permission';
|
||||||
|
|
||||||
|
// 主キー
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
// created_at / updated_at を使用
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
// 一括代入許可カラム
|
||||||
|
protected $fillable = [
|
||||||
|
'municipality_id', // 自治体ID(外部キー)
|
||||||
|
'feature_id', // 機能ID(外部キー)
|
||||||
|
'permission_id', // 操作権限ID(外部キー)
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 機能単位で権限を置換(自治体単位)
|
||||||
|
* ※ municipality_id + feature_id の組み合わせを置換する
|
||||||
|
*/
|
||||||
|
public static function replaceByFeature(
|
||||||
|
int $municipalityId,
|
||||||
|
int $featureId,
|
||||||
|
array $permissionIds
|
||||||
|
): void {
|
||||||
|
// ※既存削除
|
||||||
|
self::query()
|
||||||
|
->where('municipality_id', $municipalityId)
|
||||||
|
->where('feature_id', $featureId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
// ※新規追加
|
||||||
|
$permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
|
||||||
|
foreach ($permissionIds as $pid) {
|
||||||
|
self::create([
|
||||||
|
'municipality_id' => $municipalityId,
|
||||||
|
'feature_id' => $featureId,
|
||||||
|
'permission_id' => $pid,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 付与済み権限ID一覧を取得(自治体単位)
|
||||||
|
*/
|
||||||
|
public static function getPermissionIds(
|
||||||
|
int $municipalityId,
|
||||||
|
int $featureId
|
||||||
|
): array {
|
||||||
|
return self::query()
|
||||||
|
->where('municipality_id', $municipalityId)
|
||||||
|
->where('feature_id', $featureId)
|
||||||
|
->pluck('permission_id')
|
||||||
|
->map(fn ($v) => (int)$v)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/Permission.php
Normal file
32
app/Models/Permission.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Permission extends Model
|
||||||
|
{
|
||||||
|
// テーブル名
|
||||||
|
protected $table = 'permissions';
|
||||||
|
|
||||||
|
// 主キー
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
// created_at / updated_at を使用
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
// 一括代入許可カラム
|
||||||
|
protected $fillable = [
|
||||||
|
'code', // 操作コード(read/create/update/delete/export)
|
||||||
|
'name', // 操作名(閲覧/登録/編集/削除/CSV出力)
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作コードからIDを取得(存在しない場合はnull)
|
||||||
|
*/
|
||||||
|
public static function idByCode(string $code): ?int
|
||||||
|
{
|
||||||
|
$row = self::query()->where('code', $code)->first(['id']);
|
||||||
|
return $row?->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Models/management.php
Normal file
62
app/Models/management.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Management extends Model
|
||||||
|
{
|
||||||
|
/** テーブル名 */
|
||||||
|
protected $table = 'management';
|
||||||
|
|
||||||
|
/** 主キー */
|
||||||
|
protected $primaryKey = 'management_id';
|
||||||
|
|
||||||
|
/** 自動インクリメント */
|
||||||
|
public $incrementing = true;
|
||||||
|
|
||||||
|
/** 主キー型 */
|
||||||
|
protected $keyType = 'int';
|
||||||
|
|
||||||
|
/** タイムスタンプ(created_at / updated_at) */
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
/** 一括代入を許可する項目 */
|
||||||
|
protected $fillable = [
|
||||||
|
'management_name', // 運営元名
|
||||||
|
'management_code', // 運営元コード(URL用)
|
||||||
|
'municipality_flag', // 自治体フラグ
|
||||||
|
'tel', // 電話番号
|
||||||
|
'service_time', // 受付時間
|
||||||
|
'government_approval_required', // 役所承認要否
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 型キャスト */
|
||||||
|
protected $casts = [
|
||||||
|
'management_id' => 'integer',
|
||||||
|
'municipality_flag' => 'integer',
|
||||||
|
'government_approval_required' => 'integer',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 自治体かどうかを判定 */
|
||||||
|
public function isMunicipality(): bool
|
||||||
|
{
|
||||||
|
return (int)$this->municipality_flag === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 役所承認が必要かどうか */
|
||||||
|
public function requiresGovernmentApproval(): bool
|
||||||
|
{
|
||||||
|
return (int)$this->government_approval_required === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* この運営元に属する自治体を取得
|
||||||
|
*/
|
||||||
|
public function cities(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(City::class, 'management_id', 'management_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ use App\Services\ShjNineService;
|
|||||||
use App\Services\ShjTenService;
|
use App\Services\ShjTenService;
|
||||||
use App\Services\ShjSixService;
|
use App\Services\ShjSixService;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Services\MenuAccessService;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@ -109,10 +110,15 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
->limit(5)
|
->limit(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$menuAccessService = app(MenuAccessService::class);
|
||||||
|
$isSorin = $menuAccessService->isSorin();
|
||||||
|
$visibleCities = $menuAccessService->visibleCities();
|
||||||
|
|
||||||
$view->with(compact(
|
$view->with(compact(
|
||||||
'taskCount','taskLatest',
|
'taskCount','taskLatest',
|
||||||
'hardCount','hardLatest',
|
'hardCount','hardLatest',
|
||||||
'latestTasks','latestHards'
|
'latestTasks','latestHards',
|
||||||
|
'isSorin', 'visibleCities'
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
148
app/Services/EmailOtpService.php
Normal file
148
app/Services/EmailOtpService.php
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Ope;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メール OTP 認証サービス
|
||||||
|
*
|
||||||
|
* 6桁の OTP コード生成、送信、検証、および24時間免OTPの判定を担当します
|
||||||
|
*/
|
||||||
|
class EmailOtpService
|
||||||
|
{
|
||||||
|
// OTP コード長(6桁)
|
||||||
|
const OTP_CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
// OTP 有効期限(分)
|
||||||
|
const OTP_VALIDITY_MINUTES = 10;
|
||||||
|
|
||||||
|
// OTP 重発制限時間(秒)
|
||||||
|
const OTP_RESEND_DELAY_SECONDS = 60;
|
||||||
|
|
||||||
|
// 24時間免OTP期限(時間)
|
||||||
|
const OTP_FREE_PERIOD_HOURS = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コードを生成・保存して送信
|
||||||
|
*
|
||||||
|
* 6桁の数字コードを生成し、hash化して保存します
|
||||||
|
* 同時に有効期限と最後の送信時刻を更新します
|
||||||
|
*
|
||||||
|
* @param Ope $ope オペレータモデル
|
||||||
|
* @return string 明文のOTPコード(メール送信用)
|
||||||
|
*/
|
||||||
|
public function issue(Ope $ope): string
|
||||||
|
{
|
||||||
|
// 6桁の数字コードを生成
|
||||||
|
$plainCode = str_pad(random_int(0, 999999), self::OTP_CODE_LENGTH, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// hash化して保存
|
||||||
|
$ope->email_otp_code_hash = Hash::make($plainCode);
|
||||||
|
$ope->email_otp_expires_at = now()->addMinutes(self::OTP_VALIDITY_MINUTES);
|
||||||
|
$ope->email_otp_last_sent_at = now();
|
||||||
|
$ope->save();
|
||||||
|
|
||||||
|
return $plainCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コードを検証
|
||||||
|
*
|
||||||
|
* 入力されたコードが正しく、且つ有効期限内かを確認します
|
||||||
|
*
|
||||||
|
* @param Ope $ope オペレータモデル
|
||||||
|
* @param string $inputCode ユーザーが入力したコード
|
||||||
|
* @return bool 検証結果
|
||||||
|
*/
|
||||||
|
public function verify(Ope $ope, string $inputCode): bool
|
||||||
|
{
|
||||||
|
// 有効期限チェック
|
||||||
|
if (!$ope->email_otp_expires_at || $ope->email_otp_expires_at < now()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash化されたコードと比較
|
||||||
|
if (!Hash::check($inputCode, $ope->email_otp_code_hash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 検証成功:検証完了時刻を記録し、OTPコードをクリア
|
||||||
|
$ope->email_otp_verified_at = now();
|
||||||
|
$ope->email_otp_code_hash = null;
|
||||||
|
$ope->email_otp_expires_at = null;
|
||||||
|
$ope->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP が最近検証されたかを確認(24時間以内)
|
||||||
|
*
|
||||||
|
* @param Ope $ope オペレータモデル
|
||||||
|
* @return bool true: 24時間以内に検証済み、false: 未検証または24時間以上経過
|
||||||
|
*/
|
||||||
|
public function isOtpRecent(Ope $ope): bool
|
||||||
|
{
|
||||||
|
if (!$ope->email_otp_verified_at) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ope->email_otp_verified_at > now()->subHours(self::OTP_FREE_PERIOD_HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重発可能かを確認
|
||||||
|
*
|
||||||
|
* 前回送信から60秒以上経過しているかを確認します
|
||||||
|
*
|
||||||
|
* @param Ope $ope オペレータモデル
|
||||||
|
* @return bool true: 重発可能、false: 待機中
|
||||||
|
*/
|
||||||
|
public function canResend(Ope $ope): bool
|
||||||
|
{
|
||||||
|
if (!$ope->email_otp_last_sent_at) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ope->email_otp_last_sent_at <= now()->subSeconds(self::OTP_RESEND_DELAY_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 次の重発までの待機時間を取得(秒)
|
||||||
|
*
|
||||||
|
* @param Ope $ope オペレータモデル
|
||||||
|
* @return int 待機時間(秒)、0以下は重発可能
|
||||||
|
*/
|
||||||
|
public function getResendWaitSeconds(Ope $ope): int
|
||||||
|
{
|
||||||
|
if (!$ope->email_otp_last_sent_at) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diffSeconds = now()->diffInSeconds($ope->email_otp_last_sent_at, absolute: false);
|
||||||
|
$waitSeconds = self::OTP_RESEND_DELAY_SECONDS - abs($diffSeconds);
|
||||||
|
|
||||||
|
return max(0, $waitSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTP コードのマスク済みメールアドレスを取得
|
||||||
|
*
|
||||||
|
* abc@example.com -> abc***@example.com のようなマスク表示用
|
||||||
|
*
|
||||||
|
* @param string $email メールアドレス
|
||||||
|
* @return string マスク済みメールアドレス
|
||||||
|
*/
|
||||||
|
public function maskEmail(string $email): string
|
||||||
|
{
|
||||||
|
[$name, $domain] = explode('@', $email);
|
||||||
|
|
||||||
|
// 名前部分の最初の1文字を残して、残りは*でマスク
|
||||||
|
$maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 1));
|
||||||
|
|
||||||
|
return "{$maskedName}@{$domain}";
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/Services/MenuAccessService.php
Normal file
133
app/Services/MenuAccessService.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\City;
|
||||||
|
use App\Models\Management;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メニュー・アクセス制御サービス
|
||||||
|
*
|
||||||
|
* ログイン中のオペレータが属する運営元に基づいて、
|
||||||
|
* 表示可能なメニュー項目およびアクセス可能な画面を制御します。
|
||||||
|
* ソーリンは全メニュー/全画面OK、その他は所属自治体のみアクセス可。
|
||||||
|
*/
|
||||||
|
class MenuAccessService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* ログイン中のオペレータを取得
|
||||||
|
*/
|
||||||
|
private function getAuthOperator()
|
||||||
|
{
|
||||||
|
return Auth::user();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ログイン中のオペレータが属する運営元を取得
|
||||||
|
*/
|
||||||
|
private function getAuthManagement(): ?Management
|
||||||
|
{
|
||||||
|
$operator = $this->getAuthOperator();
|
||||||
|
if (!$operator || !isset($operator->management_id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Management::find($operator->management_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ログイン中のオペレータがソーリンであるか判定
|
||||||
|
*
|
||||||
|
* ソーリン(management_name === 'ソーリン')の場合は全メニュー表示可能
|
||||||
|
*
|
||||||
|
* @return bool true: ソーリン, false: その他
|
||||||
|
*/
|
||||||
|
public function isSorin(): bool
|
||||||
|
{
|
||||||
|
$management = $this->getAuthManagement();
|
||||||
|
if (!$management) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $management->management_name === 'ソーリン';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* オペレータが表示可能な自治体リストを取得
|
||||||
|
*
|
||||||
|
* ソーリン: 全自治体(削除フラグなどは既存仕様に合わせる)
|
||||||
|
* その他: ope.city_id で指定された1つの自治体のみ
|
||||||
|
*
|
||||||
|
* @return Collection<City>
|
||||||
|
*/
|
||||||
|
public function visibleCities(): Collection
|
||||||
|
{
|
||||||
|
if ($this->isSorin()) {
|
||||||
|
return City::query()
|
||||||
|
->orderBy('city_name')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非ソーリン時は ope.city_id で指定された自治体のみ
|
||||||
|
$operator = $this->getAuthOperator();
|
||||||
|
if (!$operator || !isset($operator->city_id)) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ope.city_id に一致する city のみを返す
|
||||||
|
$city = City::find($operator->city_id);
|
||||||
|
if (!$city) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect([$city]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定された自治体へのアクセスが許可されているか判定
|
||||||
|
*
|
||||||
|
* ソーリン: 常に true
|
||||||
|
* その他: city.management_id == ope.management_id の場合のみ true
|
||||||
|
*
|
||||||
|
* @param int $cityId 自治体ID
|
||||||
|
* @return bool true: アクセス許可, false: アクセス拒否
|
||||||
|
*/
|
||||||
|
public function canAccessCity(int $cityId): bool
|
||||||
|
{
|
||||||
|
if ($this->isSorin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operator = $this->getAuthOperator();
|
||||||
|
if (!$operator || !isset($operator->management_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// city.management_id が ope.management_id と一致するか確認
|
||||||
|
$city = City::find($cityId);
|
||||||
|
if (!$city) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$city->management_id === (int)$operator->management_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 複数の自治体へのアクセス許可を一括確認
|
||||||
|
*
|
||||||
|
* @param array<int> $cityIds 自治体IDの配列
|
||||||
|
* @return bool すべての自治体へのアクセスが許可されている場合のみ true
|
||||||
|
*/
|
||||||
|
public function canAccessCities(array $cityIds): bool
|
||||||
|
{
|
||||||
|
foreach ($cityIds as $cityId) {
|
||||||
|
if (!$this->canAccessCity($cityId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,15 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
'/shj4a', // SHJ-4A本番用エンドポイント
|
'/shj4a', // SHJ-4A本番用エンドポイント
|
||||||
'/webhook/wellnet', // SHJ-4A開発・デバッグ用エンドポイント
|
'/webhook/wellnet', // SHJ-4A開発・デバッグ用エンドポイント
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// グローバルミドルウェア登録(すべてのリクエストに適用)
|
||||||
|
$middleware->append(\App\Http\Middleware\ShareMenuAccessData::class);
|
||||||
|
|
||||||
|
// ミドルウェアエイリアス登録
|
||||||
|
$middleware->alias([
|
||||||
|
'check.city.access' => \App\Http\Middleware\CheckCityAccess::class,
|
||||||
|
'ensure.otp.verified' => \App\Http\Middleware\EnsureOtpVerified::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|||||||
50
config/view.php
Normal file
50
config/view.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| View Storage Paths
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Most templating systems load templates from disk. Here you may specify
|
||||||
|
| an array of paths that should be checked when loading a view. Of course
|
||||||
|
| the usual Laravel view path has already been registered for you.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'paths' => [
|
||||||
|
resource_path('views'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Compiled View Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines where all the compiled Blade templates will be
|
||||||
|
| stored for your application. Typically, this is within the storage
|
||||||
|
| directory. However, as usual, you are free to change this value.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'compiled' => env(
|
||||||
|
'VIEW_COMPILED_PATH',
|
||||||
|
realpath(storage_path('framework/views'))
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Blade Namespace Paths
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Define custom namespace paths for views. This allows you to organize
|
||||||
|
| views into different directories and reference them using namespace syntax.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'namespaces' => [
|
||||||
|
'mail' => resource_path('views/emails'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
265
resources/views/admin/CityMaster/dashboard.blade.php
Normal file
265
resources/views/admin/CityMaster/dashboard.blade.php
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $city->city_name . ' ダッシュボード')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0 text-dark">{{ $city->city_name }} ダッシュボード</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('information') }}">ホーム</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $city->city_name }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- 統計情報カード -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-info">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ $stats['parks_count'] }}</h3>
|
||||||
|
<p>駐輪場数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-building"></i>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('parks', ['city_id' => $city->city_id]) }}" class="small-box-footer">
|
||||||
|
詳細を見る <i class="fa fa-arrow-circle-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-success">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($stats['contracts_count']) }}</h3>
|
||||||
|
<p>契約数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-file-text"></i>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('regularcontracts') }}" class="small-box-footer">
|
||||||
|
詳細を見る <i class="fa fa-arrow-circle-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-warning">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($stats['users_count']) }}</h3>
|
||||||
|
<p>利用者数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-users"></i>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('users') }}" class="small-box-footer">
|
||||||
|
詳細を見る <i class="fa fa-arrow-circle-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-danger">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($stats['waiting_count']) }}</h3>
|
||||||
|
<p>予約待ち人数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-clock-o"></i>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('reserves') }}" class="small-box-footer">
|
||||||
|
詳細を見る <i class="fa fa-arrow-circle-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第2行:追加情報 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-purple">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($parks->sum('park_capacity') ?? 0) }}</h3>
|
||||||
|
<p>総収容台数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-bicycle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small-box-footer" style="height: 30px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-teal">
|
||||||
|
<div class="inner">
|
||||||
|
@php
|
||||||
|
$totalCapacity = $parks->sum('park_capacity') ?? 0;
|
||||||
|
$utilizationRate = $totalCapacity > 0 ? round(($stats['contracts_count'] / $totalCapacity) * 100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
<h3>{{ $utilizationRate }}%</h3>
|
||||||
|
<p>利用率</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-pie-chart"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small-box-footer" style="height: 30px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-secondary">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($totalCapacity - $stats['contracts_count']) }}</h3>
|
||||||
|
<p>空き台数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-square-o"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small-box-footer" style="height: 30px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="small-box bg-navy">
|
||||||
|
<div class="inner">
|
||||||
|
@php
|
||||||
|
$waitingRate = $stats['contracts_count'] > 0 ? round(($stats['waiting_count'] / $stats['contracts_count']) * 100, 1) : 0;
|
||||||
|
@endphp
|
||||||
|
<h3>{{ $waitingRate }}%</h3>
|
||||||
|
<p>予約待ち率</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-percent"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small-box-footer" style="height: 30px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 駐輪場一覧 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">{{ $city->city_name }}の駐輪場一覧</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<a href="{{ route('parks.add') }}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fa fa-plus"></i> 新規駐輪場追加
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if($parks->count() > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>駐輪場ID</th>
|
||||||
|
<th>駐輪場名</th>
|
||||||
|
<th>住所</th>
|
||||||
|
<th>収容台数</th>
|
||||||
|
<th>状態</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($parks as $park)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $park->park_id }}</td>
|
||||||
|
<td>{{ $park->park_name }}</td>
|
||||||
|
<td>{{ $park->park_address ?? '-' }}</td>
|
||||||
|
<td class="text-right">{{ number_format($park->park_capacity ?? 0) }}台</td>
|
||||||
|
<td>
|
||||||
|
@if($park->park_status == 1)
|
||||||
|
<span class="badge badge-success">稼働中</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">停止中</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('parks.edit', $park->park_id) }}" class="btn btn-sm btn-info">
|
||||||
|
<i class="fa fa-edit"></i> 編集
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fa fa-info-circle"></i> この自治体にはまだ駐輪場が登録されていません。
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- クイックアクション -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">{{ $city->city_name }}関連のクイックアクション</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('parks', ['city_id' => $city->city_id]) }}" class="btn btn-block btn-outline-primary">
|
||||||
|
<i class="fa fa-building"></i><br>
|
||||||
|
駐輪場管理
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('users') }}" class="btn btn-block btn-outline-success">
|
||||||
|
<i class="fa fa-users"></i><br>
|
||||||
|
利用者管理
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('regularcontracts') }}" class="btn btn-block btn-outline-warning">
|
||||||
|
<i class="fa fa-file-text"></i><br>
|
||||||
|
契約管理
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('city_edit', $city->city_id) }}" class="btn btn-block btn-outline-info">
|
||||||
|
<i class="fa fa-cog"></i><br>
|
||||||
|
運営元設定
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 必要に応じてJavaScriptを追加
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
381
resources/views/admin/information/dashboard.blade.php
Normal file
381
resources/views/admin/information/dashboard.blade.php
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '総合ダッシュボード')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0 text-dark">総合ダッシュボード</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('information') }}">ホーム</a></li>
|
||||||
|
<li class="breadcrumb-item active">総合ダッシュボード</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- 全体統計情報 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">全体統計</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-info">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ $totalStats['total_cities'] }}</h3>
|
||||||
|
<p>自治体数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-map-marker"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-success">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ $totalStats['total_parks'] }}</h3>
|
||||||
|
<p>駐輪場数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-building"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-warning">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($totalStats['total_contracts']) }}</h3>
|
||||||
|
<p>総契約数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-file-text"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-danger">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($totalStats['total_waiting']) }}</h3>
|
||||||
|
<p>総予約待ち</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-clock-o"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-purple">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ number_format($totalStats['total_capacity']) }}</h3>
|
||||||
|
<p>総収容台数</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-bicycle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-6">
|
||||||
|
<div class="small-box bg-teal">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>{{ $totalStats['total_utilization_rate'] }}%</h3>
|
||||||
|
<p>全体利用率</p>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-pie-chart"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自治体別統計表 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">自治体別統計</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fa fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped table-hover" id="cityStatsTable">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>自治体名</th>
|
||||||
|
<th class="text-center">駐輪場数</th>
|
||||||
|
<th class="text-center">契約数</th>
|
||||||
|
<th class="text-center">利用者数</th>
|
||||||
|
<th class="text-center">予約待ち</th>
|
||||||
|
<th class="text-center">収容台数</th>
|
||||||
|
<th class="text-center">利用率</th>
|
||||||
|
<th class="text-center">空き台数</th>
|
||||||
|
<th class="text-center">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($cityStats as $stat)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ $stat['city']->city_name }}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ $stat['parks_count'] }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ number_format($stat['contracts_count']) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ number_format($stat['users_count']) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if($stat['waiting_count'] > 0)
|
||||||
|
<span class="badge badge-warning">{{ number_format($stat['waiting_count']) }}</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success">0</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ number_format($stat['capacity']) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@php
|
||||||
|
$rate = $stat['utilization_rate'];
|
||||||
|
$badgeClass = $rate >= 90 ? 'danger' : ($rate >= 70 ? 'warning' : 'success');
|
||||||
|
@endphp
|
||||||
|
<span class="badge badge-{{ $badgeClass }}">{{ $rate }}%</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if($stat['available_spaces'] > 0)
|
||||||
|
<span class="text-success">{{ number_format($stat['available_spaces']) }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-danger">満車</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ route('city_dashboard', ['city_id' => $stat['city']->city_id]) }}"
|
||||||
|
class="btn btn-sm btn-primary">
|
||||||
|
<i class="fa fa-eye"></i> 詳細
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 利用率チャート -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">自治体別利用率</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="utilizationChart" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">予約待ち状況</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="waitingChart" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- アラート表示 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card card-warning">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">⚠️ 注意が必要な自治体</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@php
|
||||||
|
$alertCities = collect($cityStats)->filter(function($stat) {
|
||||||
|
return $stat['utilization_rate'] >= 90 || $stat['waiting_count'] > 10;
|
||||||
|
});
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($alertCities->count() > 0)
|
||||||
|
<div class="row">
|
||||||
|
@foreach($alertCities as $stat)
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h5><i class="icon fa fa-exclamation-triangle"></i> {{ $stat['city']->city_name }}</h5>
|
||||||
|
@if($stat['utilization_rate'] >= 90)
|
||||||
|
<p>利用率が {{ $stat['utilization_rate'] }}% と高くなっています</p>
|
||||||
|
@endif
|
||||||
|
@if($stat['waiting_count'] > 10)
|
||||||
|
<p>予約待ちが {{ $stat['waiting_count'] }}人 います</p>
|
||||||
|
@endif
|
||||||
|
<a href="{{ route('city_dashboard', ['city_id' => $stat['city']->city_id]) }}"
|
||||||
|
class="btn btn-sm btn-warning">詳細確認</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="icon fa fa-check"></i> 現在、特に注意が必要な自治体はありません。
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// データテーブル初期化
|
||||||
|
$('#cityStatsTable').DataTable({
|
||||||
|
"language": {
|
||||||
|
"url": "//cdn.datatables.net/plug-ins/1.10.21/i18n/Japanese.json"
|
||||||
|
},
|
||||||
|
"pageLength": 10,
|
||||||
|
"order": [[ 6, "desc" ]], // 利用率で降順ソート
|
||||||
|
"columnDefs": [
|
||||||
|
{ "orderable": false, "targets": 8 } // 操作列はソート無効
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 利用率チャート(円グラフ)
|
||||||
|
var ctxUtil = document.getElementById('utilizationChart').getContext('2d');
|
||||||
|
var utilizationChart = new Chart(ctxUtil, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
@foreach($cityStats as $stat)
|
||||||
|
'{{ $stat['city']->city_name }} ({{ $stat['utilization_rate'] }}%)',
|
||||||
|
@endforeach
|
||||||
|
],
|
||||||
|
datasets: [{
|
||||||
|
label: '利用率',
|
||||||
|
data: [
|
||||||
|
@foreach($cityStats as $stat)
|
||||||
|
{{ $stat['utilization_rate'] }},
|
||||||
|
@endforeach
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.8)',
|
||||||
|
'rgba(54, 162, 235, 0.8)',
|
||||||
|
'rgba(255, 206, 86, 0.8)',
|
||||||
|
'rgba(75, 192, 192, 0.8)',
|
||||||
|
'rgba(153, 102, 255, 0.8)',
|
||||||
|
'rgba(255, 159, 64, 0.8)',
|
||||||
|
'rgba(199, 199, 199, 0.8)',
|
||||||
|
'rgba(83, 102, 255, 0.8)',
|
||||||
|
'rgba(255, 193, 7, 0.8)',
|
||||||
|
'rgba(220, 53, 69, 0.8)',
|
||||||
|
'rgba(108, 117, 125, 0.8)',
|
||||||
|
'rgba(40, 167, 69, 0.8)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 99, 132, 1)',
|
||||||
|
'rgba(54, 162, 235, 1)',
|
||||||
|
'rgba(255, 206, 86, 1)',
|
||||||
|
'rgba(75, 192, 192, 1)',
|
||||||
|
'rgba(153, 102, 255, 1)',
|
||||||
|
'rgba(255, 159, 64, 1)',
|
||||||
|
'rgba(199, 199, 199, 1)',
|
||||||
|
'rgba(83, 102, 255, 1)',
|
||||||
|
'rgba(255, 193, 7, 1)',
|
||||||
|
'rgba(220, 53, 69, 1)',
|
||||||
|
'rgba(108, 117, 125, 1)',
|
||||||
|
'rgba(40, 167, 69, 1)'
|
||||||
|
],
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
boxWidth: 12,
|
||||||
|
padding: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
var label = context.label.split(' (')[0]; // 自治体名のみ取得
|
||||||
|
var value = context.parsed;
|
||||||
|
var total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||||
|
var percentage = ((value / total) * 100).toFixed(1);
|
||||||
|
return label + ': ' + value + '% (全体の' + percentage + '%)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 予約待ちチャート
|
||||||
|
var ctxWait = document.getElementById('waitingChart').getContext('2d');
|
||||||
|
var waitingChart = new Chart(ctxWait, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
@foreach($cityStats as $stat)
|
||||||
|
'{{ $stat['city']->city_name }}',
|
||||||
|
@endforeach
|
||||||
|
],
|
||||||
|
datasets: [{
|
||||||
|
label: '予約待ち人数',
|
||||||
|
data: [
|
||||||
|
@foreach($cityStats as $stat)
|
||||||
|
{{ $stat['waiting_count'] }},
|
||||||
|
@endforeach
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.8)',
|
||||||
|
'rgba(54, 162, 235, 0.8)',
|
||||||
|
'rgba(255, 206, 86, 0.8)',
|
||||||
|
'rgba(75, 192, 192, 0.8)',
|
||||||
|
'rgba(153, 102, 255, 0.8)',
|
||||||
|
'rgba(255, 159, 64, 0.8)',
|
||||||
|
'rgba(199, 199, 199, 0.8)',
|
||||||
|
'rgba(83, 102, 255, 0.8)'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@ -169,7 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endfor
|
@endfor
|
||||||
{{-- ▲ キュー1〜13 --}}
|
{{-- ▲ キュー1〜13 --}}
|
||||||
|
{{--
|
||||||
<!-- 管理者権限付与 -->
|
<!-- 管理者権限付与 -->
|
||||||
<div class="form-group col-3">
|
<div class="form-group col-3">
|
||||||
<label>{{__('validation.attributes.ope_auth1')}}</label>
|
<label>{{__('validation.attributes.ope_auth1')}}</label>
|
||||||
@ -245,7 +245,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
--}}
|
||||||
<!-- 退職フラグ -->
|
<!-- 退職フラグ -->
|
||||||
<div class="form-group col-3">
|
<div class="form-group col-3">
|
||||||
<label>{{__('validation.attributes.ope_quit_flag')}}</label>
|
<label>{{__('validation.attributes.ope_quit_flag')}}</label>
|
||||||
@ -278,6 +278,51 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- /.form group - 退職日 -->
|
<!-- /.form group - 退職日 -->
|
||||||
|
|
||||||
|
<!-- 機能(画面) -->
|
||||||
|
<div class="form-group col-3">
|
||||||
|
<label>機能(画面)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<select id="feature_id"
|
||||||
|
name="feature_id"
|
||||||
|
class="form-control form-control-lg permission-control">
|
||||||
|
<option value="">機能を選択してください</option>
|
||||||
|
@foreach($features as $feature)
|
||||||
|
<option value="{{ $feature->id }}"
|
||||||
|
{{ (string)old('feature_id', $selectedFeatureId ?? '') === (string)$feature->id ? 'selected' : '' }}>
|
||||||
|
{{ $feature->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 操作権限 -->
|
||||||
|
<div class="form-group col-3 mt-3">
|
||||||
|
<label>操作権限</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-9 mt-3">
|
||||||
|
<div id="permission_box">
|
||||||
|
@foreach($permissions as $perm)
|
||||||
|
<label class="mr-4 mb-0 d-inline-flex align-items-center">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="perm-checkbox permission-control mr-1"
|
||||||
|
name="permission_ids[]"
|
||||||
|
value="{{ $perm->id }}">
|
||||||
|
{{ $perm->name }}
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
※ 機能を選択後、現在設定済みの権限が自動で反映されます。
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 保存ボタンは既存のままでOK -->
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{-- ▼ 下部ボタン --}}
|
{{-- ▼ 下部ボタン --}}
|
||||||
@ -307,3 +352,67 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* オペレータ種別に応じて
|
||||||
|
* 0: 管理者 → 機能・権限 非活性
|
||||||
|
* 1: 職員 → 機能・権限 活性
|
||||||
|
*/
|
||||||
|
function togglePermissionArea() {
|
||||||
|
const opeType = document.querySelector('select[name="ope_type"]').value;
|
||||||
|
const controls = document.querySelectorAll('.permission-control');
|
||||||
|
|
||||||
|
if (opeType === '0') { // 管理者
|
||||||
|
controls.forEach(el => {
|
||||||
|
el.disabled = true;
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
el.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
controls.forEach(el => {
|
||||||
|
el.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 機能選択時:既存権限を反映
|
||||||
|
*/
|
||||||
|
async function refreshPermissionChecks() {
|
||||||
|
const featureId = document.getElementById('feature_id').value;
|
||||||
|
const opeId = {{ (int)($record->ope_id ?? 0) }};
|
||||||
|
const opeType = document.querySelector('select[name="ope_type"]').value;
|
||||||
|
|
||||||
|
// 管理者は個別権限設定しない
|
||||||
|
if (opeType === '0') return;
|
||||||
|
|
||||||
|
// 一旦すべて解除
|
||||||
|
document.querySelectorAll('.perm-checkbox').forEach(cb => cb.checked = false);
|
||||||
|
|
||||||
|
if (!featureId || !opeId) return;
|
||||||
|
|
||||||
|
const res = await fetch(`/opes/${opeId}/permissions?feature_id=${featureId}`);
|
||||||
|
const ids = await res.json();
|
||||||
|
|
||||||
|
const set = new Set(ids.map(String));
|
||||||
|
document.querySelectorAll('.perm-checkbox').forEach(cb => {
|
||||||
|
if (set.has(String(cb.value))) cb.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- イベント登録 ---------- */
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
togglePermissionArea();
|
||||||
|
refreshPermissionChecks();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('select[name="ope_type"]').addEventListener('change', () => {
|
||||||
|
togglePermissionArea();
|
||||||
|
refreshPermissionChecks();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('feature_id').addEventListener('change', refreshPermissionChecks);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,26 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3 align-items-center">
|
||||||
|
<label class="col-sm-2 col-form-label fw-bold">エリア</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<select name="park_id" class="form-select">
|
||||||
|
<option value="">全て</option>
|
||||||
|
@foreach($parks as $park)
|
||||||
|
<option value="{{ $park->park_id }}"
|
||||||
|
|
||||||
|
{{ (string)request('park_id') === (string)$park->park_id ? 'selected' : '' }}>
|
||||||
|
{{ $park->park_name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row mb-3 align-items-center">
|
<div class="row mb-3 align-items-center">
|
||||||
<label class="col-sm-2 col-form-label fw-bold py-0">発行日</label>
|
<label class="col-sm-2 col-form-label fw-bold py-0">発行日</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
@ -76,6 +96,7 @@
|
|||||||
<div class="col-sm-12 text-end">
|
<div class="col-sm-12 text-end">
|
||||||
<button type="submit" class="btn btn-default me-2">絞り込み</button>
|
<button type="submit" class="btn btn-default me-2">絞り込み</button>
|
||||||
<a href="{{ route('seals') }}" class="btn btn-default">解除</a>
|
<a href="{{ route('seals') }}" class="btn btn-default">解除</a>
|
||||||
|
<button type="submit" class="btn btn-default me-2">CSV出力</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
171
resources/views/auth/otp.blade.php
Normal file
171
resources/views/auth/otp.blade.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
@extends('layouts.login')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-logo">
|
||||||
|
<a><b>So-Manager </b>{{ __('管理パネル') }}</a>
|
||||||
|
</div>
|
||||||
|
<!-- /.login-logo -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{{ __('セキュリティ認証') }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body login-card-body">
|
||||||
|
<p class="login-box-msg mb-3">
|
||||||
|
{{ __('登録されたメールアドレス') }} <strong>{{ $maskedEmail }}</strong>
|
||||||
|
<br/>{{ __('に6桁の認証コードが送信されました。') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<strong>{{ __('エラー') }}</strong>
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('error') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('otp.verify') }}" method="POST" novalidate>
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="code" class="form-label">
|
||||||
|
<strong>{{ __('認証コード(6桁)') }}</strong>
|
||||||
|
</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="code"
|
||||||
|
name="code"
|
||||||
|
class="form-control form-control-lg text-center"
|
||||||
|
placeholder="000000"
|
||||||
|
maxlength="6"
|
||||||
|
pattern="[0-9]{6}"
|
||||||
|
inputmode="numeric"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
value="{{ old('code') }}"
|
||||||
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="fa fa-key input-group-text"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="d-block text-muted">
|
||||||
|
{{ __('数字のみで6桁入力してください。有効期限は10分です。') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt40">
|
||||||
|
<div class="col-12 col-lg-8 offset-0 offset-lg-2">
|
||||||
|
<button type="submit" class="btn btn-lg btn-primary btn-block btn-flat">
|
||||||
|
{{ __('認証する') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<form action="{{ route('otp.resend') }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
id="resendBtn"
|
||||||
|
@if ($resendWaitSeconds > 0) disabled @endif
|
||||||
|
>
|
||||||
|
<span id="resendText">
|
||||||
|
@if ($resendWaitSeconds > 0)
|
||||||
|
{{ __('コード再送(') }}{{ $resendWaitSeconds }}{{ __('秒待機)') }}
|
||||||
|
@else
|
||||||
|
{{ __('コードを再送信する') }}
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<a href="{{ route('logout') }}" class="text-muted small">
|
||||||
|
{{ __('ログアウト') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 入力フィールドの自動フォーカス制御
|
||||||
|
const codeInput = document.getElementById('code');
|
||||||
|
const resendBtn = document.getElementById('resendBtn');
|
||||||
|
const resendText = document.getElementById('resendText');
|
||||||
|
|
||||||
|
// 数字のみ許可
|
||||||
|
codeInput.addEventListener('keypress', function(e) {
|
||||||
|
if (!/\d/.test(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 貼り付け時に数字のみを許可
|
||||||
|
codeInput.addEventListener('paste', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const pastedText = (e.clipboardData || window.clipboardData).getData('text');
|
||||||
|
const numbers = pastedText.replace(/\D/g, '').slice(0, 6);
|
||||||
|
codeInput.value = numbers;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 再送ボタンのカウントダウン処理
|
||||||
|
let waitSeconds = {{ $resendWaitSeconds }};
|
||||||
|
|
||||||
|
if (waitSeconds > 0) {
|
||||||
|
const countdown = setInterval(function() {
|
||||||
|
waitSeconds--;
|
||||||
|
if (waitSeconds <= 0) {
|
||||||
|
clearInterval(countdown);
|
||||||
|
resendBtn.disabled = false;
|
||||||
|
resendText.textContent = '{{ __("コードを再送信する") }}';
|
||||||
|
} else {
|
||||||
|
resendText.textContent = '{{ __("コード再送(") }}' + waitSeconds + '{{ __("秒待機)") }}';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#code {
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#code::placeholder {
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
105
resources/views/emails/otp.blade.php
Normal file
105
resources/views/emails/otp.blade.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>セキュリティ認証</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', 'Helvetica Neue', sans-serif, 'Arial';
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.code-box {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #856404;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔐 セキュリティ認証</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ $operatorName }} 様</p>
|
||||||
|
|
||||||
|
<p>いつもご利用いただきありがとうございます。</p>
|
||||||
|
|
||||||
|
<p>ログイン認証用の OTP(ワンタイムパスワード)をお送りしました。</p>
|
||||||
|
|
||||||
|
<h2 style="text-align: center; color: #007bff;">認証コード</h2>
|
||||||
|
|
||||||
|
<div class="code-box">{{ $otpCode }}</div>
|
||||||
|
|
||||||
|
<p style="text-align: center; color: #666; margin: 10px 0;">
|
||||||
|
<strong>⏱️ 有効期限:10分間</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>このコードを入力フォームに入力して、認証を完了してください。</p>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>セキュリティに関するご注意:</strong>
|
||||||
|
<ul style="margin: 5px 0; padding-left: 20px;">
|
||||||
|
<li>このコードは絶対に他の方に教えないでください</li>
|
||||||
|
<li>このメールの送信に心当たりがない場合は、無視してください</li>
|
||||||
|
<li>本システムは認証コードをメールで送信することはありません。リンククリックは不要です。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>ご不明な点やサポートが必要な場合は、お気軽にお問い合わせください。</p>
|
||||||
|
|
||||||
|
<p>よろしくお願いいたします。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>このメールは自動送信です。返信はしないでください。</p>
|
||||||
|
<p>© 2026 So-Manager. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,117 +1,159 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'インフォメーション')
|
@section('title', 'インフォメーション')
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
@php
|
||||||
use App\Models\OperatorLog;
|
use App\Models\OperatorLog;
|
||||||
use App\Models\Ope;
|
use App\Models\Ope;
|
||||||
$logs = OperatorLog::orderByDesc('created_at')->limit(20)->get();
|
$logs = OperatorLog::orderByDesc('created_at')->limit(20)->get();
|
||||||
$operatorNames = [];
|
$operatorNames = [];
|
||||||
$queLabels = [];
|
$queLabels = [];
|
||||||
$queIcons = [];
|
$queIcons = [];
|
||||||
$queClassNums = [];
|
$queClassNums = [];
|
||||||
foreach ($logs as $log) {
|
foreach ($logs as $log) {
|
||||||
// オペレータ名取得
|
// オペレータ名取得
|
||||||
$operatorNames[$log->operator_id] = $operatorNames[$log->operator_id] ?? (\App\Models\Ope::find($log->operator_id)->ope_name ?? $log->operator_id);
|
$operatorNames[$log->operator_id] =
|
||||||
// operator_queからque_class, que_status取得
|
$operatorNames[$log->operator_id] ??
|
||||||
$que = null;
|
(\App\Models\Ope::find($log->operator_id)->ope_name ?? $log->operator_id);
|
||||||
if (!empty($log->user_id) && !empty($log->contract_id)) {
|
// operator_queからque_class, que_status取得
|
||||||
$que = \App\Models\OperatorQue::where('user_id', $log->user_id)->where('contract_id', $log->contract_id)->first();
|
$que = null;
|
||||||
|
if (!empty($log->user_id) && !empty($log->contract_id)) {
|
||||||
|
$que = \App\Models\OperatorQue::where('user_id', $log->user_id)
|
||||||
|
->where('contract_id', $log->contract_id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
// 条件を満たさない場合はスキップ
|
||||||
|
if (!$que || !in_array($que->que_status, [3, 4])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($que->que_class < 100) {
|
||||||
|
$queLabels[$log->operator_log_id] = '<span class="badge badge-primary">タスク</span>';
|
||||||
|
$queIcons[$log->operator_log_id] = '';
|
||||||
|
} elseif ($que->que_class > 99) {
|
||||||
|
$queLabels[$log->operator_log_id] = '<span class="badge badge-danger">ハード異常</span>';
|
||||||
|
$queIcons[$log->operator_log_id] = '';
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$queClassNums[$log->operator_log_id] =
|
||||||
|
\App\Models\OperatorQue::QueClass[$que->que_class] ?? $que->que_class;
|
||||||
}
|
}
|
||||||
// 条件を満たさない場合はスキップ
|
@endphp
|
||||||
if (!$que || !in_array($que->que_status, [3,4])) {
|
<div class="container-fluid" style="background:#f4f6f9;min-height:calc(100vh - 60px);">
|
||||||
continue;
|
<div class="row">
|
||||||
}
|
<!-- メイン -->
|
||||||
if ($que->que_class < 100) {
|
<div class="col-md-12">
|
||||||
$queLabels[$log->operator_log_id] = '<span class="badge badge-primary">タスク</span>';
|
<div class="row mt-4">
|
||||||
$queIcons[$log->operator_log_id] = '';
|
<div class="col-lg-7">
|
||||||
} elseif ($que->que_class > 99) {
|
<div class="card mb-4">
|
||||||
$queLabels[$log->operator_log_id] = '<span class="badge badge-danger">ハード異常</span>';
|
<div class="card-header bg-white font-weight-bold">インフォメーション</div>
|
||||||
$queIcons[$log->operator_log_id] = '';
|
<div class="card-body">
|
||||||
} else {
|
@php
|
||||||
continue;
|
// ▼ 各メニューの件数を取得(que_status は運用に合わせて調整)
|
||||||
}
|
// 例:未処理/処理中を 1,2 として集計
|
||||||
$queClassNums[$log->operator_log_id] = \App\Models\OperatorQue::QueClass[$que->que_class] ?? $que->que_class;
|
$statusTargets = [1, 2];
|
||||||
}
|
|
||||||
@endphp
|
// ▼ QueClass は実際の定義に合わせて値を変更してください
|
||||||
<div class="container-fluid" style="background:#f4f6f9;min-height:calc(100vh - 60px);">
|
// ここでは例として que_class を使って種別を分ける想定
|
||||||
<div class="row">
|
$cntPersonalCheck = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||||
<!-- メイン -->
|
->whereIn('que_class', [
|
||||||
<div class="col-md-12">
|
/* 本人確認の que_class を入れる */
|
||||||
<div class="row mt-4">
|
])
|
||||||
<div class="col-lg-7">
|
->count();
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header bg-white font-weight-bold">インフォメーション</div>
|
$cntCancelRequest = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||||
<div class="card-body">
|
->whereIn('que_class', [
|
||||||
@php
|
/* 解約リクエストの que_class を入れる */
|
||||||
$infoQue = \App\Models\OperatorQue::whereIn('que_status', [1,2])
|
])
|
||||||
->orderByDesc('created_at')
|
->count();
|
||||||
->limit(5)
|
|
||||||
->get();
|
$cntUserInfoChange = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||||
@endphp
|
->whereIn('que_class', [
|
||||||
@if(count($infoQue) > 0)
|
/* ユーザー情報変更の que_class を入れる */
|
||||||
@foreach($infoQue as $q)
|
])
|
||||||
<div class="card mb-3" style="box-shadow:none;border:1px solid #e0e0e0;">
|
->count();
|
||||||
<div class="card-body py-3 px-4" style="position:relative;">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
// ▼ 0埋め(000件 表示用)
|
||||||
<div class="text-secondary" style="font-size:1.1em; margin-right:1em;">
|
$fmt = fn($n) => str_pad((string) $n, 3, '0', STR_PAD_LEFT) . '件';
|
||||||
{{ $q->created_at }}
|
@endphp
|
||||||
</div>
|
|
||||||
@if($q->que_class > 99)
|
{{-- 本人確認処理 --}}
|
||||||
<span class="badge badge-danger" style="font-size:0.95em;">ハード異常</span>
|
<div class="card mb-3" style="box-shadow:none;border:1px solid #e0e0e0;">
|
||||||
@elseif($q->que_class < 100)
|
<div class="card-body py-3 px-4 d-flex align-items-center ">
|
||||||
<span class="badge badge-primary" style="font-size:0.95em;">タスク</span>
|
<a href="{{ route('personal') }}"
|
||||||
@endif
|
style="font-weight:bold;color:#007bff;font-size:1.05em;">
|
||||||
</div>
|
本人確認処理
|
||||||
<div>
|
</a>
|
||||||
<a href="{{ url('/admin/manual_personal_check/edit/'.$q->que_id) }}" style="font-weight:bold;color:#007bff;font-size:1.1em;text-decoration:underline;">
|
<span class="badge badge-primary ml-auto"
|
||||||
{{ $q->getQueClassLabel() }}
|
style="font-size:0.95em;">{{ $fmt($cntPersonalCheck) }}</span>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
</div>
|
||||||
@endif
|
|
||||||
|
{{-- 解約リクエスト --}}
|
||||||
|
<div class="card mb-3" style="box-shadow:none;border:1px solid #e0e0e0;">
|
||||||
|
<div class="card-body py-3 px-4 d-flex align-items-center ">
|
||||||
|
<a href="{{ route('regularcontracts') }}"
|
||||||
|
style="font-weight:bold;color:#007bff;font-size:1.05em;">
|
||||||
|
解約リクエスト
|
||||||
|
</a>
|
||||||
|
<span class="badge badge-primary ml-auto"
|
||||||
|
style="font-size:0.95em;">{{ $fmt($cntCancelRequest) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ユーザー情報変更 --}}
|
||||||
|
<div class="card mb-0" style="box-shadow:none;border:1px solid #e0e0e0;">
|
||||||
|
<div class="card-body py-3 px-4 d-flex align-items-center ">
|
||||||
|
<a href="{{ route('users') }}"
|
||||||
|
style="font-weight:bold;color:#007bff;font-size:1.05em;">
|
||||||
|
ユーザー情報変更
|
||||||
|
</a>
|
||||||
|
<span class="badge badge-primary ml-auto"
|
||||||
|
style="font-size:0.95em;">{{ $fmt($cntUserInfoChange) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-5">
|
||||||
<div class="col-lg-5">
|
<div class="card mb-4">
|
||||||
<div class="card mb-4">
|
<div class="card-header bg-white font-weight-bold">操作履歴</div>
|
||||||
<div class="card-header bg-white font-weight-bold">操作履歴</div>
|
<div class="card-body p-0">
|
||||||
<div class="card-body p-0">
|
<table class="table table-sm mb-0">
|
||||||
<table class="table table-sm mb-0">
|
<thead>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">操作内容</th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
<th scope="col">オペレーター</th>
|
|
||||||
<th scope="col">日時</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@php $hasData = false; @endphp
|
|
||||||
@foreach($logs as $log)
|
|
||||||
@if(isset($queLabels[$log->operator_log_id]))
|
|
||||||
@php $hasData = true; @endphp
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{!! $queIcons[$log->operator_log_id] !!}{!! $queLabels[$log->operator_log_id] !!}</td>
|
<th scope="col">操作内容</th>
|
||||||
<td></td>
|
<th scope="col"></th>
|
||||||
<td>{{ $queClassNums[$log->operator_log_id] }}</td>
|
<th scope="col"></th>
|
||||||
<td>{{ $operatorNames[$log->operator_id] ?? $log->operator_id }}</td>
|
<th scope="col">オペレーター</th>
|
||||||
<td>{{ $log->created_at }}</td>
|
<th scope="col">日時</th>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
</thead>
|
||||||
@endforeach
|
<tbody>
|
||||||
@if(!$hasData)
|
@php $hasData = false; @endphp
|
||||||
<tr><td colspan="5" class="text-center">データがありません</td></tr>
|
@foreach ($logs as $log)
|
||||||
@endif
|
@if (isset($queLabels[$log->operator_log_id]))
|
||||||
</tbody>
|
@php $hasData = true; @endphp
|
||||||
</table>
|
<tr>
|
||||||
|
<td>{!! $queIcons[$log->operator_log_id] !!}{!! $queLabels[$log->operator_log_id] !!}</td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{ $queClassNums[$log->operator_log_id] }}</td>
|
||||||
|
<td>{{ $operatorNames[$log->operator_id] ?? $log->operator_id }}</td>
|
||||||
|
<td>{{ $log->created_at }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@if (!$hasData)
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">データがありません</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -46,17 +46,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<nav class="main-header navbar navbar-expand bg-white navbar-light border-bottom">
|
<nav class="main-header navbar navbar-expand bg-white navbar-light border-bottom">
|
||||||
<!-- Left navbar links -->
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item d-lg-none">
|
|
||||||
<a class="nav-link" data-widget="pushmenu" href="#"><i class="fa fa-bars"></i></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item d-none d-sm-inline-block">
|
|
||||||
<a class="nav-link" style="margin-left:20px;">
|
|
||||||
{{ __('ようこそ、:ope_name様', ['ope_name' => Auth::user()->ope_name]) }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- SEARCH FORM -->
|
<!-- SEARCH FORM -->
|
||||||
<!--
|
<!--
|
||||||
@ -73,7 +63,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Right navbar links -->
|
<!-- Right navbar links -->
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-3">
|
||||||
<!-- ハード異常 件数表示(ドロップダウン任意表示:ここでは件数バッジのみ簡易) -->
|
<!-- ハード異常 件数表示(ドロップダウン任意表示:ここでは件数バッジのみ簡易) -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link text-danger" data-toggle="dropdown" href="#">
|
<a class="nav-link text-danger" data-toggle="dropdown" href="#">
|
||||||
@ -145,6 +135,14 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" style="margin-left:20px;">
|
||||||
|
{{ __('ようこそ、:ope_name様', ['ope_name' => Auth::user()->ope_name]) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout"><i class="fa fa-sign-out"></i> {{ __('ログアウト') }}</a>
|
<a class="nav-link" href="/logout"><i class="fa fa-sign-out"></i> {{ __('ログアウト') }}</a>
|
||||||
</li>
|
</li>
|
||||||
@ -186,10 +184,277 @@
|
|||||||
<nav class="mt-2">
|
<nav class="mt-2">
|
||||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
|
||||||
data-accordion="false">
|
data-accordion="false">
|
||||||
|
|
||||||
|
@if(!$isSorin)
|
||||||
|
<!-- 非ソーリンユーザー用: 運営元メニューを親として構造変更 -->
|
||||||
|
@if($visibleCities && $visibleCities->count() > 0)
|
||||||
|
@php
|
||||||
|
// 現在のページが属する city_id を判定
|
||||||
|
// 1. ルートパラメータから city_id を取得(city_dashboard など)
|
||||||
|
// 2. ない場合は、visibleCities の最初の city_id を使用(非ソーリンユーザーは1つのみ)
|
||||||
|
$currentCityId = request()->route('city_id') ?? ($visibleCities->first()?->city_id ?? null);
|
||||||
|
|
||||||
|
// 現在のページが都市管理ページか通常ページかを判定
|
||||||
|
$isCityRoute = request()->routeIs('city_dashboard') || str_contains(request()->url(), '/city/');
|
||||||
|
|
||||||
|
// 現在のルート名を取得(ハイライト判定用)
|
||||||
|
$currentRoute = app('router')->currentRouteName();
|
||||||
|
|
||||||
|
// 非ソーリンユーザーが閲覧可能なページのルート名
|
||||||
|
$visibleRoutes = [
|
||||||
|
'dashboard', // 総合ダッシュボード
|
||||||
|
'information', // 常時表示インフォメーション
|
||||||
|
'tagissue', // タグ発行キュー処理、履歴表示
|
||||||
|
'seals', // シール発行履歴
|
||||||
|
'periodical', // 定期利用・契約状況
|
||||||
|
'contractor', // 契約者一覧
|
||||||
|
'contractor_List', // 未更新者一覧
|
||||||
|
'update_candidate', // 更新予定者一覧
|
||||||
|
'reservation', // 予約者一覧
|
||||||
|
'personal', // 本人確認手動処理
|
||||||
|
'using_status', // 区画別利用率状況
|
||||||
|
'news', // 一時売り上げ入力 / 最新ニュース登録
|
||||||
|
'users', // 利用者マスタ
|
||||||
|
'regularcontracts', // 定期契約マスタ
|
||||||
|
'reserves', // 定期予約マスタ
|
||||||
|
'usertypes', // 利用者分類マスタ
|
||||||
|
'parks', // 駐輪場マスタ
|
||||||
|
'pricelist', // 料金一覧表
|
||||||
|
'prices', // 駐輪場所、料金マスタ
|
||||||
|
'psections', // 車種区分マスタ
|
||||||
|
'city_dashboard', // 自治体ダッシュボード
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item has-treeview menu-open">
|
||||||
|
<a href="#" class="nav-link active">
|
||||||
|
<i class="nav-icon fa fa-map-marker"></i>
|
||||||
|
<p>
|
||||||
|
{{ __("運営元メニュー") }}
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="display: block;">
|
||||||
|
@foreach($visibleCities as $city)
|
||||||
|
<li class="nav-item has-treeview @if($currentCityId == $city->city_id) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if($currentCityId == $city->city_id) active @endif">
|
||||||
|
<i class="nav-icon fa fa-building-o"></i>
|
||||||
|
<span style="margin-left:10px;">{{ $city->city_name }}</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="display: @if($currentCityId == $city->city_id) block @else none @endif; margin-left: 20px;">
|
||||||
|
<!-- ホーム -->
|
||||||
|
<li class="nav-item has-treeview @if(in_array($currentRoute, ['dashboard', 'information'])) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if(in_array($currentRoute, ['dashboard', 'information'])) active @endif">
|
||||||
|
<i class="nav-icon fa fa-home"></i>
|
||||||
|
<p>
|
||||||
|
ホーム
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 20px; display: @if(in_array($currentRoute, ['dashboard', 'information'])) block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('dashboard') }}" class="nav-link {{ $currentRoute === 'dashboard' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">総合ダッシュボード</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('information') }}" class="nav-link {{ $currentRoute === 'information' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">常時表示インフォメーション</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- タグ・シール管理 -->
|
||||||
|
<li class="nav-item has-treeview @if(in_array($currentRoute, ['tagissue', 'seals'])) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if(in_array($currentRoute, ['tagissue', 'seals'])) active @endif">
|
||||||
|
<i class="nav-icon fa fa-repeat"></i>
|
||||||
|
<span style="margin-left:10px;">タグ・シール管理</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if(in_array($currentRoute, ['tagissue', 'seals'])) block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('tagissue', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'tagissue' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">タグ発行キュー処理、履歴表示</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('seals', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'seals' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">シール発行履歴</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 定期駐輪管理 -->
|
||||||
|
@php
|
||||||
|
$parkingRoutes = ['periodical', 'contractor', 'contractor_List', 'update_candidate', 'reservation', 'personal', 'using_status'];
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item has-treeview @if(in_array($currentRoute, $parkingRoutes)) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if(in_array($currentRoute, $parkingRoutes)) active @endif">
|
||||||
|
<i class="nav-icon fa fa-repeat"></i>
|
||||||
|
<span style="margin-left:10px;">定期駐輪管理</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if(in_array($currentRoute, $parkingRoutes)) block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('periodical', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'periodical' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">定期利用・契約状況</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('contractor', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'contractor' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">契約者一覧</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('contractor_List', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'contractor_List' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">未更新者一覧</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('update_candidate', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'update_candidate' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">更新予定者一覧</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('reservation', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'reservation' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">予約者一覧</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('personal', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'personal' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">本人確認手動処理</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('using_status', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'using_status' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">区画別利用率状況</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 集計業務 -->
|
||||||
|
<li class="nav-item has-treeview @if($currentRoute === 'news') menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if($currentRoute === 'news') active @endif">
|
||||||
|
<i class="nav-icon fa fa-calculator"></i>
|
||||||
|
<span style="margin-left:10px;">集計業務</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if($currentRoute === 'news') block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('news', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'news' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">一時売り上げ入力</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 一般ウェブ管理 -->
|
||||||
|
<li class="nav-item has-treeview @if($currentRoute === 'news') menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if($currentRoute === 'news') active @endif">
|
||||||
|
<i class="nav-icon fa fa-dashboard"></i>
|
||||||
|
<span style="margin-left:10px;">一般ウェブ管理</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if($currentRoute === 'news') block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('news', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'news' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">最新ニュース登録</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 利用者マスタ -->
|
||||||
|
@php
|
||||||
|
$userRoutes = ['users', 'regularcontracts', 'reserves', 'usertypes'];
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item has-treeview @if(in_array($currentRoute, $userRoutes)) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if(in_array($currentRoute, $userRoutes)) active @endif">
|
||||||
|
<i class="nav-icon fa fa-dashboard"></i>
|
||||||
|
<span style="margin-left:10px;">利用者マスタ</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if(in_array($currentRoute, $userRoutes)) block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('users', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'users' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">利用者マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('regularcontracts', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'regularcontracts' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">定期契約マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('reserves', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'reserves' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">定期予約マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('usertypes', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'usertypes' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">利用者分類マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 駐輪場マスタ -->
|
||||||
|
@php
|
||||||
|
$priceRoutes = ['parks', 'pricelist', 'prices', 'psections'];
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item has-treeview @if(in_array($currentRoute, $priceRoutes)) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if(in_array($currentRoute, $priceRoutes)) active @endif">
|
||||||
|
<i class="nav-icon fa fa-th"></i>
|
||||||
|
<span style="margin-left:10px;">駐輪場マスタ</span>
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="margin-left: 40px; display: @if(in_array($currentRoute, $priceRoutes)) block @else none @endif;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('parks', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'parks' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">駐輪場マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('pricelist', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'pricelist' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">料金一覧表</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('prices', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'prices' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">駐輪場所、料金マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('psections', ['city_id' => $city->city_id]) }}" class="nav-link {{ $currentRoute === 'psections' ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">車種区分マスタ</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- ダッシュボード -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('city_dashboard', ['city_id' => $city->city_id]) }}"
|
||||||
|
class="nav-link {{ $currentRoute === 'city_dashboard' && $currentCityId == $city->city_id ? 'active' : '' }}">
|
||||||
|
<i class="nav-icon fa fa-dashboard"></i>
|
||||||
|
<span style="margin-left:10px;">ダッシュボード</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<!-- ソーリンユーザー用: 元のメニュー構造を維持 -->
|
||||||
<!-- OU START -->
|
<!-- OU START -->
|
||||||
<!-- ホーム(親) -->
|
<!-- ホーム(親) -->
|
||||||
<li class="nav-item has-treeview {{ request()->routeIs('information') ? 'menu-open' : '' }}">
|
<li class="nav-item has-treeview {{ request()->routeIs('information') || request()->routeIs('dashboard') ? 'menu-open' : '' }}">
|
||||||
<a href="#" class="nav-link {{ request()->routeIs('information') ? 'active' : '' }}">
|
<a href="#" class="nav-link {{ request()->routeIs('information') || request()->routeIs('dashboard') ? 'active' : '' }}">
|
||||||
<i class="nav-icon fa fa-home"></i>
|
<i class="nav-icon fa fa-home"></i>
|
||||||
<p>
|
<p>
|
||||||
ホーム
|
ホーム
|
||||||
@ -197,12 +462,17 @@
|
|||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<ul class="nav nav-treeview"
|
<ul class="nav nav-treeview"
|
||||||
style="{{ request()->routeIs('information') ? 'display:block;' : 'display:none;' }}">
|
style="{{ request()->routeIs('information') || request()->routeIs('dashboard') ? 'display:block;' : 'display:none;' }}">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('dashboard') }}" class="nav-link {{ request()->routeIs('dashboard') ? 'active' : '' }}">
|
||||||
|
<span style="margin-left:20px;">総合ダッシュボード</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
<span style="margin-left:20px;">ハードウェア異常表示</span>
|
<span style="margin-left:20px;">ハードウェア異常表示</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> -->
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ route('information') }}"
|
<a href="{{ route('information') }}"
|
||||||
@ -724,12 +994,36 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- kin end -->
|
<!-- ソーリンユーザー用: 運営元メニュー(全自治体表示) -->
|
||||||
|
@if($visibleCities && $visibleCities->count() > 0)
|
||||||
|
@php
|
||||||
|
$currentCityId = request()->route('city_id');
|
||||||
|
$isCityRoute = request()->routeIs('city_dashboard') || str_contains(request()->url(), '/city/');
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item has-treeview @if($isCityRoute) menu-open @endif">
|
||||||
|
<a href="#" class="nav-link @if($isCityRoute) active @endif">
|
||||||
|
<i class="nav-icon fa fa-map-marker"></i>
|
||||||
|
<p>
|
||||||
|
{{ __("運営元メニュー") }}
|
||||||
|
<i class="right fa fa-angle-down"></i>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<ul class="nav nav-treeview" style="display: @if($isCityRoute) block @else none @endif;">
|
||||||
|
@foreach($visibleCities as $city)
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('city_dashboard', ['city_id' => $city->city_id]) }}"
|
||||||
|
class="nav-link @if($currentCityId == $city->city_id && request()->routeIs('city_dashboard')) active @endif">
|
||||||
|
<i class="nav-icon fa fa-building-o"></i>
|
||||||
|
<span style="margin-left:10px;">{{ $city->city_name }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
<!-- OU END -->
|
<!-- OU END -->
|
||||||
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- /.sidebar-menu -->
|
<!-- /.sidebar-menu -->
|
||||||
@ -761,6 +1055,132 @@
|
|||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="{{ asset('plugins/jquery/jquery.min.js') }}"></script>
|
<script src="{{ asset('plugins/jquery/jquery.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/select2/js/select2.full.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/moment/moment.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/inputmask/jquery.inputmask.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/daterangepicker/daterangepicker.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/bootstrap-colorpicker/js/bootstrap-colorpicker.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/bootstrap-switch/js/bootstrap-switch.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/bootstrap-switch/js/bootstrap-switch.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables/jquery.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-bs4/js/dataTables.bootstrap4.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-responsive/js/dataTables.responsive.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-responsive/js/responsive.bootstrap4.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-buttons/js/dataTables.buttons.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-buttons/js/buttons.bootstrap4.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/jszip/jszip.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/pdfmake/pdfmake.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/pdfmake/vfs_fonts.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-buttons/js/buttons.html5.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-buttons/js/buttons.print.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/datatables-buttons/js/buttons.colVis.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/adminlte/js/adminlte.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/chart.js/Chart.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/sparklines/jquery.sparkline.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/jqueryKnob/jquery.knob.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/moment/moment.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/fullcalendar/index.global.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/filterramework/jquery.filter_input.js') }}"></script>
|
||||||
|
<script src="{{ asset('plugins/filterramework/jquery.csv.min.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#table-users-list').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#table-users-list_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#list-all').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#list-all_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#table-list').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#table-list_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#table-form-list').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#table-form-list_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#example1').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#example1_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#example2').DataTable({
|
||||||
|
responsive: true,
|
||||||
|
lengthChange: false,
|
||||||
|
autoWidth: false,
|
||||||
|
buttons: ["csv", "excel", "pdf", "print"]
|
||||||
|
}).buttons().container().appendTo('#example2_wrapper .col-md-6:eq(0)');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
|
$('[data-toggle="popover"]').popover({
|
||||||
|
showInputs: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="{{ asset('js/app.js') }}" defer></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
margin-left: 280px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
<script src="{{ asset('plugins/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
<script src="{{ asset('plugins/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||||
<!-- Select2 -->
|
<!-- Select2 -->
|
||||||
@ -857,18 +1277,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script src="{{ asset('js/app.js') }}" defer></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.main-sidebar {
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
margin-left: 280px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -42,6 +42,8 @@ use App\Http\Controllers\Admin\MailTemplateController;
|
|||||||
use App\Http\Controllers\Admin\InvSettingController;
|
use App\Http\Controllers\Admin\InvSettingController;
|
||||||
use App\Http\Controllers\Admin\ZoneController;
|
use App\Http\Controllers\Admin\ZoneController;
|
||||||
use App\Http\Controllers\Admin\PplaceController;
|
use App\Http\Controllers\Admin\PplaceController;
|
||||||
|
use App\Http\Controllers\HomeController;
|
||||||
|
use App\Http\Controllers\Auth\EmailOtpController;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,50 +72,62 @@ Route::middleware('guest')->group(function () {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ログアウトルート(認証済みユーザー専用)
|
// ログアウトルート(認証済みユーザー専用、OTP チェック対象外)
|
||||||
Route::get('logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])->middleware('auth');
|
Route::get('logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])
|
||||||
|
->middleware(['auth'])
|
||||||
|
->name('logout');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 保護されたルート(認証済みユーザー専用)
|
// 保護されたルート(認証済みユーザー専用)
|
||||||
// Laravel 12変更点:middleware()をコントローラーではなくルートで指定
|
// Laravel 12変更点:middleware()をコントローラーではなくルートで指定
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
// ダッシュボード(ホーム画面)
|
// OTP メール認証ルート(OTP チェック対象外、ここに配置)
|
||||||
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
|
// ログイン直後に OTP(ワンタイムパスワード)コード入力画面を表示・検証
|
||||||
|
Route::prefix('otp')->name('otp.')->group(function () {
|
||||||
|
Route::get('/', [EmailOtpController::class, 'show'])->name('show');
|
||||||
|
Route::post('/', [EmailOtpController::class, 'verify'])->name('verify');
|
||||||
|
Route::post('/resend', [EmailOtpController::class, 'resend'])->name('resend');
|
||||||
|
});
|
||||||
|
|
||||||
// Laravel 12 移行時の一時的な占位符路由
|
// 以下のすべてのルートに OTP 認証チェックミドルウェアを適用
|
||||||
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義
|
Route::middleware('ensure.otp.verified')->group(function () {
|
||||||
// 実装完了後は各機能の正式なルートに置き換える予定
|
// ダッシュボード(ホーム画面)
|
||||||
|
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
|
||||||
|
|
||||||
// 利用者マスタ管理機能(仮ルート)
|
// Laravel 12 移行時の一時的な占位符路由
|
||||||
Route::match(['get', 'post'], '/users', function () {
|
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義
|
||||||
return view('admin.placeholder', ['title' => '利用者マスタ', 'feature' => 'users']);
|
// 実装完了後は各機能の正式なルートに置き換える予定
|
||||||
})->name('users');
|
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/users/add', function () {
|
// 利用者マスタ管理機能(仮ルート)
|
||||||
return view('admin.placeholder', ['title' => '利用者追加', 'feature' => 'users']);
|
Route::match(['get', 'post'], '/users', function () {
|
||||||
})->name('user_add');
|
return view('admin.placeholder', ['title' => '利用者マスタ', 'feature' => 'users']);
|
||||||
|
})->name('users');
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/users/edit/{seq}', function ($seq) {
|
Route::match(['get', 'post'], '/users/add', function () {
|
||||||
return view('admin.placeholder', ['title' => '利用者編集', 'feature' => 'users', 'id' => $seq]);
|
return view('admin.placeholder', ['title' => '利用者追加', 'feature' => 'users']);
|
||||||
})->name('user_edit')->where(['seq' => '[0-9]+']);
|
})->name('user_add');
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/users/info/{seq}', function ($seq) {
|
Route::match(['get', 'post'], '/users/edit/{seq}', function ($seq) {
|
||||||
return view('admin.placeholder', ['title' => '利用者詳細', 'feature' => 'users', 'id' => $seq]);
|
return view('admin.placeholder', ['title' => '利用者編集', 'feature' => 'users', 'id' => $seq]);
|
||||||
})->name('user_info')->where(['seq' => '[0-9]+']);
|
})->name('user_edit')->where(['seq' => '[0-9]+']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/users/import', function () {
|
Route::match(['get', 'post'], '/users/info/{seq}', function ($seq) {
|
||||||
return redirect()->route('users')->with('info', 'インポート機能は現在実装中です。');
|
return view('admin.placeholder', ['title' => '利用者詳細', 'feature' => 'users', 'id' => $seq]);
|
||||||
})->name('users_import');
|
})->name('user_info')->where(['seq' => '[0-9]+']);
|
||||||
|
|
||||||
Route::get('/users/export', function () {
|
Route::match(['get', 'post'], '/users/import', function () {
|
||||||
return redirect()->route('users')->with('info', 'エクスポート機能は現在実装中です。');
|
return redirect()->route('users')->with('info', 'インポート機能は現在実装中です。');
|
||||||
})->name('users_export');
|
})->name('users_import');
|
||||||
|
|
||||||
// その他の管理機能の仮ルート(必要に応じて追加)
|
Route::get('/users/export', function () {
|
||||||
Route::match(['get', 'post'], '/regular_contracts', function () {
|
return redirect()->route('users')->with('info', 'エクスポート機能は現在実装中です。');
|
||||||
return view('admin.placeholder', ['title' => '定期契約管理', 'feature' => 'regular_contracts']);
|
})->name('users_export');
|
||||||
})->name('regular_contracts');
|
|
||||||
|
// その他の管理機能の仮ルート(必要に応じて追加)
|
||||||
|
Route::match(['get', 'post'], '/regular_contracts', function () {
|
||||||
|
return view('admin.placeholder', ['title' => '定期契約管理', 'feature' => 'regular_contracts']);
|
||||||
|
})->name('regular_contracts');
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/parks', function () {
|
Route::match(['get', 'post'], '/parks', function () {
|
||||||
return view('admin.placeholder', ['title' => '駐輪場管理', 'feature' => 'parks']);
|
return view('admin.placeholder', ['title' => '駐輪場管理', 'feature' => 'parks']);
|
||||||
@ -151,6 +165,12 @@ Route::middleware('auth')->group(function () {
|
|||||||
// sou end
|
// sou end
|
||||||
|
|
||||||
// ou start
|
// ou start
|
||||||
|
// 自治体ダッシュボード
|
||||||
|
Route::get('/city/{city_id}/dashboard', [CityController::class, 'dashboard'])
|
||||||
|
->where(['city_id' => '[0-9]+'])
|
||||||
|
->name('city_dashboard')
|
||||||
|
->middleware('check.city.access');
|
||||||
|
|
||||||
// 市区マスタ
|
// 市区マスタ
|
||||||
Route::match(['get', 'post'], '/city', [CityController::class, 'list'])->name('city');
|
Route::match(['get', 'post'], '/city', [CityController::class, 'list'])->name('city');
|
||||||
Route::match(['get', 'post'], '/city/add', [CityController::class, 'add'])->name('city_add');
|
Route::match(['get', 'post'], '/city/add', [CityController::class, 'add'])->name('city_add');
|
||||||
@ -265,6 +285,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
// 常時表示インフォメーション
|
// 常時表示インフォメーション
|
||||||
Route::get('/information', [InformationController::class, 'list'])->name('information');
|
Route::get('/information', [InformationController::class, 'list'])->name('information');
|
||||||
|
Route::get('/dashboard', [InformationController::class, 'dashboard'])->name('dashboard');
|
||||||
Route::post('/information/status', [InformationController::class, 'updateStatus'])->name('information.status');
|
Route::post('/information/status', [InformationController::class, 'updateStatus'])->name('information.status');
|
||||||
|
|
||||||
// タグ発行キュー処理、履歴表示
|
// タグ発行キュー処理、履歴表示
|
||||||
@ -495,7 +516,13 @@ Route::middleware('auth')->group(function () {
|
|||||||
->name('zones_delete');
|
->name('zones_delete');
|
||||||
|
|
||||||
//kin end
|
//kin end
|
||||||
});
|
|
||||||
|
Route::get('/opes/{id}/permissions', [OpeController::class, 'getPermissionsByFeature'])
|
||||||
|
->where(['id' => '[0-9]+'])
|
||||||
|
->name('opes.permissions_by_feature');
|
||||||
|
|
||||||
|
}); // ensure.otp.verified ミドルウェアグループの閉じ括弧
|
||||||
|
}); // auth ミドルウェアグループの閉じ括弧
|
||||||
|
|
||||||
|
|
||||||
// Wellnet PUSH webhook (SHJ-4A)
|
// Wellnet PUSH webhook (SHJ-4A)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user