SMerge 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 23s
All checks were successful
Deploy main / deploy (push) Successful in 23s
This commit is contained in:
commit
a118709f9b
81
app/Console/Commands/CheckPasswordExpiry.php
Normal file
81
app/Console/Commands/CheckPasswordExpiry.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Ope;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CheckPasswordExpiry extends Command
|
||||
{
|
||||
/**
|
||||
* コマンドの名前と説明
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'password:check-expiry {ope_id?}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'パスワード有効期限をチェック';
|
||||
|
||||
/**
|
||||
* コマンド実行
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$opeId = $this->argument('ope_id');
|
||||
|
||||
if ($opeId) {
|
||||
$ope = Ope::find($opeId);
|
||||
if (!$ope) {
|
||||
$this->error("Ope with ID {$opeId} not found");
|
||||
return 1;
|
||||
}
|
||||
$this->checkOpe($ope);
|
||||
} else {
|
||||
// すべてのオペレータをチェック
|
||||
$opes = Ope::all();
|
||||
foreach ($opes as $ope) {
|
||||
$this->checkOpe($ope);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 単一オペレータの有効期限をチェック
|
||||
*/
|
||||
private function checkOpe(Ope $ope): void
|
||||
{
|
||||
$this->info("=== Ope ID: {$ope->ope_id} ({$ope->ope_name}) ===");
|
||||
$this->info("ope_pass_changed_at: " . ($ope->ope_pass_changed_at ?? 'NULL'));
|
||||
|
||||
if (is_null($ope->ope_pass_changed_at)) {
|
||||
$this->warn("❌ Password change REQUIRED (never changed)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
|
||||
$now = Carbon::now();
|
||||
$monthsDiff = $now->diffInMonths($changedAt);
|
||||
|
||||
$this->info("Changed At: " . $changedAt->format('Y-m-d H:i:s'));
|
||||
$this->info("Now: " . $now->format('Y-m-d H:i:s'));
|
||||
$this->info("Months Difference: {$monthsDiff}");
|
||||
|
||||
if ($monthsDiff >= 3) {
|
||||
$this->warn("❌ Password change REQUIRED ({$monthsDiff} months passed)");
|
||||
} else {
|
||||
$this->line("✅ Password is valid ({$monthsDiff} months passed, {3 - $monthsDiff} months remaining)");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Error parsing date: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$this->line("");
|
||||
}
|
||||
}
|
||||
@ -20,23 +20,20 @@ final class CityController extends Controller
|
||||
$sortType = (string) $request->input('sort_type', 'asc');
|
||||
$page = (int) $request->get('page', 1);
|
||||
|
||||
// ソート許可(安全 + 規約)
|
||||
$sortable = ['city_id', 'city_name', 'print_layout', 'city_remarks', 'created_at', 'updated_at'];
|
||||
if (!in_array($sort, $sortable, true)) {
|
||||
$sort = 'city_id';
|
||||
$query = City::query();
|
||||
|
||||
if ($request->filled('city_name')) {
|
||||
$query->where('city_name', 'like', '%' . $request->input('city_name') . '%');
|
||||
}
|
||||
|
||||
$sortType = strtolower($sortType);
|
||||
if (!in_array($sortType, ['asc', 'desc'], true)) {
|
||||
$sortType = 'asc';
|
||||
// 排序处理
|
||||
if (!empty($sort)) {
|
||||
$query->orderBy($sort, $sortType);
|
||||
}
|
||||
|
||||
$list = $service->paginateList(
|
||||
$request->input('city_name'),
|
||||
$sort,
|
||||
$sortType
|
||||
);
|
||||
$list = $query->paginate(20);
|
||||
|
||||
// 页码越界处理
|
||||
if ($list->total() > 0 && $page > $list->lastPage()) {
|
||||
return redirect()->route('cities.index', [
|
||||
'sort' => $sort,
|
||||
@ -54,56 +51,127 @@ final class CityController extends Controller
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.cities.create', [
|
||||
'record' => new City(),
|
||||
$inputs = [
|
||||
'city_name' => '',
|
||||
'print_layout' => '',
|
||||
'city_user' => '',
|
||||
'city_remarks' => '',
|
||||
];
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$rules = [
|
||||
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'city_remarks' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
$messages = [
|
||||
'city_name.required' => '市区名は必須です。',
|
||||
'city_name.regex' => '市区名は全角で入力してください。',
|
||||
'print_layout.required' => '印字レイアウトファイルは必須です。',
|
||||
'print_layout.regex' => '印字レイアウトファイルは全角で入力してください。',
|
||||
'city_user.required' => '顧客M入力不要フィールドIDは必須です。',
|
||||
'city_user.regex' => '顧客M入力不要フィールドIDは全角で入力してください。',
|
||||
'city_remarks.max' => '備考は20文字以内で入力してください。',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $rules, $messages);
|
||||
|
||||
$inputs = array_merge($inputs, $request->all());
|
||||
|
||||
if (!$validator->fails()) {
|
||||
$maxId = DB::table('city')->max('city_id');
|
||||
$newCityId = $maxId ? $maxId + 1 : 1;
|
||||
|
||||
$city = new City();
|
||||
$city->city_id = $newCityId;
|
||||
$city->fill($request->only([
|
||||
'city_name',
|
||||
'print_layout',
|
||||
'city_user',
|
||||
'city_remarks',
|
||||
]));
|
||||
|
||||
if ($city->save()) {
|
||||
$request->session()->flash('success', __('登録に成功しました'));
|
||||
return redirect()->route('city');
|
||||
} else {
|
||||
$request->session()->flash('error', __('登録に失敗しました'));
|
||||
}
|
||||
} else {
|
||||
$inputs['errorMsg'] = $validator->errors()->all();
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin.CityMaster.add', $inputs);
|
||||
}
|
||||
|
||||
public function edit(Request $request, $pk, $view = '')
|
||||
{
|
||||
$city = City::find($pk);
|
||||
if (!$city) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$rules = [
|
||||
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
|
||||
'city_remarks' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
$messages = [
|
||||
'city_name.required' => '市区名は必須です。',
|
||||
'city_name.regex' => '市区名は全角で入力してください。',
|
||||
'print_layout.required' => '印字レイアウトファイルは必須です。',
|
||||
'print_layout.regex' => '印字レイアウトファイルは全角で入力してください。',
|
||||
'city_user.required' => '顧客M入力不要フィールドIDは必須です。',
|
||||
'city_user.regex' => '顧客M入力不要フィールドIDは全角で入力してください。',
|
||||
'city_remarks.max' => '備考は20文字以内で入力してください。',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $rules, $messages);
|
||||
|
||||
if (!$validator->fails()) {
|
||||
$city->fill($request->only([
|
||||
'city_name',
|
||||
'print_layout',
|
||||
'city_user',
|
||||
'city_remarks',
|
||||
]));
|
||||
|
||||
if ($city->save()) {
|
||||
$request->session()->flash('success', __('更新に成功しました'));
|
||||
return redirect()->route('city');
|
||||
} else {
|
||||
$request->session()->flash('error', __('更新に失敗しました'));
|
||||
}
|
||||
} else {
|
||||
return view('admin.CityMaster.edit', [
|
||||
'city' => $city,
|
||||
'errorMsg' => $validator->errors()->all(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return view($view ?: 'admin.CityMaster.edit', [
|
||||
'city' => $city,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CityRequest $request, CityService $service): RedirectResponse
|
||||
public function info(Request $request, $pk)
|
||||
{
|
||||
$service->create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('cities.index')
|
||||
->with('success', __('登録に成功しました'));
|
||||
return $this->edit($request, $pk, 'CityMaster.info');
|
||||
}
|
||||
|
||||
public function edit(int $id): View
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$city = City::findOrFail($id);
|
||||
|
||||
return view('admin.cities.edit', [
|
||||
'record' => $city,
|
||||
]);
|
||||
$arr_pk = $request->get('pk');
|
||||
if (!$arr_pk) {
|
||||
return redirect()->route('city')->with('error', __('削除する市区を選択してください。'));
|
||||
}
|
||||
|
||||
public function update(CityRequest $request, int $id, CityService $service): RedirectResponse
|
||||
{
|
||||
$city = City::findOrFail($id);
|
||||
|
||||
$service->update($city, $request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('cities.index')
|
||||
->with('success', __('更新に成功しました'));
|
||||
if (City::destroy($arr_pk)) {
|
||||
return redirect()->route('city')->with('success', __("削除が完了しました。"));
|
||||
} else {
|
||||
return redirect()->route('city')->with('error', __('削除に失敗しました。'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$ids = $request->input('pk');
|
||||
|
||||
// pk が単体でも配列でも受けられるようにする(編集画面/一覧画面両対応)
|
||||
if ($ids === null || $ids === '' || $ids === []) {
|
||||
return redirect()
|
||||
->route('cities.index')
|
||||
->with('error', __('削除する市区を選択してください。'));
|
||||
}
|
||||
$ids = is_array($ids) ? $ids : [$ids];
|
||||
|
||||
$deleted = City::destroy($ids);
|
||||
|
||||
return $deleted
|
||||
? redirect()->route('cities.index')->with('success', __('削除が完了しました。'))
|
||||
: redirect()->route('cities.index')->with('error', __('削除に失敗しました。'));
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,189 @@ class InformationController extends Controller
|
||||
return view('admin.information.list', compact('jobs','period','type','status'));
|
||||
}
|
||||
|
||||
// ダッシュボード表示
|
||||
public function dashboard(Request $request)
|
||||
{
|
||||
// ダッシュボード統計情報を集計
|
||||
|
||||
// park_number テーブルから総容量を計算
|
||||
// park_standard(標準) + park_number(割当)+ park_limit(制限値)の合算
|
||||
$totalCapacity = DB::table('park_number')
|
||||
->selectRaw('
|
||||
COALESCE(SUM(park_standard), 0) as std_sum,
|
||||
COALESCE(SUM(park_number), 0) as num_sum,
|
||||
COALESCE(SUM(park_limit), 0) as limit_sum
|
||||
')
|
||||
->first();
|
||||
|
||||
$totalCapacityValue = ($totalCapacity->std_sum ?? 0) +
|
||||
($totalCapacity->num_sum ?? 0) +
|
||||
($totalCapacity->limit_sum ?? 0);
|
||||
|
||||
// 予約待ち人数(reserve テーブルから集計)
|
||||
// 条件:有効(valid_flag=1) かつ契約化されていない(contract_id IS NULL)
|
||||
// キャンセル除外:reserve_cancel_flag が NULL または 0、かつ reserve_cancelday が NULL
|
||||
$reserveQuery = DB::table('reserve')
|
||||
->where('valid_flag', 1)
|
||||
->whereNull('contract_id');
|
||||
|
||||
// キャンセルフラグの有無をチェック(列が存在するかどうか)
|
||||
try {
|
||||
$testResult = DB::table('reserve')
|
||||
->select(DB::raw('1'))
|
||||
->whereNotNull('reserve_cancel_flag')
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
// 列が存在する場合、キャンセル除外条件を追加
|
||||
$reserveQuery = $reserveQuery
|
||||
->where(function ($q) {
|
||||
$q->whereNull('reserve_cancel_flag')
|
||||
->orWhere('reserve_cancel_flag', 0);
|
||||
})
|
||||
->whereNull('reserve_cancelday');
|
||||
} catch (\Exception $e) {
|
||||
// キャンセルフラグが未運用の場合は基本条件のみで計算
|
||||
}
|
||||
|
||||
$totalWaiting = $reserveQuery->count();
|
||||
|
||||
// 使用中台数(park_number の park_number が使用台数)
|
||||
$totalUsed = DB::table('park_number')
|
||||
->sum('park_number') ?? 0;
|
||||
|
||||
// 空き台数 = 総容量 - 使用中台数
|
||||
$totalVacant = max(0, $totalCapacityValue - $totalUsed);
|
||||
|
||||
// 利用率計算(小数点以下切捨て)
|
||||
$utilizationRate = $totalCapacityValue > 0
|
||||
? (int) floor(($totalUsed / $totalCapacityValue) * 100)
|
||||
: 0;
|
||||
|
||||
// 予約待ち率(超過時のみ、超過なしは0%)
|
||||
// 超過判定:待機人数 > 空き台数
|
||||
$totalWaitingRate = 0;
|
||||
if ($totalCapacityValue > 0 && $totalWaiting > 0 && $totalWaiting > $totalVacant) {
|
||||
// 超過分 / 総容量 * 100(分母チェック付き)
|
||||
$totalWaitingRate = (int) floor((($totalWaiting - $totalVacant) / $totalCapacityValue) * 100);
|
||||
}
|
||||
|
||||
$totalStats = [
|
||||
'total_cities' => DB::table('city')->count(),
|
||||
'total_parks' => DB::table('park')->count(),
|
||||
'total_contracts' => DB::table('regular_contract')->count(),
|
||||
'total_users' => DB::table('user')->count(),
|
||||
'total_devices' => DB::table('device')->count(),
|
||||
'today_queues' => DB::table('operator_que')
|
||||
->whereDate('created_at', today())
|
||||
->count(),
|
||||
'total_waiting' => $totalWaiting,
|
||||
'total_capacity' => $totalCapacityValue,
|
||||
'total_utilization_rate' => $utilizationRate,
|
||||
'total_vacant_number' => $totalVacant,
|
||||
'total_waiting_rate' => $totalWaitingRate,
|
||||
];
|
||||
|
||||
// 自治体別統計情報を作成
|
||||
$cityStats = [];
|
||||
$cities = DB::table('city')->get();
|
||||
|
||||
foreach ($cities as $city) {
|
||||
// その自治体に属する駐輪場 ID を取得
|
||||
$parkIds = DB::table('park')
|
||||
->where('city_id', $city->city_id)
|
||||
->pluck('park_id')
|
||||
->toArray();
|
||||
|
||||
// ① 駐輪場数
|
||||
$parksCount = count($parkIds);
|
||||
|
||||
// ② 総収容台数(park_number テーブルの park_standard を合算)
|
||||
$capacity = 0;
|
||||
if (!empty($parkIds)) {
|
||||
$capacityResult = DB::table('park_number')
|
||||
->whereIn('park_id', $parkIds)
|
||||
->sum('park_standard');
|
||||
$capacity = $capacityResult ?? 0;
|
||||
}
|
||||
|
||||
// ③ 契約台数(contract_cancel_flag = 0 かつ有効期間内)
|
||||
$contractsCount = 0;
|
||||
if (!empty($parkIds)) {
|
||||
$contractsCount = DB::table('regular_contract')
|
||||
->whereIn('park_id', $parkIds)
|
||||
->where('contract_cancel_flag', 0)
|
||||
->where(function ($q) {
|
||||
// 有効期間内:開始日 <= 今日 かつ 終了日 >= 今日
|
||||
$q->where('contract_periods', '<=', now())
|
||||
->where('contract_periode', '>=', now());
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
// ④ 利用率計算(小数点以下切捨て)
|
||||
$utilizationRate = $capacity > 0
|
||||
? (int) floor(($contractsCount / $capacity) * 100)
|
||||
: 0;
|
||||
|
||||
// ⑤ 空き台数
|
||||
$availableSpaces = max(0, $capacity - $contractsCount);
|
||||
|
||||
// ⑥ 予約待ち人数(reserve テーブルで contract_id IS NULL かつ valid_flag = 1)
|
||||
$waitingCount = 0;
|
||||
if (!empty($parkIds)) {
|
||||
$waitingQuery = DB::table('reserve')
|
||||
->whereIn('park_id', $parkIds)
|
||||
->where('valid_flag', 1)
|
||||
->whereNull('contract_id');
|
||||
|
||||
// キャンセルフラグの有無をチェック
|
||||
try {
|
||||
DB::table('reserve')
|
||||
->select(DB::raw('1'))
|
||||
->whereNotNull('reserve_cancel_flag')
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
// 列が存在する場合、キャンセル除外条件を追加
|
||||
$waitingQuery = $waitingQuery
|
||||
->where(function ($q) {
|
||||
$q->whereNull('reserve_cancel_flag')
|
||||
->orWhere('reserve_cancel_flag', 0);
|
||||
})
|
||||
->whereNull('reserve_cancelday');
|
||||
} catch (\Exception $e) {
|
||||
// キャンセルフラグが未運用の場合は基本条件のみで計算
|
||||
}
|
||||
|
||||
$waitingCount = $waitingQuery->count();
|
||||
}
|
||||
|
||||
// ⑦ 利用者数(ユニークユーザー数)
|
||||
$usersCount = 0;
|
||||
if (!empty($parkIds)) {
|
||||
$usersCount = DB::table('regular_contract')
|
||||
->whereIn('park_id', $parkIds)
|
||||
->distinct()
|
||||
->count('user_id');
|
||||
}
|
||||
|
||||
// 配列に追加
|
||||
$cityStats[] = [
|
||||
'city' => $city,
|
||||
'parks_count' => $parksCount,
|
||||
'contracts_count' => $contractsCount,
|
||||
'users_count' => $usersCount,
|
||||
'waiting_count' => $waitingCount,
|
||||
'capacity' => $capacity,
|
||||
'utilization_rate' => $utilizationRate,
|
||||
'available_spaces' => $availableSpaces,
|
||||
];
|
||||
}
|
||||
|
||||
return view('admin.information.dashboard', compact('totalStats', 'cityStats'));
|
||||
}
|
||||
|
||||
// ステータス一括更新(着手=2 / 対応完了=3)
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -7,6 +8,9 @@ use App\Models\Ope;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use App\Models\Feature;
|
||||
use App\Models\Permission;
|
||||
use App\Models\OpePermission;
|
||||
|
||||
class OpeController extends Controller
|
||||
{
|
||||
@ -27,7 +31,6 @@ class OpeController extends Controller
|
||||
$sort = $inputs['sort'];
|
||||
$sort_type = $inputs['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)
|
||||
{
|
||||
// ※機能(画面)一覧を取得(プルダウン用)
|
||||
$features = Feature::query()
|
||||
->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,
|
||||
'record' => new Ope(),
|
||||
|
||||
'ope_id' => null,
|
||||
'ope_name' => '',
|
||||
'ope_type' => '',
|
||||
'ope_mail' => '',
|
||||
'ope_phone'=> '',
|
||||
'ope_phone' => '',
|
||||
|
||||
'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_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0,
|
||||
'ope_sendalart_que10'=> 0, 'ope_sendalart_que11'=> 0, 'ope_sendalart_que12'=> 0,
|
||||
'ope_sendalart_que13'=> 0,
|
||||
'ope_sendalart_que10' => 0, 'ope_sendalart_que11' => 0, 'ope_sendalart_que12' => 0,
|
||||
'ope_sendalart_que13' => 0,
|
||||
'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '',
|
||||
'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', '登録しました。');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 編集(GET 画面 / POST 更新)
|
||||
* ※権限(自治体×機能×操作)も同画面で設定する
|
||||
*/
|
||||
public function edit($id, Request $request)
|
||||
{
|
||||
$ope = Ope::getByPk($id);
|
||||
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')) {
|
||||
return view('admin.opes.edit', [
|
||||
'isEdit' => true,
|
||||
'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();
|
||||
|
||||
// --- バリデーション ---
|
||||
$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_type' => 'required|string|max:50',
|
||||
'ope_phone' => 'nullable|string|max:50',
|
||||
'ope_mail' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
// , でも ; でもOKにする
|
||||
$emails = array_map('trim', explode(';', str_replace(',', ';', $value)));
|
||||
foreach ($emails as $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);
|
||||
@ -157,18 +211,42 @@ class OpeController extends Controller
|
||||
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 複数)
|
||||
*/
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$ids = [];
|
||||
|
||||
// 単体削除
|
||||
if ($request->filled('id')) {
|
||||
$ids[] = (int) $request->input('id');
|
||||
$ids[] = (int)$request->input('id');
|
||||
}
|
||||
|
||||
// 複数削除
|
||||
|
||||
204
app/Http/Controllers/Admin/ParkingRegulationsController.php
Normal file
204
app/Http/Controllers/Admin/ParkingRegulationsController.php
Normal file
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Park;
|
||||
use App\Models\ParkingRegulation;
|
||||
|
||||
class ParkingRegulationsController extends Controller
|
||||
{
|
||||
/**
|
||||
* 一覧表示
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
// park_id 検証
|
||||
$request->validate([
|
||||
'park_id' => 'required|integer|exists:park,park_id',
|
||||
], [
|
||||
'park_id.required' => '駐輪場IDは必須です。',
|
||||
'park_id.integer' => '駐輪場IDは整数である必要があります。',
|
||||
'park_id.exists' => '指定された駐輪場が見つかりません。',
|
||||
]);
|
||||
|
||||
$parkId = (int) $request->input('park_id');
|
||||
|
||||
// 駐輪場情報取得
|
||||
$park = Park::where('park_id', $parkId)->firstOrFail();
|
||||
|
||||
// parking_regulations を取得し、psection / ptype 名を JOIN して表示
|
||||
$data = DB::table('parking_regulations')
|
||||
->leftJoin('psection', 'parking_regulations.psection_id', '=', 'psection.psection_id')
|
||||
->leftJoin('ptype', 'parking_regulations.ptype_id', '=', 'ptype.ptype_id')
|
||||
->where('parking_regulations.park_id', $parkId)
|
||||
->select(
|
||||
'parking_regulations.parking_regulations_seq',
|
||||
'parking_regulations.park_id',
|
||||
'parking_regulations.psection_id',
|
||||
'parking_regulations.ptype_id',
|
||||
'parking_regulations.regulations_text',
|
||||
'psection.psection_subject as psection_subject',
|
||||
'ptype.ptype_subject as ptype_subject'
|
||||
)
|
||||
->orderBy('parking_regulations.psection_id')
|
||||
->orderBy('parking_regulations.ptype_id')
|
||||
->paginate(50);
|
||||
|
||||
return view('admin.parking_regulations.list', [
|
||||
'park' => $park,
|
||||
'parkId' => $parkId,
|
||||
'regulations' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新規作成フォーム表示/登録
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
$parkId = $request->input('park_id');
|
||||
|
||||
// 駐輪場存在確認
|
||||
if (!$parkId) {
|
||||
return redirect()->back()->withErrors(['park_id' => '駐輪場IDが指定されていません。']);
|
||||
}
|
||||
|
||||
$park = Park::where('park_id', $parkId)->firstOrFail();
|
||||
|
||||
// マスタの選択肢取得
|
||||
$psections = DB::table('psection')->orderBy('psection_id')->get();
|
||||
$ptypes = DB::table('ptype')->orderBy('ptype_id')->get();
|
||||
|
||||
if ($request->isMethod('post')) {
|
||||
// 登録処理
|
||||
$validated = $request->validate([
|
||||
'park_id' => 'required|integer|exists:park,park_id',
|
||||
'psection_id' => 'required|integer',
|
||||
'ptype_id' => 'required|integer',
|
||||
'regulations_text' => 'nullable|string',
|
||||
], [
|
||||
'park_id.required' => '駐輪場IDは必須です。',
|
||||
'psection_id.required' => '車種区分は必須です。',
|
||||
'ptype_id.required' => '駐輪分類は必須です。',
|
||||
]);
|
||||
|
||||
// 重複チェック
|
||||
$exists = DB::table('parking_regulations')
|
||||
->where('park_id', $validated['park_id'])
|
||||
->where('psection_id', $validated['psection_id'])
|
||||
->where('ptype_id', $validated['ptype_id'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()->withErrors(['duplicate' => '同じ組み合わせの規定が既に存在します。'])->withInput();
|
||||
}
|
||||
|
||||
ParkingRegulation::create([
|
||||
'park_id' => $validated['park_id'],
|
||||
'psection_id' => $validated['psection_id'],
|
||||
'ptype_id' => $validated['ptype_id'],
|
||||
'regulations_text' => $validated['regulations_text'] ?? null,
|
||||
]);
|
||||
|
||||
return redirect()->route('parking_regulations_list', ['park_id' => $validated['park_id']])->with('success', '登録しました。');
|
||||
}
|
||||
|
||||
return view('admin.parking_regulations.add', [
|
||||
'park' => $park,
|
||||
'parkId' => $parkId,
|
||||
'psections' => $psections,
|
||||
'ptypes' => $ptypes,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 編集フォーム表示
|
||||
*/
|
||||
public function edit($seq, Request $request)
|
||||
{
|
||||
$record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
|
||||
if (!$record) {
|
||||
return redirect()->back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
|
||||
}
|
||||
|
||||
$park = Park::where('park_id', $record->park_id)->firstOrFail();
|
||||
|
||||
$psections = DB::table('psection')->orderBy('psection_id')->get();
|
||||
$ptypes = DB::table('ptype')->orderBy('ptype_id')->get();
|
||||
|
||||
return view('admin.parking_regulations.edit', [
|
||||
'park' => $park,
|
||||
'record' => $record,
|
||||
'psections' => $psections,
|
||||
'ptypes' => $ptypes,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新処理
|
||||
*/
|
||||
public function update($seq, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'psection_id' => 'required|integer',
|
||||
'ptype_id' => 'required|integer',
|
||||
'regulations_text' => 'nullable|string',
|
||||
], [
|
||||
'psection_id.required' => '車種区分は必須です。',
|
||||
'ptype_id.required' => '駐輪分類は必須です。',
|
||||
]);
|
||||
|
||||
// 対象レコード取得
|
||||
$record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
|
||||
if (!$record) {
|
||||
return back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
|
||||
}
|
||||
|
||||
// 重複チェック(自分自身は除外)
|
||||
$exists = DB::table('parking_regulations')
|
||||
->where('park_id', $record->park_id)
|
||||
->where('psection_id', $validated['psection_id'])
|
||||
->where('ptype_id', $validated['ptype_id'])
|
||||
->where('parking_regulations_seq', '<>', $seq)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()->withErrors(['duplicate' => '同じ組み合わせの規定が既に存在します。'])->withInput();
|
||||
}
|
||||
|
||||
DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->update([
|
||||
'psection_id' => $validated['psection_id'],
|
||||
'ptype_id' => $validated['ptype_id'],
|
||||
'regulations_text' => $validated['regulations_text'] ?? null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect()->route('parking_regulations_list', ['park_id' => $record->park_id])->with('success', '更新しました。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 削除処理
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'parking_regulations_seq' => 'required|integer',
|
||||
], [
|
||||
'parking_regulations_seq.required' => '削除対象が指定されていません。',
|
||||
]);
|
||||
|
||||
$seq = (int) $validated['parking_regulations_seq'];
|
||||
|
||||
$record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
|
||||
if (!$record) {
|
||||
return back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
|
||||
}
|
||||
|
||||
DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->delete();
|
||||
|
||||
return redirect()->route('parking_regulations_list', ['park_id' => $record->park_id])->with('success', '削除しました。');
|
||||
}
|
||||
}
|
||||
120
app/Http/Controllers/Admin/ReductionConfirmMasterController.php
Normal file
120
app/Http/Controllers/Admin/ReductionConfirmMasterController.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Park;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReductionConfirmMasterController extends Controller
|
||||
{
|
||||
/**
|
||||
* 減免確認マスタ画面を表示
|
||||
*
|
||||
* @param Request $request park_id をクエリパラメータで受け取る
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
// park_id の検証
|
||||
$request->validate([
|
||||
'park_id' => 'required|integer|exists:park,park_id',
|
||||
], [
|
||||
'park_id.required' => '駐輪場IDは必須です。',
|
||||
'park_id.integer' => '駐輪場IDは整数である必要があります。',
|
||||
'park_id.exists' => '指定された駐輪場が見つかりません。',
|
||||
]);
|
||||
|
||||
$parkId = (int) $request->input('park_id');
|
||||
|
||||
// 駐輪場情報を取得
|
||||
$park = Park::where('park_id', $parkId)->firstOrFail();
|
||||
|
||||
// reduction_confirm を主テーブルとして、usertype と JOIN して一覧を取得
|
||||
// WHERE park_id = ? で対象駐輪場のレコードのみ取得
|
||||
$reductionData = DB::table('reduction_confirm')
|
||||
->leftJoin('usertype', 'reduction_confirm.user_categoryid', '=', 'usertype.user_categoryid')
|
||||
->where('reduction_confirm.park_id', $parkId)
|
||||
->orderBy('reduction_confirm.user_categoryid', 'asc')
|
||||
->select(
|
||||
'reduction_confirm.park_id',
|
||||
'reduction_confirm.user_categoryid',
|
||||
'reduction_confirm.reduction_confirm_type',
|
||||
'usertype.usertype_subject1',
|
||||
'usertype.usertype_subject2',
|
||||
'usertype.usertype_subject3'
|
||||
)
|
||||
->paginate(50);
|
||||
|
||||
return view('admin.reduction_confirm.list', [
|
||||
'park' => $park,
|
||||
'parkId' => $parkId,
|
||||
'reductionData' => $reductionData,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 減免確認情報を一括更新
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
// バリデーション
|
||||
$validated = $request->validate([
|
||||
'park_id' => 'required|integer|exists:park,park_id',
|
||||
'row_user_categoryid' => 'array',
|
||||
'row_user_categoryid.*' => 'integer',
|
||||
'reduction_confirm_type' => 'array',
|
||||
'reduction_confirm_type.*' => 'in:0,1,2',
|
||||
], [
|
||||
'park_id.required' => '駐輪場IDは必須です。',
|
||||
'park_id.integer' => '駐輪場IDは整数である必要があります。',
|
||||
'park_id.exists' => '指定された駐輪場が見つかりません。',
|
||||
'reduction_confirm_type.*.in' => '減免確認種別は0, 1, 2 のいずれかである必要があります。',
|
||||
]);
|
||||
|
||||
$parkId = (int) $validated['park_id'];
|
||||
|
||||
// ログイン中のオペレータID取得
|
||||
$opeId = auth()->user()->ope_id ?? null;
|
||||
|
||||
// POST された配列は index ベースで来るため、row_user_categoryid のインデックスに合わせてマッピングする
|
||||
$rowUserCategory = $request->input('row_user_categoryid', []);
|
||||
$types = $request->input('reduction_confirm_type', []);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($parkId, $rowUserCategory, $types, $opeId) {
|
||||
foreach ($rowUserCategory as $idx => $userCategoryId) {
|
||||
if (!isset($types[$idx])) {
|
||||
continue;
|
||||
}
|
||||
$type = (int) $types[$idx];
|
||||
|
||||
DB::table('reduction_confirm')
|
||||
->where('park_id', $parkId)
|
||||
->where('user_categoryid', (int) $userCategoryId)
|
||||
->update([
|
||||
'reduction_confirm_type' => $type,
|
||||
'updated_at' => now(),
|
||||
'ope_id' => $opeId,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('reduction_confirm_list', ['park_id' => $parkId])
|
||||
->with('success', '減免確認マスタを更新しました。');
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('ReductionConfirm update failed', [
|
||||
'park_id' => $parkId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return back()->withErrors(['error' => '更新に失敗しました。管理者にお問い合わせください。'])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,8 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
@ -37,24 +39,73 @@ class ForgotPasswordController extends Controller
|
||||
return back()->withErrors(['email' => '該当するユーザーが見つかりません。']);
|
||||
}
|
||||
|
||||
// 5分間隔のメール送信制限チェック(最新のトークンを対象)
|
||||
$lastToken = DB::table('password_reset_tokens')
|
||||
->where('ope_mail', $user->ope_mail)
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
if ($lastToken) {
|
||||
// タイムゾーンを明示的に指定(デフォルトはUTCで解析される可能性がある)
|
||||
$lastCreatedAt = Carbon::parse($lastToken->created_at, config('app.timezone'));
|
||||
$now = now();
|
||||
|
||||
// 経過秒数で判定
|
||||
$diffSeconds = $lastCreatedAt->diffInSeconds(now(), false);
|
||||
$limitSeconds = 5 * 60; // 5分
|
||||
if ($diffSeconds < $limitSeconds) {
|
||||
$remainSeconds = $limitSeconds - $diffSeconds;
|
||||
// 残り秒を「分」に変換:端数は切り上げ(例:1秒残りでも1分と表示)
|
||||
$waitMinutes = (int) ceil($remainSeconds / 60);
|
||||
return back()->withErrors([
|
||||
'email' => "パスワード再設定メールは5分以上の間隔を置いて送信してください。{$waitMinutes}分後に再度お試しください。"
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// トークン生成
|
||||
$token = Str::random(60);
|
||||
// SHA256ハッシュで保存(セキュリティ向上)
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
// トークン保存(既存レコードがあれば更新)
|
||||
DB::table('password_reset_tokens')->updateOrInsert(
|
||||
['ope_mail' => $user->ope_mail],
|
||||
[
|
||||
'token' => $token,
|
||||
'token' => $tokenHash,
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// メール送信
|
||||
try {
|
||||
$resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
|
||||
Mail::raw("下記URLからパスワード再設定を行ってください。\n\n{$resetUrl}", function ($message) use ($user) {
|
||||
|
||||
$body = $user->ope_name . " 様\n\n" .
|
||||
"So-Managerをご利用いただき、ありがとうございます。\n\n" .
|
||||
"本メールは、パスワード再設定のご依頼を受けてお送りしております。\n\n" .
|
||||
"以下のURLをクリックし、新しいパスワードを設定してください。\n\n" .
|
||||
$resetUrl . "\n\n" .
|
||||
"※このURLの有効期限は、24時間です。\n" .
|
||||
"※有効期限を過ぎた場合は、再度パスワード再設定手続きを行ってください。\n" .
|
||||
"※本メールにお心当たりがない場合は、本メールを破棄してください。\n\n" .
|
||||
"_________________________________\n" .
|
||||
"So-Manager サポートセンター\n" .
|
||||
"E-mail : support@so-manager.com\n" .
|
||||
"URL : https://www.so-manager.com/\n" .
|
||||
"_________________________________";
|
||||
|
||||
Mail::raw($body, function ($message) use ($user) {
|
||||
$message->to($user->ope_mail)
|
||||
->subject('パスワード再設定のご案内');
|
||||
->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject('【【So-Manager】パスワード再設定のご案内】');
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('ForgotPassword mail send failed', [
|
||||
'to' => $user->ope_mail,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return back()->withErrors(['email' => 'メール送信に失敗しました。サーバログを確認してください。']);
|
||||
}
|
||||
|
||||
return back()->with('status', 'パスワード再設定メールを送信しました。');
|
||||
}
|
||||
|
||||
@ -3,8 +3,12 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\EmailOtpMail;
|
||||
use App\Services\EmailOtpService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -135,6 +139,10 @@ class LoginController extends Controller
|
||||
/**
|
||||
* ログイン成功時のレスポンス
|
||||
*
|
||||
* OTP認証チェック:
|
||||
* - 24時間以内に OTP 認証済みの場合:/home にリダイレクト
|
||||
* - 未認証の場合:OTP メール送信 → /otp にリダイレクト
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
||||
*/
|
||||
@ -147,9 +155,47 @@ class LoginController extends Controller
|
||||
// ここで保持する値も login_id(入力名は ope_id のまま)
|
||||
$request->session()->put('login_ope_id', $request->input('ope_id'));
|
||||
|
||||
// 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認証メールの送信に失敗しました。');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ログイン失敗時のレスポンス
|
||||
*
|
||||
|
||||
135
app/Http/Controllers/Auth/PasswordChangeController.php
Normal file
135
app/Http/Controllers/Auth/PasswordChangeController.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ChangePasswordRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PasswordChangeController extends Controller
|
||||
{
|
||||
/**
|
||||
* コントローラーのコンストラクタ
|
||||
*
|
||||
* ログイン状態のユーザーのみアクセス可能
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Laravel 12: ミドルウェアは routes/web.php で処理
|
||||
}
|
||||
|
||||
/**
|
||||
* パスワード変更フォーム表示
|
||||
*
|
||||
* GET /password/change
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showChangeForm()
|
||||
{
|
||||
// 現在のユーザー情報を取得
|
||||
$ope = Auth::user();
|
||||
|
||||
// ビューにパスワード変更が必須かどうかを判定するデータを渡す
|
||||
$isRequired = $this->isPasswordChangeRequired($ope);
|
||||
|
||||
return view('auth.password-change', [
|
||||
'isRequired' => $isRequired,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* パスワード変更成功画面を表示
|
||||
*
|
||||
* GET /password/change/success
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showSuccessPage()
|
||||
{
|
||||
return view('auth.password-change-success');
|
||||
}
|
||||
|
||||
/**
|
||||
* パスワード変更処理
|
||||
*
|
||||
* POST /password/change
|
||||
*
|
||||
* バリデーション:
|
||||
* - 当前パスワード:必填、8-64文字、ハッシュ値一致確認
|
||||
* - 新パスワード:必填、8-64文字、英数字+記号のみ、当前と異なる
|
||||
* - 新パスワード確認:必填、新パスワードと一致
|
||||
*
|
||||
* @param \App\Http\Requests\ChangePasswordRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function updatePassword(ChangePasswordRequest $request)
|
||||
{
|
||||
// 現在のユーザーを取得
|
||||
$ope = Auth::user();
|
||||
|
||||
// ステップ1:当前パスワードの認証(ハッシュ値の確認)
|
||||
if (!Hash::check($request->current_password, $ope->ope_pass)) {
|
||||
// バリデーションエラーとして当前パスワード が正しくないことを返す
|
||||
throw ValidationException::withMessages([
|
||||
'current_password' => '当前パスワードが正しくありません。',
|
||||
]);
|
||||
}
|
||||
|
||||
// ステップ2:新パスワードが当前パスワードと同一でないか確認
|
||||
// FormRequest側でも not_in ルールで確認しているが、ハッシュ値での二重チェック
|
||||
if (Hash::check($request->password, $ope->ope_pass)) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => '新パスワードは当前パスワードと異なる必要があります。',
|
||||
]);
|
||||
}
|
||||
|
||||
// ステップ3:データベース更新
|
||||
// パスワードをハッシュ化して更新
|
||||
$ope->ope_pass = Hash::make($request->password);
|
||||
|
||||
// パスワード変更時刻を現在時刻に更新
|
||||
$ope->ope_pass_changed_at = Carbon::now();
|
||||
|
||||
// updated_at も自動更新される
|
||||
$ope->save();
|
||||
|
||||
// イベント発火:パスワード変更イベント
|
||||
event(new PasswordReset($ope));
|
||||
|
||||
// 成功画面へリダイレクト
|
||||
return redirect()->route('password.change.success');
|
||||
}
|
||||
|
||||
/**
|
||||
* パスワード変更が必須かどうかを判定
|
||||
*
|
||||
* 初回ログイン時(ope_pass_changed_at が NULL)または
|
||||
* 最後変更から3ヶ月以上経過している場合、TRUE を返す
|
||||
*
|
||||
* @param \App\Models\Ope $ope
|
||||
* @return bool
|
||||
*/
|
||||
private function isPasswordChangeRequired($ope): bool
|
||||
{
|
||||
// パスワード変更日時が未設定(初回ログイン等)
|
||||
if (is_null($ope->ope_pass_changed_at)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// パスワード変更から経過日数を計算
|
||||
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
|
||||
$now = Carbon::now();
|
||||
|
||||
// 3ヶ月以上経過している場合
|
||||
if ($now->diffInMonths($changedAt) >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,33 @@ class ResetPasswordController extends Controller
|
||||
{
|
||||
$token = $request->query('token');
|
||||
$email = $request->query('email');
|
||||
|
||||
// トークンのハッシュ化
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
// トークン・メール・24時間以内の有効性をチェック
|
||||
$record = DB::table('password_reset_tokens')
|
||||
->where('ope_mail', $email)
|
||||
->where('token', $tokenHash)
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
return redirect()->route('forgot_password')
|
||||
->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
|
||||
}
|
||||
|
||||
// 24時間チェック
|
||||
$createdAt = \Carbon\Carbon::parse($record->created_at);
|
||||
if ($createdAt->addHours(24)->isPast()) {
|
||||
// 期限切れトークンを削除
|
||||
DB::table('password_reset_tokens')
|
||||
->where('ope_mail', $email)
|
||||
->delete();
|
||||
|
||||
return redirect()->route('forgot_password')
|
||||
->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
|
||||
}
|
||||
|
||||
return view('auth.reset-password', compact('token', 'email'));
|
||||
}
|
||||
|
||||
@ -25,14 +52,28 @@ class ResetPasswordController extends Controller
|
||||
'password' => 'required|confirmed|min:8',
|
||||
]);
|
||||
|
||||
// トークンチェック
|
||||
// トークンのハッシュ化
|
||||
$tokenHash = hash('sha256', $request->token);
|
||||
|
||||
// トークン・メール・24時間以内の有効性をチェック
|
||||
$record = DB::table('password_reset_tokens')
|
||||
->where('ope_mail', $request->email)
|
||||
->where('token', $request->token)
|
||||
->where('token', $tokenHash)
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
return back()->withErrors(['email' => '無効なトークンまたはメールアドレスです。']);
|
||||
return back()->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
|
||||
}
|
||||
|
||||
// 24時間チェック
|
||||
$createdAt = \Carbon\Carbon::parse($record->created_at);
|
||||
if ($createdAt->addHours(24)->isPast()) {
|
||||
// 期限切れトークンを削除
|
||||
DB::table('password_reset_tokens')
|
||||
->where('ope_mail', $request->email)
|
||||
->delete();
|
||||
|
||||
return back()->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
|
||||
}
|
||||
|
||||
// パスワード更新
|
||||
@ -42,11 +83,14 @@ class ResetPasswordController extends Controller
|
||||
}
|
||||
$user->password = Hash::make($request->password);
|
||||
$user->updated_at = now();
|
||||
// パスワード再設定時もope_pass_changed_atを更新
|
||||
$user->ope_pass_changed_at = now();
|
||||
$user->save();
|
||||
|
||||
// トークン削除
|
||||
DB::table('password_reset_tokens')->where('ope_mail', $request->email)->delete();
|
||||
|
||||
return redirect()->route('login')->with('status', 'パスワードを再設定しました。');
|
||||
// パスワード再設定成功画面へリダイレクト
|
||||
return redirect()->route('password.change.success');
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\City;
|
||||
use App\Services\MenuAccessService;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
@ -22,10 +24,15 @@ class HomeController extends Controller
|
||||
* アプリケーションのダッシュボードを表示
|
||||
* 認証後のホーム画面
|
||||
*
|
||||
* @param MenuAccessService $menuAccessService メニューアクセス制御サービス
|
||||
* @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'));
|
||||
}
|
||||
}
|
||||
34
app/Http/Middleware/CheckCityAccess.php
Normal file
34
app/Http/Middleware/CheckCityAccess.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckCityAccess
|
||||
{
|
||||
/**
|
||||
* 自治体へのアクセス権限を確認するミドルウェア
|
||||
*
|
||||
* 将来的に以下の権限判定を追加予定:
|
||||
* - ユーザーが指定自治体にアクセス権があるか確認
|
||||
* - 権限がない場合は 403 Forbidden を返す
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 現在の処理:権限判定なしで通す
|
||||
// TODO: 将来的に以下の権限判定ロジックを追加
|
||||
// $city_id = $request->route('city_id');
|
||||
// $user = auth()->user();
|
||||
// if (!$user->canAccessCity($city_id)) {
|
||||
// return abort(403, '指定された自治体へのアクセス権がありません。');
|
||||
// }
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
102
app/Http/Middleware/CheckPasswordChangeRequired.php
Normal file
102
app/Http/Middleware/CheckPasswordChangeRequired.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Carbon\Carbon;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 定期パスワード変更チェックミドルウェア
|
||||
*
|
||||
* ログインしているオペレータのパスワード最後変更時刻をチェック
|
||||
* 3ヶ月以上経過している場合、パスワード変更画面へ強制リダイレクト
|
||||
*/
|
||||
class CheckPasswordChangeRequired
|
||||
{
|
||||
/**
|
||||
* リクエストを処理
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// ログインしていない場合はスキップ
|
||||
if (!Auth::check()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 既にパスワード変更ページにいる場合はスキップ
|
||||
if ($request->routeIs('password.change.show', 'password.change.update')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// 現在のユーザーを取得
|
||||
$ope = Auth::user();
|
||||
|
||||
// パスワード変更が必須か判定
|
||||
if ($this->isPasswordChangeRequired($ope)) {
|
||||
return redirect()->route('password.change.show');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* パスワード変更が必須かどうかを判定
|
||||
*
|
||||
* 初回ログイン時(ope_pass_changed_at が NULL)または
|
||||
* 最後変更から3ヶ月以上経過している場合、TRUE を返す
|
||||
*
|
||||
* @param \App\Models\Ope $ope
|
||||
* @return bool
|
||||
*/
|
||||
private function isPasswordChangeRequired($ope): bool
|
||||
{
|
||||
// パスワード変更日時が未設定(初回ログイン等)
|
||||
if (is_null($ope->ope_pass_changed_at)) {
|
||||
\Log::info('Password change required: ope_pass_changed_at is null', [
|
||||
'ope_id' => $ope->ope_id,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// パスワード変更から経過日数を計算
|
||||
// ope_pass_changed_at は複数のフォーマットに対応
|
||||
try {
|
||||
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
|
||||
} catch (\Exception $e) {
|
||||
// パース失敗時は強制変更
|
||||
\Log::warning('Failed to parse ope_pass_changed_at', [
|
||||
'ope_id' => $ope->ope_id,
|
||||
'value' => $ope->ope_pass_changed_at,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
// 3ヶ月以上経過しているか判定
|
||||
// diffInMonths は絶対値ではなく符号付きなので、abs() で絶対値を取得
|
||||
$monthsDiff = abs($now->diffInMonths($changedAt));
|
||||
|
||||
\Log::info('Password change check', [
|
||||
'ope_id' => $ope->ope_id,
|
||||
'changed_at' => $changedAt->format('Y-m-d H:i:s'),
|
||||
'now' => $now->format('Y-m-d H:i:s'),
|
||||
'months_diff' => $monthsDiff,
|
||||
'is_required' => $monthsDiff >= 3,
|
||||
]);
|
||||
|
||||
if ($monthsDiff >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
97
app/Http/Requests/ChangePasswordRequest.php
Normal file
97
app/Http/Requests/ChangePasswordRequest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ChangePasswordRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* リクエストを処理することを認可するか判定
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* 入力値の検証ルール
|
||||
*
|
||||
* クライアント側:必填(3項目)、長度 8-64、新密码仅半角英数字+记号、新/确认一致
|
||||
* サーバー側:当前密码认证(hash check)、新密码不能等于旧密码(CustomRulesで実装)
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// 当前パスワード:必填、8-64文字
|
||||
'current_password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:8',
|
||||
'max:64',
|
||||
],
|
||||
|
||||
// 新パスワード:必填、8-64文字、英数字+記号のみ
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:8',
|
||||
'max:64',
|
||||
'regex:/^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:\'",.<>?\/\\|`~]+$/', // 半角英数字+記号のみ
|
||||
],
|
||||
|
||||
// 新パスワード確認:必填、新パスワードと一致
|
||||
'password_confirmation' => [
|
||||
'required',
|
||||
'string',
|
||||
'same:password',
|
||||
],
|
||||
|
||||
// hidden フィールド(フォーム側で出力)
|
||||
'updated_at' => 'nullable|date_format:Y-m-d H:i:s',
|
||||
'ope_pass_changed_at' => 'nullable|date_format:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性の表示名
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => '当前パスワード',
|
||||
'password' => '新パスワード',
|
||||
'password_confirmation' => '新パスワード確認',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 検証エラーメッセージのカスタマイズ
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'current_password.required' => '当前パスワードを入力してください。',
|
||||
'current_password.min' => '当前パスワードは8文字以上です。',
|
||||
'current_password.max' => '当前パスワードは64文字以下です。',
|
||||
|
||||
'password.required' => '新パスワードを入力してください。',
|
||||
'password.min' => '新パスワードは8文字以上です。',
|
||||
'password.max' => '新パスワードは64文字以下です。',
|
||||
'password.regex' => '新パスワードは英数字と記号のみ使用できます。',
|
||||
|
||||
'password_confirmation.required' => '新パスワード確認を入力してください。',
|
||||
'password_confirmation.same' => '新パスワードと新パスワード確認は一致する必要があります。',
|
||||
];
|
||||
}
|
||||
}
|
||||
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',
|
||||
'city_user',
|
||||
'city_remarks',
|
||||
'management_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
@ -26,11 +27,40 @@ class City extends Model
|
||||
public static function getList(?int $operatorId = null): array
|
||||
{
|
||||
return static::query()
|
||||
->when($operatorId, fn ($q) => $q->where('operator_id', $operatorId))
|
||||
->when($operatorId, fn($q) => $q->where('operator_id', $operatorId))
|
||||
->orderBy('city_name')
|
||||
->pluck('city_name', 'city_id')
|
||||
->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 = [
|
||||
'管理者',
|
||||
'マネージャー',
|
||||
'オペレーター',
|
||||
'エリアマネージャー',
|
||||
'職員',
|
||||
];
|
||||
|
||||
protected $table = 'ope'; // データベーステーブル名(旧システムと同じ)
|
||||
@ -29,7 +27,7 @@ class Ope extends Authenticatable
|
||||
|
||||
/**
|
||||
* 一括代入可能な属性
|
||||
* Laravel 5.7から引き継いだフィールド構成
|
||||
* Laravel 5.7から引き継いだフィールド構成 + OTP認証フィールド
|
||||
*/
|
||||
protected $fillable = [
|
||||
'//TODO オペレータID not found in database specs',
|
||||
@ -58,6 +56,13 @@ class Ope extends Authenticatable
|
||||
'ope_auth4',
|
||||
'ope_quit_flag',
|
||||
'ope_quitday',
|
||||
// OTP認証関連フィールド(メール二段階認証)
|
||||
'email_otp_code_hash',
|
||||
'email_otp_expires_at',
|
||||
'email_otp_last_sent_at',
|
||||
'email_otp_verified_at',
|
||||
// パスワード変更関連フィールド(定期パスワード変更)
|
||||
'ope_pass_changed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -178,4 +183,12 @@ class Ope extends Authenticatable
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
30
app/Models/ParkingRegulation.php
Normal file
30
app/Models/ParkingRegulation.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ParkingRegulation extends Model
|
||||
{
|
||||
// テーブル名
|
||||
protected $table = 'parking_regulations';
|
||||
|
||||
// プライマリキー
|
||||
protected $primaryKey = 'parking_regulations_seq';
|
||||
|
||||
// 自動インクリメントが有効
|
||||
public $incrementing = true;
|
||||
|
||||
// タイムスタンプ自動管理
|
||||
public $timestamps = true;
|
||||
|
||||
// マスアサイン可能カラム
|
||||
protected $fillable = [
|
||||
'park_id',
|
||||
'psection_id',
|
||||
'ptype_id',
|
||||
'regulations_text',
|
||||
];
|
||||
|
||||
// 必要ならリレーションを追加(Park / Psection / Ptype がある場合)
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
48
app/Models/ReductionMaster.php
Normal file
48
app/Models/ReductionMaster.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ReductionMaster extends Model
|
||||
{
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $table = 'reduction_confirm';
|
||||
protected $primaryKey = null; // 複合キーを使用
|
||||
public $incrementing = false;
|
||||
|
||||
protected $fillable = [
|
||||
'park_id',
|
||||
'user_categoryid',
|
||||
'reduction_check_type',
|
||||
'operator_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 複合キー (park_id, user_categoryid) で既存レコードを検索または作成
|
||||
*/
|
||||
public static function findOrCreateByKeys($parkId, $userCategoryId)
|
||||
{
|
||||
return self::where('park_id', $parkId)
|
||||
->where('user_categoryid', $userCategoryId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* レコード保存時に operator_id を自動設定
|
||||
*/
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
self::saving(function (ReductionMaster $model) {
|
||||
if (!isset($model->operator_id) || $model->operator_id === null) {
|
||||
$model->operator_id = Auth::user()->ope_id ?? null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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\ShjSixService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\MenuAccessService;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -109,10 +110,15 @@ class AppServiceProvider extends ServiceProvider
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
$menuAccessService = app(MenuAccessService::class);
|
||||
$isSorin = $menuAccessService->isSorin();
|
||||
$visibleCities = $menuAccessService->visibleCities();
|
||||
|
||||
$view->with(compact(
|
||||
'taskCount','taskLatest',
|
||||
'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,16 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'/shj4a', // 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,
|
||||
'check.password.change.required' => \App\Http\Middleware\CheckPasswordChangeRequired::class,
|
||||
]);
|
||||
})
|
||||
->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'),
|
||||
],
|
||||
|
||||
];
|
||||
BIN
public/plugins/summernote/font/summernote.ttf
Normal file
BIN
public/plugins/summernote/font/summernote.ttf
Normal file
Binary file not shown.
BIN
public/plugins/summernote/font/summernote.woff2
Normal file
BIN
public/plugins/summernote/font/summernote.woff2
Normal file
Binary file not shown.
1
public/plugins/summernote/summernote.min.css
vendored
Normal file
1
public/plugins/summernote/summernote.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/plugins/summernote/summernote.min.js
vendored
Normal file
2
public/plugins/summernote/summernote.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
283
resources/views/admin/CityMaster/dashboard.blade.php
Normal file
283
resources/views/admin/CityMaster/dashboard.blade.php
Normal file
@ -0,0 +1,283 @@
|
||||
@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">
|
||||
@php
|
||||
// park_number テーブルから指定駐輪場群の容量を集計
|
||||
$parkIds = $parks->pluck('park_id')->toArray();
|
||||
$parkNumberCapacity = 0;
|
||||
if (!empty($parkIds)) {
|
||||
$parkNumberData = \Illuminate\Support\Facades\DB::table('park_number')
|
||||
->whereIn('park_id', $parkIds)
|
||||
->selectRaw('COALESCE(SUM(park_standard), 0) + COALESCE(SUM(park_number), 0) + COALESCE(SUM(park_limit), 0) as total')
|
||||
->first();
|
||||
$parkNumberCapacity = $parkNumberData->total ?? 0;
|
||||
}
|
||||
@endphp
|
||||
<h3>{{ number_format($parkNumberCapacity) }}</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 = $parkNumberCapacity;
|
||||
$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(max(0, $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
|
||||
// 待機超過分 / 総容量で予約待ち率を計算(超過なしは0%)
|
||||
// 分母チェック付き:総容量 > 0 の場合のみ計算
|
||||
$totalVacant = max(0, $totalCapacity - $stats['contracts_count']);
|
||||
$waitingRate = 0;
|
||||
if ($totalCapacity > 0 && $stats['waiting_count'] > 0 && $stats['waiting_count'] > $totalVacant) {
|
||||
$waitingRate = (int) floor((($stats['waiting_count'] - $totalVacant) / $totalCapacity) * 100);
|
||||
}
|
||||
@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>
|
||||
@endfor
|
||||
{{-- ▲ キュー1〜13 --}}
|
||||
|
||||
{{--
|
||||
<!-- 管理者権限付与 -->
|
||||
<div class="form-group col-3">
|
||||
<label>{{__('validation.attributes.ope_auth1')}}</label>
|
||||
@ -245,7 +245,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
--}}
|
||||
<!-- 退職フラグ -->
|
||||
<div class="form-group col-3">
|
||||
<label>{{__('validation.attributes.ope_quit_flag')}}</label>
|
||||
@ -278,6 +278,51 @@
|
||||
</div>
|
||||
<!-- /.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>
|
||||
{{-- ▼ 下部ボタン --}}
|
||||
@ -307,3 +352,67 @@
|
||||
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
88
resources/views/admin/parking_regulations/_form.blade.php
Normal file
88
resources/views/admin/parking_regulations/_form.blade.php
Normal file
@ -0,0 +1,88 @@
|
||||
@php
|
||||
// $psections: 車種区分コレクション
|
||||
// $ptypes: 駐輪分類コレクション
|
||||
// $parkId: 駐輪場ID
|
||||
// $record: 編集用レコード(null = 新規)
|
||||
$selectedPsection = old('psection_id', $record->psection_id ?? null);
|
||||
$selectedPtype = old('ptype_id', $record->ptype_id ?? null);
|
||||
$regulationsText = old('regulations_text', $record->regulations_text ?? '');
|
||||
@endphp
|
||||
|
||||
<input type="hidden" name="park_id" value="{{ $parkId }}">
|
||||
|
||||
<input type="hidden" name="park_id" value="{{ $parkId }}">
|
||||
|
||||
{{-- 駐輪場名(編集不可) --}}
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 col-form-label font-weight-bold">駐輪場名</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
value="{{ $park->park_name }}"
|
||||
readonly>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 車種区分 --}}
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 col-form-label font-weight-bold">車種区分 <span class="text-danger">*</span></label>
|
||||
<div class="col-md-10">
|
||||
<select name="psection_id" class="form-control" required>
|
||||
<option value="">選択してください</option>
|
||||
@foreach($psections as $ps)
|
||||
<option value="{{ $ps->psection_id }}"
|
||||
@selected($selectedPsection == $ps->psection_id)>
|
||||
{{ $ps->psection_subject }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 駐輪分類 --}}
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 col-form-label font-weight-bold">駐輪分類 <span class="text-danger">*</span></label>
|
||||
<div class="col-md-10">
|
||||
<select name="ptype_id" class="form-control" required>
|
||||
<option value="">選択してください</option>
|
||||
@foreach($ptypes as $pt)
|
||||
<option value="{{ $pt->ptype_id }}"
|
||||
@selected($selectedPtype == $pt->ptype_id)>
|
||||
{{ $pt->ptype_subject }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 駐輪規定 --}}
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 col-form-label font-weight-bold">
|
||||
駐輪規定 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="col-md-10">
|
||||
<textarea name="regulations_text"
|
||||
class="form-control js-summernote"
|
||||
required>{!! $regulationsText !!}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
$('.js-summernote').summernote({
|
||||
height: 420,
|
||||
minHeight: 420,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'italic', 'clear']],
|
||||
['fontname', ['fontname']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture']],
|
||||
['view', ['fullscreen', 'codeview', 'help']]
|
||||
]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
43
resources/views/admin/parking_regulations/add.blade.php
Normal file
43
resources/views/admin/parking_regulations/add.blade.php
Normal file
@ -0,0 +1,43 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '駐輪規定 新規登録')
|
||||
|
||||
@section('content')
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="m-0 text-dark">新規</h1>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">ホーム</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('parking_regulations_list', ['park_id' => $parkId]) }}">駐輪規定マスタ</a></li>
|
||||
<li class="breadcrumb-item active">新規登録</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">@foreach($errors->all() as $e) <div>{{ $e }}</div> @endforeach</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('parking_regulations_add') }}">
|
||||
@csrf
|
||||
@include('admin.parking_regulations._form', ['psections' => $psections, 'ptypes' => $ptypes, 'parkId' => $parkId, 'record' => null])
|
||||
|
||||
<div class="text-end">
|
||||
<button type="button" class="btn btn-primary register">登録</button>
|
||||
<a href="{{ route('parking_regulations_list', ['park_id' => $parkId]) }}" class="btn btn-default">戻る</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
53
resources/views/admin/parking_regulations/edit.blade.php
Normal file
53
resources/views/admin/parking_regulations/edit.blade.php
Normal file
@ -0,0 +1,53 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '駐輪規定 編集')
|
||||
|
||||
@section('content')
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="m-0 text-dark">編集</h1>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">ホーム</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('parking_regulations_list', ['park_id' => $park->park_id]) }}">駐輪規定マスタ</a></li>
|
||||
<li class="breadcrumb-item active">編集</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">@foreach($errors->all() as $e) <div>{{ $e }}</div> @endforeach</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<!-- 登録フォーム -->
|
||||
<form id="form_edit" method="POST" action="{{ route('parking_regulations_update', ['seq' => $record->parking_regulations_seq]) }}">
|
||||
@csrf
|
||||
@include('admin.parking_regulations._form', ['psections' => $psections, 'ptypes' => $ptypes, 'parkId' => $park->park_id, 'record' => $record])
|
||||
|
||||
<div class="text-end">
|
||||
<button type="button" id="register_edit" class="btn btn-success">登録</button>
|
||||
</form>
|
||||
|
||||
<!-- 削除フォーム -->
|
||||
<form id="form_delete" method="POST" action="{{ route('parking_regulations_delete') }}" style="display:inline">
|
||||
@csrf
|
||||
<input type="hidden" name="parking_regulations_seq" value="{{ $record->parking_regulations_seq }}">
|
||||
<button type="button" id="delete_edit" class="btn btn-danger">削除</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ route('parking_regulations_list', ['park_id' => $park->park_id]) }}" class="btn btn-secondary">戻る</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
109
resources/views/admin/parking_regulations/list.blade.php
Normal file
109
resources/views/admin/parking_regulations/list.blade.php
Normal file
@ -0,0 +1,109 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '駐輪規定マスタ')
|
||||
|
||||
@section('content')
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="m-0 text-dark">駐輪規定マスタ</h1>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">ホーム</a></li>
|
||||
<li class="breadcrumb-item active">駐輪規定マスタ</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
@foreach ($errors->all() as $e)
|
||||
<div>{{ $e }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">駐輪場情報</h3>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div style="width:180px;" class="font-weight-bold">
|
||||
駐輪場名
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
{{ $park->park_name ?? '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
|
||||
<a href="{{ route('parking_regulations_add', ['park_id' => $parkId]) }}" class="btn btn-primary mr-2">新規</a>
|
||||
{{-- 件数 + ページネーション --}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 px-3 pt-3">
|
||||
<div class="text-right ml-auto">
|
||||
<div>
|
||||
全 {{ $regulations->total() }} 件中
|
||||
{{ $regulations->firstItem() }}〜{{ $regulations->lastItem() }} 件を表示
|
||||
</div>
|
||||
<div>
|
||||
{{ $regulations->appends(request()->query())->links('pagination') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width:100px">操作</th>
|
||||
<th style="width:15%">車種区分</th>
|
||||
<th style="width:15%">駐輪分類</th>
|
||||
<th>駐輪規定</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style="background:#fff;">
|
||||
@forelse($regulations as $r)
|
||||
<tr>
|
||||
<td class="text-center align-middle">
|
||||
<a href="{{ route('parking_regulations_edit', ['seq' => $r->parking_regulations_seq]) }}" class="btn btn-sm btn-outline-primary">編集</a>
|
||||
<form method="POST" action="{{ route('parking_regulations_delete') }}" style="display:inline">
|
||||
@csrf
|
||||
<input type="hidden" name="parking_regulations_seq" value="{{ $r->parking_regulations_seq }}">
|
||||
</form>
|
||||
</td>
|
||||
<td>{{ $r->psection_subject ?? $r->psection_id }}</td>
|
||||
<td>{{ $r->ptype_subject ?? $r->ptype_id }}</td>
|
||||
<td class="regulations-html">{!! $r->regulations_text !!}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">規定がありません。</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
{{ $regulations->appends(request()->query())->links('pagination') }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@ -2,8 +2,8 @@
|
||||
@section('title', '編集')
|
||||
|
||||
@section('content')
|
||||
<!-- Content Header -->
|
||||
<div class="content-header">
|
||||
{{-- ▼ パンくず --}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-6">
|
||||
@ -11,50 +11,67 @@
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ route('home') }}">ホーム</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{ route('parks.index') }}">駐輪場マスタ</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">ホーム</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('parks') }}">駐輪場マスタ</a></li>
|
||||
<li class="breadcrumb-item active">編集</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
|
||||
{{-- 編集フォーム --}}
|
||||
<form id="form_edit"
|
||||
method="POST"
|
||||
action="{{ route('parks.update', ['id' => $record->park_id]) }}?back={{ urlencode(request()->get('back', request()->fullUrl())) }}"
|
||||
enctype="multipart/form-data">
|
||||
{{-- ▼ 編集フォーム --}}
|
||||
<div class="card shadow">
|
||||
<form id="park-edit-form" method="POST" action="{{ route('parks.update', $park->park_id) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@include('admin.parks._form', [
|
||||
'record' => $record,
|
||||
'isEdit' => true
|
||||
])
|
||||
<div class="card-header">
|
||||
{{-- ▼ ボタンエリア(上部) --}}
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-default mt-2 btn-submit">登録</button>
|
||||
<a href="javascript:void(0)" class="btn btn-default mt-2">減免確認編集</a>
|
||||
<a href="javascript:void(0)" class="btn btn-default mt-2">駐輪状況編集</a>
|
||||
<button type="button" class="btn btn-default mt-2">削除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{-- ▼ 入力フォーム --}}
|
||||
@include('admin.parks._form')
|
||||
|
||||
{{-- ▼ フッター(下部ボタン) --}}
|
||||
<div class="form-footer mt-4 text-end">
|
||||
<button type="button" class="btn btn-default btn-submit">登録</button>
|
||||
<a href="{{ route('parks') }}" class="btn btn-default">戻る</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{-- 削除フォーム(hidden) --}}
|
||||
<form id="form_delete" method="POST" action="{{ route('parks.destroy') }}" class="d-none">
|
||||
@csrf
|
||||
<input type="hidden" name="pk[]" value="{{ $record->park_id }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<!-- jQuery Confirm -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-confirm@3.3.4/css/jquery-confirm.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery-confirm@3.3.4/js/jquery-confirm.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
$('.btn-submit').on('click', function () {
|
||||
$.confirm({
|
||||
title: '登録確認',
|
||||
content: 'この内容で登録してよろしいですか?',
|
||||
buttons: {
|
||||
はい: {
|
||||
btnClass: 'btn-primary',
|
||||
action: function () {
|
||||
$('#park-edit-form').submit();
|
||||
}
|
||||
},
|
||||
いいえ: function () {}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
68
resources/views/admin/reduction_confirm/_form.blade.php
Normal file
68
resources/views/admin/reduction_confirm/_form.blade.php
Normal file
@ -0,0 +1,68 @@
|
||||
{{-- 減免確認マスタテーブル行(各利用者分類) --}}
|
||||
@php
|
||||
// reduction_confirm テーブルから取得したレコード
|
||||
$userCategoryId = $row->user_categoryid;
|
||||
$checkType = $row->reduction_confirm_type;
|
||||
// use numeric index passed from parent to avoid name collisions when duplicate user_categoryid exist
|
||||
$idx = $index ?? ($userCategoryId);
|
||||
$oldCheckType = old("reduction_confirm_type.$idx", $checkType);
|
||||
|
||||
// 学生分類かどうかを判定
|
||||
$isStudent = ($row->usertype_subject1 ?? '') === '学生';
|
||||
$rowClass = $isStudent ? 'table-secondary' : '';
|
||||
$isDisabled = $isStudent;
|
||||
@endphp
|
||||
|
||||
<tr class="{{ $rowClass }}">
|
||||
<td class="text-center">{{ $userCategoryId }}</td>
|
||||
<td>{{ $row->usertype_subject1 ?? '-' }}</td>
|
||||
<td>{{ $row->usertype_subject2 ?? '-' }}</td>
|
||||
<td>{{ $row->usertype_subject3 ?? '-' }}</td>
|
||||
<td>
|
||||
{{-- hidden mapping to know which user_categoryid this input row targets --}}
|
||||
<input type="hidden" name="row_user_categoryid[{{ $idx }}]" value="{{ $userCategoryId }}">
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
{{-- 確認しない(0) --}}
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio"
|
||||
id="check_type_{{ $idx }}_0"
|
||||
name="reduction_confirm_type[{{ $idx }}]"
|
||||
value="0"
|
||||
class="custom-control-input"
|
||||
@checked($oldCheckType == 0)
|
||||
@disabled($isDisabled)>
|
||||
<label class="custom-control-label" for="check_type_{{ $idx }}_0">
|
||||
確認しない
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- 年1回(1) --}}
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio"
|
||||
id="check_type_{{ $idx }}_1"
|
||||
name="reduction_confirm_type[{{ $idx }}]"
|
||||
value="1"
|
||||
class="custom-control-input"
|
||||
@checked($oldCheckType == 1)
|
||||
@disabled($isDisabled)>
|
||||
<label class="custom-control-label" for="check_type_{{ $idx }}_1">
|
||||
年1回
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- 毎更新時(2) --}}
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio"
|
||||
id="check_type_{{ $idx }}_2"
|
||||
name="reduction_confirm_type[{{ $idx }}]"
|
||||
value="2"
|
||||
class="custom-control-input"
|
||||
@checked($oldCheckType == 2)
|
||||
@disabled($isDisabled)>
|
||||
<label class="custom-control-label" for="check_type_{{ $idx }}_2">
|
||||
毎更新時
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
151
resources/views/admin/reduction_confirm/list.blade.php
Normal file
151
resources/views/admin/reduction_confirm/list.blade.php
Normal file
@ -0,0 +1,151 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '減免確認マスタ')
|
||||
|
||||
@section('content')
|
||||
<!-- Content Header -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="m-0 text-dark">減免確認マスタ</h1>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<ol class="breadcrumb float-sm-right text-sm">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">ホーム</a></li>
|
||||
<li class="breadcrumb-item active">減免確認マスタ</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
{{-- エラーメッセージ表示 --}}
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="alert-heading">エラーが発生しました</h4>
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 成功メッセージ表示 --}}
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 駐輪場情報カード -->
|
||||
<div class="col-lg-12 px-0">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">駐輪場情報</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2">
|
||||
<label class="font-weight-bold mb-0">駐輪場名</label>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="park-name-display">
|
||||
{{ $park->park_name ?? '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- 減免確認マスタ テーブル -->
|
||||
<div class="col-lg-12 px-0 ">
|
||||
<form action="{{ route('reduction_confirm_store') }}" method="POST" id="reduction-form">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
{{-- 左側:登録ボタン --}}
|
||||
<button type="submit" class="btn btn-primary btn-submit">登録</button>
|
||||
|
||||
{{-- 右側:件数 + ページネーション --}}
|
||||
<div class="text-right">
|
||||
<div>
|
||||
全 {{ $reductionData->total() }} 件中
|
||||
{{ $reductionData->firstItem() }}〜{{ $reductionData->lastItem() }} 件を表示
|
||||
</div>
|
||||
<div>
|
||||
{{ $reductionData->appends(keepUserListQuery())->links('pagination') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@csrf
|
||||
<input type="hidden" name="park_id" value="{{ $parkId }}">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 5%; text-align: center;">利用者分類</th>
|
||||
<th style="width: 10%;">分類名1</th>
|
||||
<th style="width: 10%;">分類名2</th>
|
||||
<th style="width: 10%;">分類名3</th>
|
||||
<th style="width: 20%; text-align: center;">減免確認種別</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($reductionData as $row)
|
||||
@include('admin.reduction_confirm._form', ['row' => $row, 'index' => $loop->index])
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">
|
||||
利用者分類がありません。
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.gap-3 {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.custom-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-control-label {
|
||||
margin-bottom: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.park-name-display {
|
||||
background-color: #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
font-size: 16px;
|
||||
color: #6c757d;
|
||||
width: fit-content;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.sample03-wrapper {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@ -32,6 +32,26 @@
|
||||
</select>
|
||||
</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">
|
||||
<label class="col-sm-2 col-form-label fw-bold py-0">発行日</label>
|
||||
<div class="col-sm-10">
|
||||
@ -76,6 +96,7 @@
|
||||
<div class="col-sm-12 text-end">
|
||||
<button type="submit" class="btn btn-default me-2">絞り込み</button>
|
||||
<a href="{{ route('seals') }}" class="btn btn-default">解除</a>
|
||||
<button type="submit" class="btn btn-default me-2">CSV出力</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -30,14 +30,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 会員ID --}}
|
||||
{{-- 会員ID
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">会員ID <span class="text-danger">*</span></label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="member_id" class="form-control form-control-sm"
|
||||
value="{{ $value('member_id') }}" placeholder="会員ID">
|
||||
</div>
|
||||
</div>
|
||||
</div>--}}
|
||||
|
||||
{{-- パスワード --}}
|
||||
<div class="form-group row">
|
||||
@ -164,16 +164,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
{{--<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">居場所通知用QRID</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_place_qrid" value="{{ old('user_place_qrid') }}" class="form-control">
|
||||
</div>
|
||||
</div>--}}
|
||||
|
||||
{{-- 利用者分類 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">分類名1(一般、学生)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 利用者属性 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">利用者分類ID</label>
|
||||
<label class="col-md-2 col-form-label">分類名2(区民、非区民)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">分類名3(減免種別)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
@ -197,6 +211,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 性別 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">性別</label>
|
||||
<div class="col-md-10">
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="user_gender"
|
||||
id="gender_male"
|
||||
value="1"
|
||||
{{ $value('user_gender') === '1' ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="gender_male">男</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="user_gender"
|
||||
id="gender_female"
|
||||
value="2"
|
||||
{{ $value('user_gender') === '2' ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="gender_female">女</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input"
|
||||
type="radio"
|
||||
name="user_gender"
|
||||
id="gender_unknown"
|
||||
value="0"
|
||||
{{ $value('user_gender') === '0' || $value('user_gender') === null ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="gender_unknown">未入力</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 生年月日 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">生年月日</label>
|
||||
<div class="col-md-10">
|
||||
<input type="date" name="user_birthdate" class="form-control form-control-sm"
|
||||
value="{{ $value('user_birthdate') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 年齢 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">年齢</label>
|
||||
<div class="col-md-10">
|
||||
|
||||
@php
|
||||
$age = null;
|
||||
if ($value('user_birthdate')) {
|
||||
$age = \Carbon\Carbon::parse($value('user_birthdate'))
|
||||
->age;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
value="{{ $age !== null ? $age : '' }}"
|
||||
readonly>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 自宅電話番号 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">自宅電話番号</label>
|
||||
@ -298,16 +380,20 @@
|
||||
<label class="col-md-2 col-form-label">区民</label>
|
||||
<div class="col-md-10">
|
||||
<select name="ward_residents" class="form-control form-control-sm">
|
||||
<option value="1" {{ $value('ward_residents') == 1 ? 'selected' : '' }}>
|
||||
区民
|
||||
<option value="1"
|
||||
{{ old('ward_residents', $value('ward_residents')) == 1 ? 'selected' : '' }}>
|
||||
居民
|
||||
</option>
|
||||
<option value="0" {{ $value('ward_residents') == 0 ? 'selected' : '' }}>
|
||||
非区民
|
||||
|
||||
<option value="0"
|
||||
{{ old('ward_residents', $value('ward_residents')) == 0 ? 'selected' : '' }}>
|
||||
非居民
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- 勤務先 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">勤務先</label>
|
||||
@ -335,6 +421,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 本人確認書類 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">本人確認書類</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_idcard" class="form-control form-control-sm"
|
||||
value="{{ $value('user_idcard') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 本人確認チェック済(5状態) --}}
|
||||
@php
|
||||
$options = [
|
||||
@ -503,6 +598,69 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者氏名</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者フリガナ</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者生年月日</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者住所</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者電話番号</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者予備電話番号</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者メールアドレス</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">通知方法</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">個人情報同意フラグ</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-success" id="register_edit">登録</button>
|
||||
|
||||
|
||||
@ -45,13 +45,13 @@
|
||||
<div class="card-body">
|
||||
|
||||
{{-- ▼ 1列レイアウト(ラベル左 / 入力右) --}}
|
||||
<div class="form-group row">
|
||||
{{--<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">会員ID <span class="text-danger">*</span></label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_id" value="{{ old('user_id') }}" class="form-control"
|
||||
placeholder="会員ID">
|
||||
</div>
|
||||
</div>
|
||||
</div>--}}
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">パスワード</label>
|
||||
@ -199,16 +199,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
{{-- <div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">居場所通知用QRID</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_place_qrid" value="{{ old('user_place_qrid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>--}}
|
||||
|
||||
{{-- 利用者属性 --}}
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">利用者分類ID</label>
|
||||
<label class="col-md-2 col-form-label">分類名1(一般、学生)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">分類名2(区民、非区民)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">分類名3(減免種別)</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_categoryid" value="{{ old('user_categoryid') }}" class="form-control">
|
||||
</div>
|
||||
@ -349,18 +363,9 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">区民</label>
|
||||
<div class="col-md-10">
|
||||
<select name="ward_residents" class="form-control">
|
||||
<option value="">-- 选择 --</option>
|
||||
<option value="1" {{ old('ward_residents') == '1' ? 'selected' : '' }}>
|
||||
区民
|
||||
</option>
|
||||
<option value="0" {{ old('ward_residents') == '0' ? 'selected' : '' }}>
|
||||
非区民
|
||||
</option>
|
||||
</select>
|
||||
<input type="text" name="ward_residents" value="{{ old('ward_residents') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">勤務先</label>
|
||||
<div class="col-md-10">
|
||||
@ -532,6 +537,70 @@
|
||||
<textarea name="user_remarks" rows="4" class="form-control">{{ old('user_remarks') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者氏名</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者フリガナ</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者生年月日</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者住所</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者電話番号</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者予備電話番号</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">親権者メールアドレス</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">通知方法</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 col-form-label">個人情報同意フラグ</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="user_chk_opeid" value="{{ old('user_chk_opeid') }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success register">登録</button>
|
||||
<a href="{{ route('users', keepUserListQuery()) }}"class="btn btn-secondary">戻る</a>
|
||||
</div>
|
||||
|
||||
@ -303,7 +303,16 @@
|
||||
<th style="width:150px;">本人確認チェック済</th>
|
||||
<th style="width:160px;">本人確認日時</th>
|
||||
<th style="width:110px;">退会フラグ</th>
|
||||
<th style="width:140px;">退会日</th>
|
||||
<th style="width:110px;">退会日</th>
|
||||
<th style="width:140px;">親権者氏名</th>
|
||||
<th style="width:140px;">親権者フリガナ</th>
|
||||
<th style="width:140px;">親権者生年月日</th>
|
||||
<th style="width:140px;">親権者住所</th>
|
||||
<th style="width:140px;">親権者電話番号</th>
|
||||
<th style="width:140px;">親権者予備電話番号</th>
|
||||
<th style="width:140px;">親権者メールアドレス</th>
|
||||
<th style="width:140px;">通知方法</th>
|
||||
<th style="width:140px;">個人情報同意フラグ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -355,6 +364,15 @@
|
||||
<td>{{ $item->user_quit_flag ? 'はい' : 'いいえ' }}</td>
|
||||
<td>{{ $item->user_quitday ? \Illuminate\Support\Str::limit($item->user_quitday, 10, '') : '' }}
|
||||
</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
<td>{{ '' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
||||
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
|
||||
159
resources/views/auth/password-change-success.blade.php
Normal file
159
resources/views/auth/password-change-success.blade.php
Normal file
@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>パスワード変更完了 - So-Manager</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.success-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 30px;
|
||||
background-color: #28a745;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.countdown-timer {
|
||||
display: inline-block;
|
||||
background-color: #e7f3ff;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.return-login-btn {
|
||||
padding: 14px 30px;
|
||||
background-color: #f5f5f5;
|
||||
border: 2px solid #333;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.return-login-btn:hover {
|
||||
background-color: #e8e8e8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.return-login-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="success-container">
|
||||
{{-- 成功アイコン --}}
|
||||
<div class="success-icon">✓</div>
|
||||
|
||||
{{-- 成功タイトル --}}
|
||||
<div class="success-title">パスワード変更完了</div>
|
||||
|
||||
{{-- 成功メッセージ --}}
|
||||
<div class="success-message">
|
||||
パスワードが正常に変更されました。<br>
|
||||
システムは安全に保護されています。
|
||||
</div>
|
||||
|
||||
{{-- カウントダウン --}}
|
||||
<div class="countdown">
|
||||
<span id="countdown-text">自動的にログイン画面に戻ります:</span><br>
|
||||
<div class="countdown-timer">
|
||||
<span id="countdown-timer">10</span> 秒
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 手動でログイン画面に戻るボタン --}}
|
||||
<a href="{{ route('logout') }}" class="return-login-btn">
|
||||
ログイン画面へ
|
||||
</a>
|
||||
|
||||
{{-- 補足テキスト --}}
|
||||
<div class="info-text">
|
||||
※ 自動的にログイン画面に遷移しない場合は、上記ボタンをクリックしてください。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 10秒後に自動的にログアウトしてログイン画面へリダイレクト
|
||||
let countdownSeconds = 10;
|
||||
const countdownElement = document.getElementById('countdown-timer');
|
||||
const countdownTextElement = document.getElementById('countdown-text');
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
countdownSeconds--;
|
||||
countdownElement.textContent = countdownSeconds;
|
||||
|
||||
if (countdownSeconds <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
// ログアウト処理を実行してからログイン画面へ遷移
|
||||
window.location.href = "{{ route('logout') }}";
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
259
resources/views/auth/password-change.blade.php
Normal file
259
resources/views/auth/password-change.blade.php
Normal file
@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>パスワード変更 - So-Manager</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.password-change-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 3px solid #007bff;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.alert-warning-custom {
|
||||
background-color: #fff3cd;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.alert-warning-custom .text-danger {
|
||||
color: #dc3545;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-group input[type="password"] {
|
||||
padding: 12px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group input[type="password"]:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-group input.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.form-group input.is-invalid:focus {
|
||||
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: #dc3545;
|
||||
font-size: 13px;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.requirements {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #dc3545;
|
||||
padding: 15px;
|
||||
margin-bottom: 25px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.requirements ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.requirements li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background-color: #f5f5f5;
|
||||
border: 2px solid #333;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #e8e8e8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 6px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 20px;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.error-message ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.error-message li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.password-change-container {
|
||||
max-width: 720px;
|
||||
/* 或者 800 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="password-change-container">
|
||||
{{-- パネルタイトル --}}
|
||||
<div class="panel-title">So-Manager 管理パネル</div>
|
||||
|
||||
{{-- バリデーションエラー表示 --}}
|
||||
@if ($errors->any())
|
||||
<div class="error-message">
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 警告メッセージ --}}
|
||||
<div class="alert-warning-custom">
|
||||
<p>
|
||||
セキュリティを維持するため、
|
||||
<span class="text-danger">
|
||||
パスワードの有効期限が切れました(3ヶ月)。<br>
|
||||
</span>
|
||||
<span class="text-danger">
|
||||
新しいパスワードへの変更をお願いいたします。<br>
|
||||
</span>
|
||||
安全にご利用いただくため、定期的なパスワード更新が必要です。<br>
|
||||
変更後、引き続きシステムをご利用いただけます。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- パスワード変更フォーム --}}
|
||||
<form action="{{ route('password.change.update') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
{{-- 当前パスワード --}}
|
||||
<div class="form-group">
|
||||
<label for="current_password">現在のパスワード:</label>
|
||||
<input type="password" class="@error('current_password') is-invalid @enderror" id="current_password"
|
||||
name="current_password" required minlength="8" maxlength="64">
|
||||
@error('current_password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- 新パスワード --}}
|
||||
<div class="form-group">
|
||||
<label for="password">新しいパスワード:</label>
|
||||
<input type="password" class="@error('password') is-invalid @enderror" id="password" name="password"
|
||||
required minlength="8" maxlength="64" pattern="[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:'\",.<>?/\\|`~]+">
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- 新パスワード確認 --}}
|
||||
<div class="form-group">
|
||||
<label for="password_confirmation">新しいパスワード(確認):</label>
|
||||
<input type="password" class="@error('password_confirmation') is-invalid @enderror"
|
||||
id="password_confirmation" name="password_confirmation" required minlength="8" maxlength="64"
|
||||
pattern="[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:'\",.<>?/\\|`~]+">
|
||||
@error('password_confirmation')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- 要件説明 --}}
|
||||
<div class="requirements">
|
||||
<strong>※新しいパスワードは、以下の条件を満たす必要があります。</strong>
|
||||
<ul>
|
||||
<li>8文字以上64文字以内</li>
|
||||
<li>半角英数字および記号のみ使用可能</li>
|
||||
<li>現在のパスワードと同一のものは使用できません</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{-- Hidden フィールド(設計書に記載) --}}
|
||||
<input type="hidden" name="updated_at" value="{{ now()->format('Y-m-d H:i:s') }}">
|
||||
<input type="hidden" name="ope_pass_changed_at" value="{{ now()->format('Y-m-d H:i:s') }}">
|
||||
|
||||
{{-- 送信ボタン & キャンセルボタン --}}
|
||||
<div class="button-group">
|
||||
<button type="submit" class="submit-btn">
|
||||
パスワードを変更する
|
||||
</button>
|
||||
<a href="{{ route('logout') }}" class="cancel-btn">
|
||||
ログイン画面へ
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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,7 +1,7 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'インフォメーション')
|
||||
@section('content')
|
||||
@php
|
||||
@php
|
||||
use App\Models\OperatorLog;
|
||||
use App\Models\Ope;
|
||||
$logs = OperatorLog::orderByDesc('created_at')->limit(20)->get();
|
||||
@ -11,14 +11,18 @@
|
||||
$queClassNums = [];
|
||||
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] =
|
||||
$operatorNames[$log->operator_id] ??
|
||||
(\App\Models\Ope::find($log->operator_id)->ope_name ?? $log->operator_id);
|
||||
// operator_queからque_class, que_status取得
|
||||
$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();
|
||||
$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])) {
|
||||
if (!$que || !in_array($que->que_status, [3, 4])) {
|
||||
continue;
|
||||
}
|
||||
if ($que->que_class < 100) {
|
||||
@ -30,10 +34,11 @@
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
$queClassNums[$log->operator_log_id] = \App\Models\OperatorQue::QueClass[$que->que_class] ?? $que->que_class;
|
||||
$queClassNums[$log->operator_log_id] =
|
||||
\App\Models\OperatorQue::QueClass[$que->que_class] ?? $que->que_class;
|
||||
}
|
||||
@endphp
|
||||
<div class="container-fluid" style="background:#f4f6f9;min-height:calc(100vh - 60px);">
|
||||
@endphp
|
||||
<div class="container-fluid" style="background:#f4f6f9;min-height:calc(100vh - 60px);">
|
||||
<div class="row">
|
||||
<!-- メイン -->
|
||||
<div class="col-md-12">
|
||||
@ -43,34 +48,69 @@
|
||||
<div class="card-header bg-white font-weight-bold">インフォメーション</div>
|
||||
<div class="card-body">
|
||||
@php
|
||||
$infoQue = \App\Models\OperatorQue::whereIn('que_status', [1,2])
|
||||
->orderByDesc('created_at')
|
||||
->limit(5)
|
||||
->get();
|
||||
// ▼ 各メニューの件数を取得(que_status は運用に合わせて調整)
|
||||
// 例:未処理/処理中を 1,2 として集計
|
||||
$statusTargets = [1, 2];
|
||||
|
||||
// ▼ QueClass は実際の定義に合わせて値を変更してください
|
||||
// ここでは例として que_class を使って種別を分ける想定
|
||||
$cntPersonalCheck = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||
->whereIn('que_class', [
|
||||
/* 本人確認の que_class を入れる */
|
||||
])
|
||||
->count();
|
||||
|
||||
$cntCancelRequest = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||
->whereIn('que_class', [
|
||||
/* 解約リクエストの que_class を入れる */
|
||||
])
|
||||
->count();
|
||||
|
||||
$cntUserInfoChange = \App\Models\OperatorQue::whereIn('que_status', $statusTargets)
|
||||
->whereIn('que_class', [
|
||||
/* ユーザー情報変更の que_class を入れる */
|
||||
])
|
||||
->count();
|
||||
|
||||
// ▼ 0埋め(000件 表示用)
|
||||
$fmt = fn($n) => str_pad((string) $n, 3, '0', STR_PAD_LEFT) . '件';
|
||||
@endphp
|
||||
@if(count($infoQue) > 0)
|
||||
@foreach($infoQue as $q)
|
||||
|
||||
{{-- 本人確認処理 --}}
|
||||
<div class="card mb-3" style="box-shadow:none;border:1px solid #e0e0e0;">
|
||||
<div class="card-body py-3 px-4" style="position:relative;">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="text-secondary" style="font-size:1.1em; margin-right:1em;">
|
||||
{{ $q->created_at }}
|
||||
</div>
|
||||
@if($q->que_class > 99)
|
||||
<span class="badge badge-danger" style="font-size:0.95em;">ハード異常</span>
|
||||
@elseif($q->que_class < 100)
|
||||
<span class="badge badge-primary" style="font-size:0.95em;">タスク</span>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url('/admin/manual_personal_check/edit/'.$q->que_id) }}" style="font-weight:bold;color:#007bff;font-size:1.1em;text-decoration:underline;">
|
||||
{{ $q->getQueClassLabel() }}
|
||||
<div class="card-body py-3 px-4 d-flex align-items-center ">
|
||||
<a href="{{ route('personal') }}"
|
||||
style="font-weight:bold;color:#007bff;font-size:1.05em;">
|
||||
本人確認処理
|
||||
</a>
|
||||
<span class="badge badge-primary ml-auto"
|
||||
style="font-size:0.95em;">{{ $fmt($cntPersonalCheck) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 解約リクエスト --}}
|
||||
<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>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -90,8 +130,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@php $hasData = false; @endphp
|
||||
@foreach($logs as $log)
|
||||
@if(isset($queLabels[$log->operator_log_id]))
|
||||
@foreach ($logs as $log)
|
||||
@if (isset($queLabels[$log->operator_log_id]))
|
||||
@php $hasData = true; @endphp
|
||||
<tr>
|
||||
<td>{!! $queIcons[$log->operator_log_id] !!}{!! $queLabels[$log->operator_log_id] !!}</td>
|
||||
@ -102,8 +142,10 @@
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
@if(!$hasData)
|
||||
<tr><td colspan="5" class="text-center">データがありません</td></tr>
|
||||
@if (!$hasData)
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">データがありません</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
@ -113,5 +155,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
30
resources/views/placeholder.blade.php
Normal file
30
resources/views/placeholder.blade.php
Normal file
@ -0,0 +1,30 @@
|
||||
@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">このページは未実装です</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">準備中です</h4>
|
||||
<p>このページは現在準備中です。</p>
|
||||
<hr>
|
||||
<p class="mb-0">リクエストコード: {{ request()->query('code', 'N/A') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
167
routes/web.php
167
routes/web.php
@ -42,6 +42,10 @@ use App\Http\Controllers\Admin\MailTemplateController;
|
||||
use App\Http\Controllers\Admin\InvSettingController;
|
||||
use App\Http\Controllers\Admin\ZoneController;
|
||||
use App\Http\Controllers\Admin\PplaceController;
|
||||
use App\Http\Controllers\Admin\ReductionConfirmMasterController;
|
||||
use App\Http\Controllers\Admin\ParkingRegulationsController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\Auth\EmailOtpController;
|
||||
|
||||
|
||||
/**
|
||||
@ -70,16 +74,39 @@ Route::middleware('guest')->group(function () {
|
||||
|
||||
});
|
||||
|
||||
// ログアウトルート(認証済みユーザー専用)
|
||||
Route::get('logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])->middleware('auth');
|
||||
// ログアウトルート(認証済みユーザー専用、OTP チェック対象外)
|
||||
Route::get('logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])
|
||||
->middleware(['auth'])
|
||||
->name('logout');
|
||||
|
||||
|
||||
|
||||
// 保護されたルート(認証済みユーザー専用)
|
||||
// Laravel 12変更点:middleware()をコントローラーではなくルートで指定
|
||||
Route::middleware('auth')->group(function () {
|
||||
// OTP メール認証ルート(OTP チェック対象外、ここに配置)
|
||||
// ログイン直後に 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');
|
||||
});
|
||||
|
||||
// パスワード変更ルート(OTP チェック対象外、有効期限チェック対象)
|
||||
// 定期的なパスワード変更が必須(3ヶ月ごと)
|
||||
Route::prefix('password')->name('password.')->group(function () {
|
||||
Route::get('/change', [App\Http\Controllers\Auth\PasswordChangeController::class, 'showChangeForm'])->name('change.show');
|
||||
Route::post('/change', [App\Http\Controllers\Auth\PasswordChangeController::class, 'updatePassword'])->name('change.update');
|
||||
Route::get('/success', [App\Http\Controllers\Auth\PasswordChangeController::class, 'showSuccessPage'])->name('change.success');
|
||||
})->middleware('check.password.change.required');
|
||||
|
||||
// 以下のすべてのルートに OTP 認証チェックミドルウェアを適用
|
||||
Route::middleware('ensure.otp.verified')->group(function () {
|
||||
// ダッシュボード(ホーム画面)
|
||||
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
|
||||
// パスワード有効期限チェック対象:3ヶ月超過時は強制リダイレクト
|
||||
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])
|
||||
->name('home')
|
||||
->middleware('check.password.change.required');
|
||||
|
||||
// Laravel 12 移行時の一時的な占位符路由
|
||||
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義
|
||||
@ -150,35 +177,13 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/pplace/export', [PplaceController::class, 'export'])->name('pplaces_export');
|
||||
// sou end
|
||||
|
||||
// 市区マスタ(cities.*)
|
||||
Route::prefix('cities')->group(function () {
|
||||
|
||||
// 一覧(画面)
|
||||
Route::get('/', [CityController::class, 'index'])
|
||||
->name('cities.index');
|
||||
|
||||
// 新規(画面)
|
||||
Route::get('/create', [CityController::class, 'create'])
|
||||
->name('cities.create');
|
||||
|
||||
// 新規(登録)
|
||||
Route::post('/', [CityController::class, 'store'])
|
||||
->name('cities.store');
|
||||
|
||||
// 編集(画面)
|
||||
Route::get('/{id}/edit', [CityController::class, 'edit'])
|
||||
->whereNumber('id')
|
||||
->name('cities.edit');
|
||||
|
||||
// 編集(更新)
|
||||
Route::post('/{id}', [CityController::class, 'update'])
|
||||
->whereNumber('id')
|
||||
->name('cities.update');
|
||||
|
||||
// 削除(複数削除 pk[])
|
||||
Route::post('/delete', [CityController::class, 'destroy'])
|
||||
->name('cities.destroy');
|
||||
});
|
||||
// ou start
|
||||
// 市区マスタ
|
||||
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/edit/{id}', [CityController::class, 'edit'])->where(['id' => '[0-9]+'])->name('city_edit');
|
||||
Route::match(['get', 'post'], '/city/info/{id}', [CityController::class, 'info'])->where(['id' => '[0-9]+'])->name('city_info');
|
||||
Route::match(['get', 'post'], '/city/delete', [CityController::class, 'delete'])->name('city_delete');
|
||||
|
||||
// 駐輪場マスタ(parks.*)
|
||||
Route::prefix('parks')->group(function () {
|
||||
@ -319,46 +324,14 @@ Route::middleware('auth')->group(function () {
|
||||
});
|
||||
|
||||
|
||||
// 利用者分類マスタ(usertypes.*)
|
||||
Route::prefix('usertypes')->group(function () {
|
||||
|
||||
// 一覧(画面)
|
||||
Route::get('/', [UsertypeController::class, 'index'])
|
||||
->name('usertypes.index');
|
||||
|
||||
// 新規(画面)
|
||||
Route::get('/create', [UsertypeController::class, 'create'])
|
||||
->name('usertypes.create');
|
||||
|
||||
// 新規(登録)
|
||||
Route::post('/', [UsertypeController::class, 'store'])
|
||||
->name('usertypes.store');
|
||||
|
||||
// 編集(画面)
|
||||
Route::get('/{id}/edit', [UsertypeController::class, 'edit'])
|
||||
->whereNumber('id')
|
||||
->name('usertypes.edit');
|
||||
|
||||
// 編集(更新)
|
||||
Route::post('/{id}', [UsertypeController::class, 'update'])
|
||||
->whereNumber('id')
|
||||
->name('usertypes.update');
|
||||
|
||||
// 削除(複数削除 pk[])
|
||||
Route::post('/delete', [UsertypeController::class, 'destroy'])
|
||||
->name('usertypes.destroy');
|
||||
|
||||
// インポート
|
||||
Route::post('/import', [UsertypeController::class, 'import'])
|
||||
->name('usertypes.import');
|
||||
|
||||
// エクスポート
|
||||
Route::post('/export', [UsertypeController::class, 'export'])
|
||||
->name('usertypes.export');
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 利用者分類マスタ
|
||||
Route::match(['get', 'post'], '/usertypes', [UsertypeController::class, 'list'])->name('usertypes');
|
||||
Route::match(['get', 'post'], '/usertypes/add', [UsertypeController::class, 'add'])->name('usertype_add');
|
||||
Route::match(['get', 'post'], '/usertypes/edit/{id}', [UsertypeController::class, 'edit'])->name('usertype_edit')->where(['id' => '[0-9]+']);
|
||||
Route::match(['get', 'post'], '/usertypes/info/{id}', [UsertypeController::class, 'info'])->name('usertype_info')->where(['id' => '[0-9]+']);
|
||||
Route::match(['get', 'post'], '/usertypes/delete', [UsertypeController::class, 'delete'])->name('usertypes_delete');
|
||||
Route::match(['get', 'post'], '/usertypes/import', [UsertypeController::class, 'import'])->name('usertypes_import');
|
||||
Route::match(['get', 'post'], '/usertypes/export', [UsertypeController::class, 'export'])->name('usertypes_export');
|
||||
|
||||
|
||||
|
||||
@ -380,6 +353,7 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
// 常時表示インフォメーション
|
||||
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');
|
||||
|
||||
// タグ発行キュー処理、履歴表示
|
||||
@ -610,7 +584,54 @@ Route::middleware('auth')->group(function () {
|
||||
->name('zones_delete');
|
||||
|
||||
//kin end
|
||||
});
|
||||
|
||||
Route::get('/opes/{id}/permissions', [OpeController::class, 'getPermissionsByFeature'])
|
||||
->where(['id' => '[0-9]+'])
|
||||
->name('opes.permissions_by_feature');
|
||||
|
||||
// 仮ルート(未実装機能用)
|
||||
Route::get('/placeholder', function () {
|
||||
return view('placeholder');
|
||||
})->name('placeholder');
|
||||
|
||||
Route::get('/refund', function () {
|
||||
return view('placeholder');
|
||||
})->name('refund'); // SWA-13 返金処理
|
||||
|
||||
Route::get('/tag-reissue', function () {
|
||||
return view('placeholder');
|
||||
})->name('tag.reissue'); // SWA-05 タグ再発行
|
||||
|
||||
Route::get('/seal-reissue', function () {
|
||||
return view('placeholder');
|
||||
})->name('seal.reissue'); // SWA-06 シール再発行
|
||||
|
||||
Route::get('/sales-report', function () {
|
||||
return view('placeholder');
|
||||
})->name('sales.report'); // SWA-17 売上年報/月報/日報
|
||||
|
||||
Route::get('/sales-detail', function () {
|
||||
return view('placeholder');
|
||||
})->name('sales.detail'); // SWA-18 売上詳細
|
||||
|
||||
Route::get('/ptype', function () {
|
||||
return view('placeholder');
|
||||
})->name('ptype'); // SWA-29 駐輪分類マスタ
|
||||
|
||||
Route::get('/jurisdiction-parking', function () {
|
||||
return view('placeholder');
|
||||
})->name('jurisdiction.parking'); // SWA-37 駐輪規定読込記録
|
||||
|
||||
Route::get('/lottery-master', function () {
|
||||
return view('placeholder');
|
||||
})->name('lottery.master'); // SWA-44 駐輪場抽選応募マスタ
|
||||
|
||||
Route::get('/lottery-setting', function () {
|
||||
return view('placeholder');
|
||||
})->name('lottery.setting'); // SWA-45 抽選申込設定マスタ
|
||||
|
||||
}); // ensure.otp.verified ミドルウェアグループの閉じ括弧
|
||||
}); // auth ミドルウェアグループの閉じ括弧
|
||||
|
||||
|
||||
// Wellnet PUSH webhook (SHJ-4A)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user